import { css, html, LitElement } from "lit"; import { customElement, query, state } from "lit/decorators.js"; import { ClanLeaderboardResponse, ClanLeaderboardResponseSchema, } from "../core/ApiSchemas"; import { getApiBase } from "./Api"; import { translateText } from "./Utils"; @customElement("stats-modal") export class StatsModal extends LitElement { @query("o-modal") private modalEl!: HTMLElement & { open: () => void; close: () => void; }; @state() private isLoading: boolean = false; @state() private error: string | null = null; @state() private data: ClanLeaderboardResponse | null = null; private hasLoaded = false; createRenderRoot() { return this; } public open() { this.modalEl?.open(); if (!this.hasLoaded && !this.isLoading) { void this.loadLeaderboard(); } } public close() { this.modalEl?.close(); } 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_stats")}

`; } const { start, end, clans } = this.data; const startDate = new Date(start); const endDate = new Date(end); return html`

${translateText("stats_modal.clan_stats")}

${startDate.toLocaleDateString()} · ${endDate.toLocaleDateString()}

${clans.map( (clan, index) => html` `, )}
${translateText("stats_modal.rank")} ${translateText("stats_modal.clan")} ${translateText("stats_modal.games")} ${translateText("stats_modal.win_score")} ${translateText("stats_modal.loss_score")} ${translateText("stats_modal.win_loss_ratio")}
${(index + 1).toLocaleString()} ${clan.clanTag} ${clan.games.toLocaleString()} ${clan.weightedWins} ${clan.weightedLosses} ${clan.weightedWLRatio}
`; } render() { return html` ${this.renderBody()} `; } } @customElement("stats-button") export class StatsButton extends LitElement { @query("stats-modal") private statsModal: StatsModal; @state() private isVisible: boolean = true; static styles = css` :host { display: block; } `; constructor() { super(); } createRenderRoot() { return this; } render() { if (!this.isVisible) { return html``; } return html`
`; } private open() { this.isVisible = true; this.requestUpdate(); this.statsModal?.open(); } public close() { this.statsModal?.close(); this.isVisible = false; this.requestUpdate(); } }