mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 17:36:38 +00:00
f256f497ce
## Description: Adds the following: - Snowflake animation with snowflakes falling from the top to the bottom of the screen - Changed homepage color theme from blue and white to Green and Red. Also changed the openfront logo to a red, green, and white gradient. - Added a santa hat on the announcements button ## Please complete the following: - [x] I have added screenshots for all UI updates - [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file - [ ] I have added relevant tests to the test directory - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced <img width="1616" height="836" alt="Screenshot 2025-12-12 at 3 01 17 PM" src="https://github.com/user-attachments/assets/82e29db3-3bc0-4392-b5bf-dd57c15784a3" /> <img width="1616" height="836" alt="Screenshot 2025-12-12 at 2 58 54 PM" src="https://github.com/user-attachments/assets/232da646-6923-4966-acba-5240074e7e3f" /> ## Please put your Discord username so you can be contacted if a bug or regression is found: Restart --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
244 lines
6.8 KiB
TypeScript
244 lines
6.8 KiB
TypeScript
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`
|
|
<div class="flex flex-col items-center justify-center p-6 text-white">
|
|
<p class="mb-2 text-lg font-semibold">
|
|
${translateText("stats_modal.loading")}
|
|
</p>
|
|
<div
|
|
class="w-6 h-6 border-4 border-red-500 border-t-transparent rounded-full animate-spin"
|
|
></div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (this.error) {
|
|
return html`
|
|
<div class="flex flex-col items-center justify-center p-6 text-white">
|
|
<p class="mb-4 text-center">${this.error}</p>
|
|
<button
|
|
class="px-4 py-2 bg-red-600 hover:bg-red-700 rounded text-sm font-medium"
|
|
@click=${() => this.loadLeaderboard()}
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (!this.data || this.data.clans.length === 0) {
|
|
return html`
|
|
<div class="p-6 text-center text-gray-200">
|
|
<p class="text-lg font-semibold mb-2">
|
|
${translateText("stats_modal.no_stats")}
|
|
</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
const { start, end, clans } = this.data;
|
|
const startDate = new Date(start);
|
|
const endDate = new Date(end);
|
|
|
|
return html`
|
|
<div class="p-4 md:p-6 text-gray-200">
|
|
<div
|
|
class="flex flex-col md:flex-row md:items-center md:justify-between mb-4 gap-2"
|
|
>
|
|
<div>
|
|
<h2 class="text-xl font-semibold">
|
|
${translateText("stats_modal.clan_stats")}
|
|
</h2>
|
|
<p class="text-xs text-gray-400 mt-1">
|
|
${startDate.toLocaleDateString()} ·
|
|
${endDate.toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full text-xs md:text-sm">
|
|
<thead>
|
|
<tr class="border-b border-gray-700 text-gray-300">
|
|
<th class="py-2 pr-3 text-left">
|
|
${translateText("stats_modal.rank")}
|
|
</th>
|
|
<th class="py-2 pr-3 text-left">
|
|
${translateText("stats_modal.clan")}
|
|
</th>
|
|
<th class="py-2 px-2 text-right">
|
|
${translateText("stats_modal.games")}
|
|
</th>
|
|
<th class="py-2 px-2 text-right">
|
|
${translateText("stats_modal.win_score")}
|
|
</th>
|
|
<th class="py-2 px-2 text-right">
|
|
${translateText("stats_modal.loss_score")}
|
|
</th>
|
|
<th class="py-2 pl-2 text-right">
|
|
${translateText("stats_modal.win_loss_ratio")}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${clans.map(
|
|
(clan, index) => html`
|
|
<tr class="border-b border-gray-800 last:border-b-0">
|
|
<td class="py-2 pr-3 text-center">
|
|
${(index + 1).toLocaleString()}
|
|
</td>
|
|
<td class="py-2 pr-3 font-semibold text-left">
|
|
${clan.clanTag}
|
|
</td>
|
|
<td class="py-2 px-2 text-right">
|
|
${clan.games.toLocaleString()}
|
|
</td>
|
|
<td class="py-2 px-2 text-right">${clan.weightedWins}</td>
|
|
<td class="py-2 px-2 text-right">${clan.weightedLosses}</td>
|
|
<td class="py-2 pl-2 text-right">
|
|
${clan.weightedWLRatio}
|
|
</td>
|
|
</tr>
|
|
`,
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
render() {
|
|
return html`
|
|
<o-modal id="stats-modal" title="${translateText("stats_modal.title")}">
|
|
${this.renderBody()}
|
|
</o-modal>
|
|
`;
|
|
}
|
|
}
|
|
|
|
@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`
|
|
<div class="fixed top-20 right-4 z-[9998]">
|
|
<button
|
|
@click="${this.open}"
|
|
class="w-12 h-12 bg-red-600 hover:bg-red-700 text-white rounded-full shadow-2xl hover:shadow-2xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-none focus:ring-4 focus:ring-red-500 focus:ring-offset-4"
|
|
title="${translateText("stats_modal.title")}"
|
|
>
|
|
<img src="/icons/stats.svg" alt="Stats" class="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
<stats-modal></stats-modal>
|
|
`;
|
|
}
|
|
|
|
private open() {
|
|
this.isVisible = true;
|
|
this.requestUpdate();
|
|
this.statsModal?.open();
|
|
}
|
|
|
|
public close() {
|
|
this.statsModal?.close();
|
|
this.isVisible = false;
|
|
this.requestUpdate();
|
|
}
|
|
}
|