import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { ClanLeaderboardEntry, ClanLeaderboardResponse, ClanLeaderboardResponseSchema, } from "../core/ApiSchemas"; import { getApiBase } from "./Api"; import { translateText } from "./Utils"; import { BaseModal } from "./components/BaseModal"; import { modalHeader } from "./components/ui/ModalHeader"; @customElement("stats-modal") export class StatsModal extends BaseModal { @state() private isLoading: boolean = false; @state() private error: string | null = null; @state() private data: ClanLeaderboardResponse | null = null; @state() private sortBy: "rank" | "games" | "wins" | "losses" | "ratio" = "rank"; @state() private sortOrder: "asc" | "desc" = "asc"; private hasLoaded = false; private handleSort(column: "rank" | "games" | "wins" | "losses" | "ratio") { if (this.sortBy === column) { this.sortOrder = this.sortOrder === "asc" ? "desc" : "asc"; } else { this.sortBy = column; this.sortOrder = column === "rank" ? "asc" : "desc"; } this.requestUpdate(); } private getSortedClans(clans: ClanLeaderboardEntry[]) { const sorted = [...clans]; sorted.sort((a, b) => { let aVal: number, bVal: number; switch (this.sortBy) { case "games": aVal = a.games; bVal = b.games; break; case "wins": aVal = a.weightedWins; bVal = b.weightedWins; break; case "losses": aVal = a.weightedLosses; bVal = b.weightedLosses; break; case "ratio": aVal = a.weightedWLRatio; bVal = b.weightedWLRatio; break; case "rank": default: // Original order return 0; } return this.sortOrder === "asc" ? aVal - bVal : bVal - aVal; }); return sorted; } protected onOpen(): void { if (!this.hasLoaded && !this.isLoading) { void this.loadLeaderboard(); } } private async loadLeaderboard() { this.isLoading = true; this.error = null; try { const res = await fetch(`${getApiBase()}/public/clans/leaderboard`, { headers: { Accept: "application/json", }, }); if (!res.ok) { throw new Error(`Unexpected status ${res.status}`); } const json = await res.json(); const parsed = ClanLeaderboardResponseSchema.safeParse(json); if (!parsed.success) { console.warn( "ClanLeaderboardModal: invalid response schema", parsed.error, ); throw new Error("Invalid response format"); } this.data = parsed.data; this.hasLoaded = true; } catch (err) { console.warn("ClanLeaderboardModal: failed to load leaderboard", err); this.error = translateText("stats_modal.error"); } finally { this.isLoading = false; this.requestUpdate(); } } private renderBody() { if (this.isLoading) { return html`
${translateText("stats_modal.loading")}
${this.error}
${translateText("stats_modal.no_stats")}
| ${translateText("stats_modal.rank")} | ${translateText("stats_modal.clan")} | this.handleSort("games")}
class="py-4 px-4 text-right font-bold w-32 cursor-pointer hover:text-white/60 transition-colors select-none"
>
${translateText("stats_modal.games")}
${this.sortBy === "games"
? this.sortOrder === "asc"
? html`↑`
: html`↓`
: html`↕`}
|
this.handleSort("wins")}
class="py-4 px-4 text-right font-bold hidden md:table-cell cursor-pointer hover:text-white/60 transition-colors select-none"
title=${translateText("stats_modal.win_score_tooltip")}
>
${translateText("stats_modal.win_score")}
${this.sortBy === "wins"
? this.sortOrder === "asc"
? html`↑`
: html`↓`
: html`↕`}
|
this.handleSort("losses")}
class="py-4 px-4 text-right font-bold hidden md:table-cell cursor-pointer hover:text-white/60 transition-colors select-none"
title=${translateText("stats_modal.loss_score_tooltip")}
>
${translateText("stats_modal.loss_score")}
${this.sortBy === "losses"
? this.sortOrder === "asc"
? html`↑`
: html`↓`
: html`↕`}
|
this.handleSort("ratio")}
class="py-4 px-4 text-right font-bold pr-6 cursor-pointer hover:text-white/60 transition-colors select-none"
>
${translateText("stats_modal.win_loss_ratio")}
${this.sortBy === "ratio"
? this.sortOrder === "asc"
? html`↑`
: html`↓`
: html`↕`}
|
|---|---|---|---|---|---|
|
${rankIcon}
|
${clan.clanTag}
|
${clan.games.toLocaleString()}
|
${clan.weightedWLRatio}
${translateText("stats_modal.ratio")}
|