Preact Client: Ladder (#1709)

This commit is contained in:
Adam Tran 2021-01-29 11:10:36 -05:00 committed by GitHub
parent 3e4b83298c
commit a9f8adfa39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 376 additions and 1 deletions

View File

@ -29,3 +29,5 @@ node_modules/
/js/panel-teambuilder-team.js
/js/panel-teamdropdown.js
/js/panel-battle.js
/js/panel-ladder.js
/js/panel-page.js

2
.gitignore vendored
View File

@ -42,6 +42,8 @@ package-lock.json
/js/panel-teamdropdown.js
/js/panel-battle.js
/js/replay-embed.js
/js/panel-ladder.js
/js/panel-page.js
/js/*.js.map
/replays/caches/

View File

@ -92,6 +92,8 @@
<script defer src="/js/battle-dex-search.js?"></script>
<script defer src="/js/battle-searchresults.js?"></script>
<script defer src="/js/panel-teambuilder-team.js?"></script>
<script defer src="/js/panel-ladder.js?"></script>
<script defer src="/js/panel-page.js?"></script>
<script defer src="/data/pokedex-mini.js?"></script>
<script defer src="/data/pokedex-mini-bw.js?"></script>

View File

@ -855,7 +855,7 @@ const PS = new class extends PSModel {
case 'news':
options.type = options.id;
break;
case 'battle-': case 'user-': case 'team-':
case 'battle-': case 'user-': case 'team-': case 'ladder-':
options.type = options.id.slice(0, hyphenIndex);
break;
case 'view-':

277
src/panel-ladder.tsx Normal file
View File

@ -0,0 +1,277 @@
/**
* Ladder Panel
*
* Panel for ladder formats and associated ladder tables.
*
* @author Adam Tran <aviettran@gmail.com>
* @license MIT
*/
class LadderRoom extends PSRoom {
readonly classType: string = 'ladder';
readonly format?: string = this.id.split('-')[1];
notice?: string;
searchValue: string = '';
lastSearch: string = '';
loading: boolean = false;
error?: string;
ladderData?: string;
setNotice = (notice: string) => {
this.notice = notice;
this.update(null);
};
setSearchValue = (searchValue: string) => {
this.searchValue = searchValue;
this.update(null);
};
setLastSearch = (lastSearch: string) => {
this.lastSearch = lastSearch;
this.update(null);
};
setLoading = (loading: boolean) => {
this.loading = loading;
this.update(null);
};
setError = (error: Error) => {
this.loading = false;
this.error = error.message;
this.update(null);
};
setLadderData = (ladderData: string | undefined) => {
this.loading = false;
this.ladderData = ladderData;
this.update(null);
};
requestLadderData = (searchValue?: string) => {
const { teams } = PS;
if (teams.usesLocalLadder) {
this.send(`/cmd laddertop ${this.format} ${toID(this.searchValue)}`);
} else if (this.format !== undefined) {
Net('/ladder.php')
.get({
query: {
format: this.format,
server: Config.server.id.split(':')[0],
output: 'html',
prefix: toID(searchValue),
},
})
.then(this.setLadderData)
.catch(this.setError);
}
this.setLoading(true);
};
}
function LadderBackToFormatList(room: PSRoom) {
return () => {
PS.removeRoom(room);
PS.join("ladder" as RoomID);
};
}
function LadderFormat(props: { room: LadderRoom }) {
const { teams } = PS;
const { room } = props;
const {
format, searchValue, lastSearch, loading, error, ladderData,
setSearchValue, setLastSearch, requestLadderData,
} = room;
if (format === undefined) return null;
const changeSearch = (e: Event) => {
setSearchValue((e.currentTarget as HTMLInputElement).value);
};
const submitSearch = (e: Event) => {
e.preventDefault();
setLastSearch(room.searchValue);
requestLadderData(room.searchValue);
};
const RenderHeader = () => {
if (!teams.usesLocalLadder) {
return <h3>
{BattleLog.escapeFormat(format)} Top{" "}
{BattleLog.escapeHTML(lastSearch ? `- '${lastSearch}'` : "500")}
</h3>;
}
return null;
};
const RenderSearch = () => {
if (!teams.usesLocalLadder) {
return <form class="search" onSubmit={submitSearch}>
<input
type="text"
name="searchValue"
class="textbox searchinput"
value={BattleLog.escapeHTML(searchValue)}
placeholder="username prefix"
onChange={changeSearch}
/>
<button type="submit"> Search</button>
</form>;
}
return null;
};
const RenderFormat = () => {
if (loading || !BattleFormats) {
return <p>Loading...</p>;
} else if (error !== undefined) {
return <p>Error: {error}</p>;
} else if (BattleFormats[format] === undefined) {
return <p>Format {format} not found.</p>;
} else if (ladderData === undefined) {
return null;
}
return (
<>
<p>
<button
class="button"
onClick={() => requestLadderData(lastSearch)}
>
<i class="fa fa-refresh"></i> Refresh
</button>
<RenderSearch/>
</p>
<RenderHeader/>
<SanitizedHTML>{ladderData}</SanitizedHTML>
</>
);
};
return (
<div class="ladder pad">
<p>
<button onClick={LadderBackToFormatList(room)}>
<i class="fa fa-chevron-left"></i> Format List
</button>
</p>
<RenderFormat />
</div>
);
}
class LadderPanel extends PSRoomPanel<LadderRoom> {
componentDidMount() {
const { room } = this.props;
// Request ladder data either on mount or after BattleFormats are loaded
if (BattleFormats && room.format !== undefined) room.requestLadderData();
this.subscriptions.push(
room.subscribe((response: any) => {
if (response) {
const [format, ladderData] = response;
if (room.format === format) {
if (!ladderData) {
room.setError(new Error('No data returned from server.'));
} else {
room.setLadderData(ladderData);
}
}
}
this.forceUpdate();
})
);
this.subscriptions.push(
PS.teams.subscribe(() => {
if (room.format !== undefined) room.requestLadderData();
this.forceUpdate();
})
);
}
static Notice = (props: { notice: string | undefined }) => {
const { notice } = props;
if (notice) {
return (
<p>
<strong style="color:red">{notice}</strong>
</p>
);
}
return null;
};
static BattleFormatList = () => {
if (!BattleFormats) {
return <p>Loading...</p>;
}
let currentSection: string = "";
let sections: JSX.Element[] = [];
let formats: JSX.Element[] = [];
for (const [key, format] of Object.entries(BattleFormats)) {
if (!format.rated || !format.searchShow) continue;
if (format.section !== currentSection) {
if (formats.length > 0) {
sections.push(
<preact.Fragment key={currentSection}>
<h3>{currentSection}</h3>
<ul style="list-style:none;margin:0;padding:0">
{formats}
</ul>
</preact.Fragment>
);
formats = [];
}
currentSection = format.section;
}
formats.push(
<li key={key} style="margin:5px">
<button
name="joinRoom"
value={`ladder-${key}`}
class="button"
style="width:320px;height:30px;text-align:left;font:12pt Verdana"
>
{BattleLog.escapeFormat(format.id)}
</button>
</li>
);
}
return <>{sections}</>;
};
static ShowFormatList = (props: { room: LadderRoom }) => {
const { room } = props;
return (
<>
<p>
See a user's ranking with{" "}
<a
class="button"
href={`/${Config.routes.users}/`}
target="_blank"
>
User lookup
</a>
</p>
<LadderPanel.Notice notice={room.notice} />
<p>
(btw if you couldn't tell the ladder screens aren't done yet;
they'll look nicer than this once I'm done.)
</p>
<p>
<button name="joinRoom" value="view-ladderhelp" class="button">
<i class="fa fa-info-circle"></i> How the ladder works
</button>
</p>
<LadderPanel.BattleFormatList />
</>
);
};
render() {
const { room } = this.props;
return (
<PSPanelWrapper room={room} scrollable>
<div class="ladder pad">
{room.format === undefined && (
<LadderPanel.ShowFormatList room={room} />
)}
{room.format !== undefined && <LadderFormat room={room} />}
</div>
</PSPanelWrapper>
);
}
}
PS.roomTypes['ladder'] = {
Model: LadderRoom,
Component: LadderPanel,
};
PS.updateRoomTypes();

View File

@ -257,6 +257,13 @@ class MainMenuRoom extends PSRoom {
battlesRoom.battles = battles;
battlesRoom.update(null);
}
break;
case 'laddertop':
const ladderRoomEntries = Object.entries(PS.rooms).filter(entry => entry[0].startsWith('ladder'));
for (const [, ladderRoom] of ladderRoomEntries) {
(ladderRoom as LadderRoom).update(response);
}
break;
}
}
}

78
src/panel-page.tsx Normal file
View File

@ -0,0 +1,78 @@
/**
* Page Panel
*
* Panel for static content and server-rendered HTML.
*
* @author Adam Tran <aviettran@gmail.com>
* @license MIT
*/
class PageRoom extends PSRoom {
readonly classType: string = 'page';
readonly page?: string = this.id.split("-")[1];
readonly canConnect = true;
}
function PageNotFound() {
// Future development: server-rendered HTML panels
return <p>Page not found</p>;
}
function PagerLadderHelp(props: { room: PageRoom }) {
const { room } = props;
return (
<div class="ladder pad">
<p>
<button name="selectFormat" onClick={LadderBackToFormatList(room)}>
<i class="fa fa-chevron-left"></i> Format List
</button>
</p>
<h3>How the ladder works</h3>
<p>Our ladder displays three ratings: Elo, GXE, and Glicko-1.</p>
<p>
<strong>Elo</strong> is the main ladder rating. It's a pretty
normal ladder rating: goes up when you win and down when you
lose.
</p>
<p>
<strong>GXE</strong> (Glicko X-Act Estimate) is an estimate of
your win chance against an average ladder player.
</p>
<p>
<strong>Glicko-1</strong> is a different rating system. It has
rating and deviation values.
</p>
<p>
Note that win/loss should not be used to estimate skill, since
who you play against is much more important than how many times
you win or lose. Our other stats like Elo and GXE are much better
for estimating skill.
</p>
</div>
);
}
class PagePanel extends PSRoomPanel<PageRoom> {
render() {
const { room } = this.props;
const RenderPage = () => {
switch (room.page) {
case 'ladderhelp':
return <PagerLadderHelp room={room}/>;
default:
return <PageNotFound/>;
}
};
return (
<PSPanelWrapper room={room} scrollable>
<RenderPage />
</PSPanelWrapper>
);
}
}
PS.roomTypes['html'] = {
Model: PageRoom,
Component: PagePanel,
};
PS.updateRoomTypes();

View File

@ -28,6 +28,7 @@ class PSHeader extends preact.Component<{style: {}}> {
icon = <i class="fa fa-pencil-square-o"></i>;
break;
case 'ladder':
case 'ladderformat':
icon = <i class="fa fa-list-ol"></i>;
break;
case 'battles':

View File

@ -478,3 +478,7 @@ class PSMain extends preact.Component {
}
type PanelPosition = {top?: number, bottom?: number, left?: number, right?: number} | null;
function SanitizedHTML(props: {children: string}) {
return <div dangerouslySetInnerHTML={{__html: BattleLog.sanitizeHTML(props.children)}}/>;
}

View File

@ -110,6 +110,8 @@
<script src="js/battle-dex-search.js?"></script>
<script src="js/battle-searchresults.js?"></script>
<script src="js/panel-teambuilder-team.js?"></script>
<script src="js/panel-ladder.js?"></script>
<script src="js/panel-page.js?"></script>
<script src="https://play.pokemonshowdown.com/data/pokedex-mini.js"></script>
<script src="https://play.pokemonshowdown.com/data/pokedex-mini-bw.js"></script>