From a9f8adfa392ac26b2ecabab9dc750b58ff19e6bf Mon Sep 17 00:00:00 2001 From: Adam Tran Date: Fri, 29 Jan 2021 11:10:36 -0500 Subject: [PATCH] Preact Client: Ladder (#1709) --- .eslintignore | 2 + .gitignore | 2 + preactalpha.template.html | 2 + src/client-main.ts | 2 +- src/panel-ladder.tsx | 277 ++++++++++++++++++++++++++++++++++++++ src/panel-mainmenu.tsx | 7 + src/panel-page.tsx | 78 +++++++++++ src/panel-topbar.tsx | 1 + src/panels.tsx | 4 + testclient-beta.html | 2 + 10 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 src/panel-ladder.tsx create mode 100644 src/panel-page.tsx diff --git a/.eslintignore b/.eslintignore index ae1754409..2c5bf276c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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 diff --git a/.gitignore b/.gitignore index 961f55036..f8be3a1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/preactalpha.template.html b/preactalpha.template.html index b279cde22..2f128fee4 100644 --- a/preactalpha.template.html +++ b/preactalpha.template.html @@ -92,6 +92,8 @@ + + diff --git a/src/client-main.ts b/src/client-main.ts index 4d3a74564..1dea48551 100644 --- a/src/client-main.ts +++ b/src/client-main.ts @@ -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-': diff --git a/src/panel-ladder.tsx b/src/panel-ladder.tsx new file mode 100644 index 000000000..476283976 --- /dev/null +++ b/src/panel-ladder.tsx @@ -0,0 +1,277 @@ +/** + * Ladder Panel + * + * Panel for ladder formats and associated ladder tables. + * + * @author Adam Tran + * @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

+ {BattleLog.escapeFormat(format)} Top{" "} + {BattleLog.escapeHTML(lastSearch ? `- '${lastSearch}'` : "500")} +

; + } + return null; + }; + const RenderSearch = () => { + if (!teams.usesLocalLadder) { + return ; + } + return null; + }; + const RenderFormat = () => { + if (loading || !BattleFormats) { + return

Loading...

; + } else if (error !== undefined) { + return

Error: {error}

; + } else if (BattleFormats[format] === undefined) { + return

Format {format} not found.

; + } else if (ladderData === undefined) { + return null; + } + return ( + <> +

+ + +

+ + {ladderData} + + ); + }; + return ( +
+

+ +

+ +
+ ); +} + +class LadderPanel extends PSRoomPanel { + 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 ( +

+ {notice} +

+ ); + } + return null; + }; + static BattleFormatList = () => { + if (!BattleFormats) { + return

Loading...

; + } + 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( + +

{currentSection}

+
    + {formats} +
+
+ ); + formats = []; + } + currentSection = format.section; + } + formats.push( +
  • + +
  • + ); + } + return <>{sections}; + }; + static ShowFormatList = (props: { room: LadderRoom }) => { + const { room } = props; + return ( + <> +

    + See a user's ranking with{" "} + + User lookup + +

    + +

    + (btw if you couldn't tell the ladder screens aren't done yet; + they'll look nicer than this once I'm done.) +

    +

    + +

    + + + ); + }; + render() { + const { room } = this.props; + return ( + +
    + {room.format === undefined && ( + + )} + {room.format !== undefined && } +
    +
    + ); + } +} + +PS.roomTypes['ladder'] = { + Model: LadderRoom, + Component: LadderPanel, +}; +PS.updateRoomTypes(); diff --git a/src/panel-mainmenu.tsx b/src/panel-mainmenu.tsx index 237950ba5..593bc5fe5 100644 --- a/src/panel-mainmenu.tsx +++ b/src/panel-mainmenu.tsx @@ -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; } } } diff --git a/src/panel-page.tsx b/src/panel-page.tsx new file mode 100644 index 000000000..b00bb560b --- /dev/null +++ b/src/panel-page.tsx @@ -0,0 +1,78 @@ +/** + * Page Panel + * + * Panel for static content and server-rendered HTML. + * + * @author Adam Tran + * @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

    Page not found

    ; +} + +function PagerLadderHelp(props: { room: PageRoom }) { + const { room } = props; + return ( +
    +

    + +

    +

    How the ladder works

    +

    Our ladder displays three ratings: Elo, GXE, and Glicko-1.

    +

    + Elo is the main ladder rating. It's a pretty + normal ladder rating: goes up when you win and down when you + lose. +

    +

    + GXE (Glicko X-Act Estimate) is an estimate of + your win chance against an average ladder player. +

    +

    + Glicko-1 is a different rating system. It has + rating and deviation values. +

    +

    + 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. +

    +
    + ); +} + +class PagePanel extends PSRoomPanel { + render() { + const { room } = this.props; + const RenderPage = () => { + switch (room.page) { + case 'ladderhelp': + return ; + default: + return ; + } + }; + return ( + + + + ); + } +} + +PS.roomTypes['html'] = { + Model: PageRoom, + Component: PagePanel, +}; +PS.updateRoomTypes(); diff --git a/src/panel-topbar.tsx b/src/panel-topbar.tsx index 62fe6f6ed..dfb72f274 100644 --- a/src/panel-topbar.tsx +++ b/src/panel-topbar.tsx @@ -28,6 +28,7 @@ class PSHeader extends preact.Component<{style: {}}> { icon = ; break; case 'ladder': + case 'ladderformat': icon = ; break; case 'battles': diff --git a/src/panels.tsx b/src/panels.tsx index 45badba6c..45c2429f4 100644 --- a/src/panels.tsx +++ b/src/panels.tsx @@ -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
    ; +} diff --git a/testclient-beta.html b/testclient-beta.html index 4ca71217f..ad02dce6a 100644 --- a/testclient-beta.html +++ b/testclient-beta.html @@ -110,6 +110,8 @@ + +