diff --git a/index.html b/index.html index e9a96fd501..d6d4b21756 100644 --- a/index.html +++ b/index.html @@ -122,12 +122,29 @@ -
+
+
+ +
+
@@ -140,10 +157,9 @@ @@ -163,7 +179,11 @@ - + diff --git a/package-lock.json b/package-lock.json index b4c33baf0e..45bbe6db1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1232,7 +1232,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1276,7 +1275,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2210,7 +2208,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" } @@ -4604,7 +4601,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" } @@ -4770,7 +4766,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", @@ -5250,7 +5245,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" }, @@ -5650,7 +5644,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -5804,7 +5797,6 @@ "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -6726,7 +6718,6 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -7296,7 +7287,6 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8627,7 +8617,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -10223,7 +10212,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", @@ -10268,7 +10256,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10371,7 +10358,6 @@ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11194,7 +11180,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", @@ -11616,7 +11601,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11859,7 +11843,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" @@ -11928,7 +11911,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" @@ -12075,7 +12057,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -12825,7 +12806,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12839,7 +12819,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..7e2e90e73e Binary files /dev/null and b/resources/images/background.png differ diff --git a/resources/lang/en.json b/resources/lang/en.json index 0650ae6a31..bf84fd13bb 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -354,7 +354,6 @@ }, "public_lobby": { "title": "Waiting for Game Start...", - "join": "Join next Game", "teams_Duos": "{team_count} teams of 2 (Duos)", "teams_Trios": "{team_count} teams of 3 (Trios)", "teams_Quads": "{team_count} teams of 4 (Quads)", @@ -375,7 +374,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", @@ -389,7 +389,6 @@ }, "host_modal": { "title": "Create Private Lobby", - "label": "Private", "mode": "Mode", "team_count": "Number of Teams", "team_type": "Team Type", @@ -452,6 +451,16 @@ "ffa": "Free for All", "teams": "Teams" }, + "mode_selector": { + "special_title": "Special Mix", + "teams_title": "Teams", + "teams_count": "{teamCount} teams", + "teams_of": "{teamCount} teams of {playersPerTeam}", + "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", @@ -920,8 +929,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", @@ -938,8 +945,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..b265ff5f3d 100644 --- a/src/client/FlagInput.ts +++ b/src/client/FlagInput.ts @@ -84,7 +84,7 @@ export class FlagInput extends LitElement { return html` - ` : html` - `; - } - - public stop() { - this.lobbySocket.stop(); - } - - private lobbyClicked(lobby: PublicGameInfo) { - // Validate username before opening the modal - 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("show-public-lobby-modal", { - detail: { lobby } as ShowPublicLobbyModalEvent, - bubbles: true, - composed: true, - }), - ); - } -} diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 6a72726bc5..2777a118b9 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -221,13 +221,11 @@ export class SinglePlayerModal extends BaseModal { ]; const content = html` -
+
${modalHeader({ title: translateText("main.solo") || "Solo", - onBack: this.close, + onBack: () => this.close(), ariaLabel: translateText("common.back"), rightContent: hasLinkedAccount(this.userMeResponse) ? html`
${this.validationError @@ -147,19 +147,22 @@ export class UsernameInput extends LitElement { } private validateAndStore() { - // Validate base username meets minimum length (clan tag doesn't count) - if (this.baseUsername.trim().length < MIN_USERNAME_LENGTH) { + // Prevent empty username even if clan tag is present + 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`