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")}

`; } if (this.error) { return html`

${this.error}

`; } if (!this.data || this.data.clans.length === 0) { return html`

${translateText("stats_modal.no_data_yet")}

${translateText("stats_modal.no_stats")}

`; } const { clans } = this.data; const maxGames = Math.max(...clans.map((c) => c.games), 1); return html`
${this.getSortedClans(clans).map((clan, index) => { const rankColor = index === 0 ? "text-yellow-400 bg-yellow-400/10 ring-1 ring-yellow-400/20" : index === 1 ? "text-slate-300 bg-slate-400/10 ring-1 ring-slate-400/20" : index === 2 ? "text-amber-600 bg-amber-600/10 ring-1 ring-amber-600/20" : "text-white/40 bg-white/5"; const rankIcon = index === 0 ? "👑" : index === 1 ? "🥈" : index === 2 ? "🥉" : String(index + 1); return html` `; })}
${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")}
`; } render() { let dateRange = html``; if (this.data) { const start = new Date(this.data.start).toLocaleDateString(); const end = new Date(this.data.end).toLocaleDateString(); dateRange = html`(${start} - ${end})`; } const content = html`
${modalHeader({ titleContent: html`
${translateText("stats_modal.clan_stats")} ${dateRange}
`, onBack: this.close, ariaLabel: translateText("common.close"), leftClassName: "flex flex-wrap items-center gap-4 flex-1", })}
${this.renderBody()}
`; if (this.inline) { return content; } return html` ${content} `; } }