mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-03-21 17:50:29 -05:00
Preact Client: Ladder (#1709)
This commit is contained in:
parent
3e4b83298c
commit
a9f8adfa39
|
|
@ -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
2
.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
277
src/panel-ladder.tsx
Normal 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();
|
||||
|
|
@ -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
78
src/panel-page.tsx
Normal 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();
|
||||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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)}}/>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user