diff --git a/index.html b/index.html index 29f534e437..2be53bc92d 100644 --- a/index.html +++ b/index.html @@ -122,12 +122,24 @@ -
+
+
+ +
+
@@ -140,10 +152,9 @@ @@ -163,7 +174,14 @@ - + + @@ -239,10 +262,10 @@
diff --git a/package-lock.json b/package-lock.json index 8169f6eece..e783c26e71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1189,7 +1189,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1233,7 +1232,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2167,7 +2165,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -4562,7 +4559,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz", "integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4728,7 +4724,6 @@ "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.34.1", "@typescript-eslint/types": "8.34.1", @@ -5208,7 +5203,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5608,7 +5602,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -5762,7 +5755,6 @@ "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -6684,7 +6676,6 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -7254,7 +7245,6 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8589,7 +8579,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -10185,7 +10174,6 @@ "integrity": "sha512-dyuThzncsgEgJZnvd/A/5x6IkUERbK+phXqUQrI+0C6WE+8xqGH5VChRTLecemhgZF0kQ+gZOM3tJTX9937xpg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@pixi/colord": "^2.9.6", "@types/css-font-loading-module": "^0.0.12", @@ -10230,7 +10218,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10333,7 +10320,6 @@ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11156,7 +11142,6 @@ "integrity": "sha512-Z0NVCW45W8Mg5oC/27/+fCqIHFnW8kpkFOq0j9XJIev4Ld0mKmERaZv5DMLAb9fGCevjKwaEeIQz5+MBXfZcDw==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.1", "@sinonjs/fake-timers": "^15.1.0", @@ -11578,7 +11563,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11821,7 +11805,6 @@ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -11890,7 +11873,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12037,7 +12019,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -12787,7 +12768,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12801,7 +12781,6 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", diff --git a/resources/images/OF.png b/resources/images/OF.png new file mode 100644 index 0000000000..8b573105bb Binary files /dev/null and b/resources/images/OF.png differ diff --git a/resources/images/OpenFront.png b/resources/images/OpenFront.png new file mode 100644 index 0000000000..2ddd74dedd Binary files /dev/null and b/resources/images/OpenFront.png differ diff --git a/resources/images/background.png b/resources/images/background.png new file mode 100644 index 0000000000..773ed4b81f Binary files /dev/null and b/resources/images/background.png differ diff --git a/resources/lang/en.json b/resources/lang/en.json index 23b15a4899..fd0e69a00f 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -394,7 +394,8 @@ "connecting": "Connecting to matchmaking server...", "searching": "Searching for game...", "waiting_for_game": "Waiting for game to start...", - "elo": "Your ELO: {elo}" + "elo": "Your ELO: {elo}", + "no_elo": "No ELO yet" }, "username": { "enter_username": "Enter your username", @@ -475,6 +476,13 @@ "teams": "Teams", "humans_vs_nations": "Humans vs Nations" }, + "mode_selector": { + "special_title": "Special Mix", + "ranked_title": "Ranked", + "ranked_1v1_title": "1v1", + "ranked_2v2_title": "2v2", + "coming_soon": "Coming Soon" + }, "public_game_modifier": { "random_spawn": "Random Spawn", "compact_map": "Compact Map", @@ -1001,8 +1009,6 @@ "recent_games": "Recent Games", "game_id": "Game ID", "mode": "Mode", - "mode_ffa": "Free-for-All", - "mode_team": "Team", "replay": "Replay", "details": "Details", "ranking": "Ranking", @@ -1020,8 +1026,6 @@ "stats_losses": "Losses", "stats_wlr": "Win:Loss Ratio", "stats_games_played": "Games Played", - "mode_ffa": "Free-for-All", - "mode_team": "Team", "no_stats": "No stats recorded for this selection." }, "matchmaking_button": { diff --git a/src/client/AccountModal.ts b/src/client/AccountModal.ts index 8169e95669..6879218dca 100644 --- a/src/client/AccountModal.ts +++ b/src/client/AccountModal.ts @@ -61,18 +61,9 @@ export class AccountModal extends BaseModal { render() { const content = this.isLoadingUser - ? html` -
-
-

- ${translateText("account_modal.fetching_account")} -

-
- ` + ? this.renderLoadingSpinner( + translateText("account_modal.fetching_account"), + ) : this.renderInner(); if (this.inline) { @@ -99,9 +90,7 @@ export class AccountModal extends BaseModal { const displayId = publicId || translateText("account_modal.not_found"); return html` -
+
${modalHeader({ title, onBack: () => this.close(), diff --git a/src/client/FlagInput.ts b/src/client/FlagInput.ts index 9bf17fce66..88f9be0602 100644 --- a/src/client/FlagInput.ts +++ b/src/client/FlagInput.ts @@ -84,7 +84,7 @@ export class FlagInput extends LitElement { return html` - `; - } - - public stop() { - this.lobbySocket.stop(); - } - - private getTeamSize( - teamCount: number | string | null, - maxPlayers: number, - ): number | undefined { - if (typeof teamCount === "string") { - if (teamCount === Duos) return 2; - if (teamCount === Trios) return 3; - if (teamCount === Quads) return 4; - if (teamCount === HumansVsNations) return maxPlayers; - return undefined; - } - if (typeof teamCount === "number" && teamCount > 0) { - return Math.floor(maxPlayers / teamCount); - } - return undefined; - } - - private getTeamTotal( - teamCount: number | string | null, - teamSize: number | undefined, - maxPlayers: number, - ): number | undefined { - if (typeof teamCount === "number") return teamCount; - if (teamCount === HumansVsNations) return 2; - if (teamSize && teamSize > 0) return Math.floor(maxPlayers / teamSize); - return undefined; - } - - private getModeLabel( - gameMode: GameMode, - teamCount: number | string | null, - teamTotal: number | undefined, - teamSize: number | undefined, - ): string { - if (gameMode !== GameMode.Team) return translateText("game_mode.ffa"); - if (teamCount === HumansVsNations && teamSize !== undefined) - return translateText("public_lobby.teams_hvn_detailed", { - num: teamSize, - }); - const totalTeams = - teamTotal ?? (typeof teamCount === "number" ? teamCount : 0); - return translateText("public_lobby.teams", { num: totalTeams }); - } - - private getTeamDetailLabel( - gameMode: GameMode, - teamCount: number | string | null, - teamTotal: number | undefined, - teamSize: number | undefined, - ): { label: string | null; isFullLabel: boolean } { - if (gameMode !== GameMode.Team) { - return { label: null, isFullLabel: false }; - } - - if (typeof teamCount === "string" && teamCount === HumansVsNations) { - return { label: null, isFullLabel: false }; - } - - if (typeof teamCount === "string") { - const teamKey = `public_lobby.teams_${teamCount}`; - // translateText returns the key when a translation is missing. - const maybeTranslated = translateText(teamKey, { - team_count: teamTotal ?? 0, - }); - if (maybeTranslated !== teamKey) { - return { label: maybeTranslated, isFullLabel: true }; - } - } - - if (teamTotal !== undefined && teamSize !== undefined) { - // Fallback when there's no specific team label translation. - return { - label: translateText("public_lobby.players_per_team", { - num: teamSize, - }), - isFullLabel: false, - }; - } - - return { label: null, isFullLabel: false }; - } - - private getModifierLabels( - publicGameModifiers: PublicGameModifiers | undefined, - ): string[] { - if (!publicGameModifiers) { - return []; - } - const labels: string[] = []; - if (publicGameModifiers.isRandomSpawn) { - labels.push(translateText("public_game_modifier.random_spawn")); - } - if (publicGameModifiers.isCompact) { - labels.push(translateText("public_game_modifier.compact_map")); - } - if (publicGameModifiers.isCrowded) { - labels.push(translateText("public_game_modifier.crowded")); - } - if (publicGameModifiers.startingGold) { - labels.push(translateText("public_game_modifier.starting_gold")); - } - return labels; - } - - private lobbyClicked(lobby: GameInfo) { - const usernameInput = document.querySelector("username-input") as any; - if ( - usernameInput && - typeof usernameInput.isValid === "function" && - !usernameInput.isValid() - ) { - window.dispatchEvent( - new CustomEvent("show-message", { - detail: { - message: usernameInput.validationError, - color: "red", - duration: 3000, - }, - }), - ); - return; - } - - this.dispatchEvent( - new CustomEvent("join-lobby", { - detail: { - gameID: lobby.gameID, - clientID: getClientIDForGame(lobby.gameID), - source: "public", - publicLobbyInfo: lobby, - } as JoinLobbyEvent, - bubbles: true, - composed: true, - }), - ); - } -} diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index db44efb6ce..1c84839319 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -140,9 +140,7 @@ export class SinglePlayerModal extends BaseModal { render() { const content = html` -
+
${modalHeader({ title: translateText("main.solo") || "Solo", @@ -618,7 +616,7 @@ export class SinglePlayerModal extends BaseModal {
-
+
${this.validationError @@ -148,18 +148,21 @@ export class UsernameInput extends LitElement { private validateAndStore() { // Prevent empty username even if clan tag is present - if (!this.baseUsername.trim()) { + const trimmedBase = this.baseUsername.trim(); + if (!trimmedBase || trimmedBase.length < MIN_USERNAME_LENGTH) { this._isValid = false; - this.validationError = translateText("username.too_short", { + const msg = translateText("username.too_short", { min: MIN_USERNAME_LENGTH, }); + this.validationError = msg; return; } // Validate clan tag if present if (this.clanTag.length > 0 && this.clanTag.length < 2) { this._isValid = false; - this.validationError = translateText("username.tag_too_short"); + const msg = translateText("username.tag_too_short"); + this.validationError = msg; return; } diff --git a/src/client/components/BaseModal.ts b/src/client/components/BaseModal.ts index 0f7d7b2e4e..80e40d9001 100644 --- a/src/client/components/BaseModal.ts +++ b/src/client/components/BaseModal.ts @@ -1,4 +1,4 @@ -import { LitElement } from "lit"; +import { html, LitElement, TemplateResult } from "lit"; import { property, query, state } from "lit/decorators.js"; /** @@ -10,11 +10,21 @@ import { property, query, state } from "lit/decorators.js"; * - Automatic listener lifecycle management * - Common inline/modal element handling * - Shared open/close logic with hooks for custom behavior + * - Standardized loading spinner UI + * - Consistent modal container styling */ export abstract class BaseModal extends LitElement { @state() protected isModalOpen = false; @property({ type: Boolean }) inline = false; + /** + * Standard modal container class string. + * Provides consistent dark glassmorphic styling across all modals. + * No rounding on mobile for full-screen appearance. + */ + protected readonly modalContainerClass = + "h-full flex flex-col overflow-hidden bg-black/70 backdrop-blur-xl lg:rounded-2xl lg:border border-white/10"; + @query("o-modal") protected modalEl?: HTMLElement & { open: () => void; close: () => void; @@ -121,4 +131,43 @@ export abstract class BaseModal extends LitElement { this.modalEl?.close(); } } + + /** + * Renders a standardized loading spinner with optional custom message. + * Use this for consistent loading states across all modals. + * + * @param message - Optional loading message text. Defaults to no message. + * @param spinnerColor - Optional spinner color. Defaults to 'blue'. + * @returns TemplateResult of the loading UI + */ + protected renderLoadingSpinner( + message?: string, + spinnerColor: "blue" | "green" | "yellow" | "white" = "blue", + ): TemplateResult { + const colorClasses = { + blue: "border-blue-500/30 border-t-blue-500", + green: "border-green-500/30 border-t-green-500", + yellow: "border-yellow-500/30 border-t-yellow-500", + white: "border-white/20 border-t-white", + }; + + return html` +
+
+ ${message + ? html`

+ ${message} +

` + : ""} +
+ `; + } } diff --git a/src/client/components/DesktopNavBar.ts b/src/client/components/DesktopNavBar.ts index 52d033054a..6fe896a93a 100644 --- a/src/client/components/DesktopNavBar.ts +++ b/src/client/components/DesktopNavBar.ts @@ -79,9 +79,14 @@ export class DesktopNavBar extends LitElement { }; render() { + const currentPage = (window as any).currentPageId ?? "page-play"; + if (!(window as any).currentPageId) { + (window as any).currentPageId = currentPage; + } + return html`