From 5e6c90d9bb3bdae6e0bed3bf1522ad313743d2de Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Sat, 10 Jan 2026 04:26:34 +0000 Subject: [PATCH] Main Menu UI Overhaul (#2829) ## Description: Overhauls the Main Menu UI, visit https://menu.openfront.dev to see everything. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n --- index.html | 1018 ++++++---- resources/icons/wiki-logo.svg | 3 + resources/lang/en.json | 102 +- resources/version.txt | 3 +- src/client/AccountModal.ts | 612 +++--- src/client/ClientGameRunner.ts | 21 +- src/client/DarkModeButton.ts | 45 - src/client/FlagInput.ts | 42 +- src/client/FlagInputModal.ts | 175 +- src/client/GameStartingModal.ts | 151 +- src/client/GoogleAdElement.ts | 25 +- src/client/HelpModal.ts | 1662 +++++++++++------ src/client/HostLobbyModal.ts | 1308 +++++++------ src/client/InputHandler.ts | 18 +- src/client/JoinPrivateLobbyModal.ts | 460 ++++- src/client/KeybindsModal.ts | 522 ++++++ src/client/LangSelector.ts | 56 +- src/client/LanguageModal.ts | 178 +- src/client/Layout.ts | 81 + src/client/Main.ts | 269 ++- src/client/Matchmaking.ts | 163 +- src/client/Navigation.ts | 77 + src/client/NewsModal.ts | 190 +- src/client/PublicLobby.ts | 181 +- src/client/SinglePlayerModal.ts | 1006 ++++++---- src/client/StatsModal.ts | 438 +++-- src/client/TerritoryPatternsModal.ts | 323 +++- src/client/UserSettingModal.ts | 444 ++--- src/client/UsernameInput.ts | 129 +- src/client/Utils.ts | 55 + src/client/components/BaseModal.ts | 114 ++ src/client/components/Difficulties.ts | 90 +- src/client/components/FluentSlider.ts | 79 +- src/client/components/LobbyTeamView.ts | 62 +- src/client/components/Maps.ts | 156 +- src/client/components/ModalOverlay.ts | 18 +- src/client/components/PatternButton.ts | 111 +- .../components/baseComponents/Button.ts | 18 +- src/client/components/baseComponents/Modal.ts | 145 +- .../baseComponents/setting/SettingKeybind.ts | 140 +- .../baseComponents/setting/SettingNumber.ts | 20 +- .../baseComponents/setting/SettingSlider.ts | 57 +- .../baseComponents/setting/SettingToggle.ts | 45 +- .../baseComponents/stats/DiscordUserHeader.ts | 35 +- .../baseComponents/stats/GameList.ts | 199 +- .../baseComponents/stats/PlayerStatsGrid.ts | 46 +- .../baseComponents/stats/PlayerStatsTable.ts | 425 +++-- .../baseComponents/stats/PlayerStatsTree.ts | 173 +- src/client/graphics/layers/HeadsUpMessage.ts | 99 +- src/client/graphics/layers/SettingsModal.ts | 4 +- src/client/graphics/layers/UnitDisplay.ts | 8 +- src/client/styles.css | 120 +- src/client/styles/components/button.css | 6 +- src/client/styles/components/modal.css | 54 +- src/client/styles/components/setting.css | 118 +- src/client/utilities/RenderUnitTypeOptions.ts | 37 +- src/client/vite-env.d.ts | 20 + src/core/Util.ts | 11 +- src/core/validations/username.ts | 5 +- tests/client/components/FluentSlider.test.ts | 45 +- 60 files changed, 7671 insertions(+), 4546 deletions(-) create mode 100644 resources/icons/wiki-logo.svg delete mode 100644 src/client/DarkModeButton.ts create mode 100644 src/client/KeybindsModal.ts create mode 100644 src/client/Layout.ts create mode 100644 src/client/Navigation.ts create mode 100644 src/client/components/BaseModal.ts diff --git a/index.html b/index.html index b68f66957..36e62b80a 100644 --- a/index.html +++ b/index.html @@ -1,15 +1,41 @@ - + - OpenFront (ALPHA) + OpenFront (ALPHA) + + + - - - - - - + + diff --git a/resources/icons/wiki-logo.svg b/resources/icons/wiki-logo.svg new file mode 100644 index 000000000..dfd1c17bd --- /dev/null +++ b/resources/icons/wiki-logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/lang/en.json b/resources/lang/en.json index 7af658d63..ec6b8ae5a 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -7,6 +7,7 @@ }, "common": { "close": "Close", + "back": "Back", "available": "Available", "preset_max": "Max", "summary_send": "Send", @@ -17,7 +18,9 @@ "cap_tooltip": "Recipient’s remaining capacity", "target_dead": "Target eliminated", "target_dead_note": "You can't send resources to an eliminated player.", - "none": "None" + "none": "None", + "copied": "Copied!", + "click_to_copy": "Click to copy" }, "main": { "title": "OpenFront (ALPHA)", @@ -26,18 +29,28 @@ "checking_login": "Checking login...", "logged_in": "Logged in!", "log_out": "Log out", - "create_lobby": "Create Lobby", - "join_lobby": "Join Lobby", - "single_player": "Single Player", + "create": "Create Lobby", + "join": "Join Lobby", + "solo": "Solo Lobby", "instructions": "Instructions", "game_info": "Game info", "wiki": "Wiki", "privacy_policy": "Privacy Policy", "terms_of_service": "Terms of Service", - "reddit": "Reddit" + "copyright": "© OpenFront™ and Contributors", + "reddit": "Reddit", + "play": "Play", + "news": "News", + "store": "Store", + "options": "Options", + "keys": "Keys", + "stats": "Stats", + "account": "Account", + "help": "Help", + "menu": "Menu", + "pick_pattern": "Pick a pattern!" }, "news": { - "see_all_releases": "See all releases", "github_link": "on GitHub", "title": "Release Notes" }, @@ -136,10 +149,11 @@ "bomb_direction": "Atom / Hydrogen bomb arc direction" }, "single_modal": { - "title": "Single Player", + "title": "Solo", "random_spawn": "Random spawn", "allow_alliances": "Allow alliances", "toggle_achievements": "Toggle achievements", + "sign_in_for_achievements": "Sign in for achievements", "options_title": "Options", "bots": "Bots: ", "bots_disabled": "Disabled", @@ -150,6 +164,8 @@ "infinite_troops": "Infinite troops", "compact_map": "Compact Map", "max_timer": "Game length (minutes)", + "max_timer_placeholder": "Mins", + "max_timer_invalid": "Please enter a valid max timer value (1-120 minutes)", "disable_nukes": "Disable Nukes", "enables_title": "Enable Settings", "start": "Start Game" @@ -161,11 +177,22 @@ }, "account_modal": { "title": "Account", + "connected_as": "Connected as", + "stats_overview": "Stats Overview", + "save_progress_title": "Save Your Progress", + "save_progress_desc": "Link your account to keep your stats, rank, and cosmetics safe.", + "link_discord": "Link Discord Account", + "link_via_email_placeholder": "Link via Email", + "link_button": "Link", + "log_out": "Log Out", + "welcome_back": "Welcome Back", + "sign_in_desc": "Sign in to save your stats and progress", + "or": "OR", + "email_placeholder": "Enter your email address", + "get_magic_link": "Get Magic Link", "linked_account": "Logged in as {account_name}", "fetching_account": "Fetching account information...", - "logged_in_with_discord": "Logged in with Discord", "recovery_email_sent": "Recovery email sent to {email}", - "player_id": "Player ID: {id}", "not_found": "Not Found", "clear_session": "Clear Session", "failed_to_send_recovery_email": "Failed to send recovery email", @@ -177,6 +204,7 @@ "loading": "Loading...", "error": "Error loading clan stats", "no_stats": "No clan stats available", + "no_data_yet": "No Data Yet", "clan": "Clan", "games": "Games", "win_score": "Win Score", @@ -184,7 +212,9 @@ "loss_score": "Loss Score", "loss_score_tooltip": "Weighted losses based on clan participation and match difficulty", "win_loss_ratio": "Win/Loss", - "rank": "Rank" + "ratio": "Ratio", + "rank": "Rank", + "try_again": "Try Again" }, "game_info_modal": { "title": "Game info", @@ -263,10 +293,12 @@ "continental": "Continental", "regional": "Regional", "fantasy": "Other", + "special": "Special", "arcade": "Arcade" }, "map_component": { - "loading": "Loading..." + "loading": "Loading...", + "error": "Error" }, "private_lobby": { "title": "Join Private Lobby", @@ -277,8 +309,9 @@ "checking": "Checking lobby...", "not_found": "Lobby not found. Please check the ID and try again.", "error": "An error occurred. Please try again or contact support.", - "joined_waiting": "Joined successfully! Waiting for game to start...", - "version_mismatch": "This game was created with a different version. Cannot join." + "joined_waiting": "Lobby joined! Waiting for host to start...", + "version_mismatch": "This game was created with a different version. Cannot join.", + "disabled_units": "Disabled Units" }, "public_lobby": { "join": "Join next Game", @@ -291,26 +324,32 @@ "teams_hvn": "Humans vs Nations", "teams_hvn_detailed": "{num} Humans vs {num} Nations", "teams": "{num} teams", - "players_per_team": "teams of {num}" + "players_per_team": "of {num}", + "started": "Started" }, "matchmaking_modal": { "title": "1v1 Ranked Matchmaking (ALPHA)", - "elo": "ELO: {elo}", "connecting": "Connecting to matchmaking server...", "searching": "Searching for game...", - "waiting_for_game": "Waiting for game to start..." + "waiting_for_game": "Waiting for game to start...", + "elo": "Your ELO: {elo}" }, "username": { "enter_username": "Enter your username", "not_string": "Username must be a string.", "too_short": "Username must be at least {min} characters long.", "too_long": "Username must not exceed {max} characters.", - "invalid_chars": "Username can only contain letters, numbers, spaces, underscores, and [square brackets]." + "invalid_chars": "Username can only contain letters, numbers, spaces, and underscores.", + "tag": "TAG", + "tag_too_short": "Clan tag must be 2-5 alphanumeric characters.", + "tag_invalid_chars": "Clan tag can only contain letters and numbers." }, "host_modal": { - "title": "Private Lobby", + "title": "Create Private Lobby", + "label": "Private", "mode": "Mode", "team_count": "Number of Teams", + "team_type": "Team Type", "options_title": "Options", "bots": "Bots: ", "bots_disabled": "Disabled", @@ -318,6 +357,7 @@ "nations": "Nations: ", "disable_nations": "Disable Nations", "max_timer": "Game length (minutes)", + "mins_placeholder": "Mins", "instant_build": "Instant build", "infinite_gold": "Infinite gold", "donate_gold": "Donate gold", @@ -339,7 +379,8 @@ "remove_player": "Remove {username}", "teams_Duos": "Duos (teams of 2)", "teams_Trios": "Trios (teams of 3)", - "teams_Quads": "Quads (teams of 4)" + "teams_Quads": "Quads (teams of 4)", + "teams_Humans Vs Nations": "Humans vs Nations" }, "team_colors": { "red": "Red", @@ -406,6 +447,7 @@ "anonymous_names_desc": "Hide real player names with random ones on your screen.", "lobby_id_visibility_label": "Hidden Lobby IDs", "lobby_id_visibility_desc": "Hide Lobby ID in private lobby creation", + "toggle_visibility": "Toggle Visibility", "left_click_label": "Left Click to Open Menu", "left_click_desc": "When ON, left-click opens menu and sword button attacks. When OFF, left-click attacks directly.", "left_click_menu": "Left Click Menu", @@ -419,6 +461,7 @@ "easter_writing_speed_desc": "Adjust how fast you pretend to code (x1–x100)", "easter_bug_count_label": "Bug Count", "easter_bug_count_desc": "How many bugs you're okay with (0–1000, emotionally)", + "press_a_key": "Press a key", "view_options": "View Options", "toggle_view": "Toggle View", "toggle_view_desc": "Alternate view (terrain/countries)", @@ -477,7 +520,8 @@ "exit_game_label": "Exit Game", "exit_game_info": "Return to main menu", "background_music_volume": "Background Music Volume", - "sound_effects_volume": "Sound Effects Volume" + "sound_effects_volume": "Sound Effects Volume", + "keybind_conflict_error": "The key {key} is already bound to another action." }, "chat": { "title": "Quick Chat", @@ -779,6 +823,7 @@ "colors": "Colors", "purchase": "Purchase", "show_only_owned": "My Skins", + "all_owned": "All skins owned! Check back later for new items.", "not_logged_in": "Not logged in", "blocked": { "login": "You must be logged in to access this skin.", @@ -786,7 +831,9 @@ }, "pattern": { "default": "Default" - } + }, + "select_skin": "Select Skin", + "selected": "selected" }, "flag_input": { "title": "Select Flag", @@ -857,7 +904,7 @@ "mode": "Mode", "mode_ffa": "Free-for-All", "mode_team": "Team", - "view": "View", + "replay": "Replay", "details": "Details", "ranking": "Ranking", "started": "Started", @@ -868,13 +915,20 @@ "player_stats_tree": { "public": "Public", "private": "Private", - "singleplayer": "Single Player", + "singleplayer": "Solo", "mode": "Mode", "stats_wins": "Wins", "stats_losses": "Losses", "stats_wlr": "Win:Loss Ratio", "stats_games_played": "Games Played", "mode_ffa": "Free-for-All", - "mode_team": "Team" + "mode_team": "Team", + "no_stats": "No stats recorded for this selection." + }, + "matchmaking_button": { + "play_ranked": "1v1 Ranked Matchmaking", + "description": "(ALPHA)", + "login_required": "Login to play ranked!", + "must_login": "You must be logged in to play ranked matchmaking." } } diff --git a/resources/version.txt b/resources/version.txt index 6f6d9df94..22e2e4673 100644 --- a/resources/version.txt +++ b/resources/version.txt @@ -1,2 +1 @@ -EXPERIMENTAL BUILD -FOR INTERNAL USE ONLY +x.xx.xx diff --git a/src/client/AccountModal.ts b/src/client/AccountModal.ts index 9a2fece0d..456379caf 100644 --- a/src/client/AccountModal.ts +++ b/src/client/AccountModal.ts @@ -1,5 +1,5 @@ -import { html, LitElement, TemplateResult } from "lit"; -import { customElement, query, state } from "lit/decorators.js"; +import { html, TemplateResult } from "lit"; +import { customElement, state } from "lit/decorators.js"; import { PlayerGame, PlayerStatsTree, @@ -11,19 +11,16 @@ import "./components/baseComponents/stats/DiscordUserHeader"; import "./components/baseComponents/stats/GameList"; import "./components/baseComponents/stats/PlayerStatsTable"; import "./components/baseComponents/stats/PlayerStatsTree"; +import { BaseModal } from "./components/BaseModal"; import "./components/Difficulties"; import "./components/PatternButton"; -import { isInIframe, translateText } from "./Utils"; +import { copyToClipboard, translateText } from "./Utils"; @customElement("account-modal") -export class AccountModal extends LitElement { - @query("o-modal") private modalEl!: HTMLElement & { - open: () => void; - close: () => void; - }; - +export class AccountModal extends BaseModal { @state() private email: string = ""; @state() private isLoadingUser: boolean = false; + @state() private showCopied: boolean = false; private userMeResponse: UserMeResponse | null = null; private statsTree: PlayerStatsTree | null = null; @@ -48,63 +45,246 @@ export class AccountModal extends LitElement { }); } - createRenderRoot() { - return this; + private async copyIdToClipboard() { + const id = this.userMeResponse?.player?.publicId; + if (!id) return; + + await copyToClipboard( + id, + () => (this.showCopied = true), + () => (this.showCopied = false), + ); + } + + private hasAnyStats(): boolean { + if (!this.statsTree) return false; + // Check if statsTree has any data + return ( + Object.keys(this.statsTree).length > 0 && + Object.values(this.statsTree).some( + (gameTypeStats) => + gameTypeStats && Object.keys(gameTypeStats).length > 0, + ) + ); } render() { + const content = this.isLoadingUser + ? html` +
+
+

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

+
+ ` + : this.renderInner(); + + if (this.inline) { + return content; + } + return html` - ${this.isLoadingUser - ? html` -
-

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

-
-
- ` - : this.renderInner()} + ${content}
`; } private renderInner() { - if (this.userMeResponse?.user) { - return this.renderAccountInfo(); - } else { - return this.renderLoginOptions(); - } + const isLoggedIn = !!this.userMeResponse?.user; + const title = translateText("account_modal.title"); + + return html` +
+
+
+ + + ${title} + +
+ ${isLoggedIn + ? html` +
+ ID: + +
+ ` + : ""} +
+ +
+ ${isLoggedIn ? this.renderAccountInfo() : this.renderLoginOptions()} +
+
+ `; } private renderAccountInfo() { + const me = this.userMeResponse?.user; + const isLinked = me?.discord ?? me?.email; + + if (!isLinked) { + return html` +
+
${this.renderLinkAccountSection()}
+
+ `; + } + return html`
-
-

- ${translateText("account_modal.player_id", { - id: - this.userMeResponse?.player?.publicId ?? - translateText("account_modal.not_found"), - })} -

+
+ +
+
+
+ ${translateText("account_modal.connected_as")} +
+
+ + ${this.renderLoggedInAs()} +
+
+
+ + + ${this.hasAnyStats() + ? html`
+

+ 📊 + ${translateText("account_modal.stats_overview")} +

+ +
` + : ""} + + +
+

+ 🎮 + ${translateText("game_list.recent_games")} +

+ this.viewGame(id)} + > +
-
-

${this.renderLoggedInAs()}

+
+ `; + } + + private renderLinkAccountSection(): TemplateResult { + return html` +
+
+
+

+ ${translateText("account_modal.save_progress_title")} +

+

+ ${translateText("account_modal.save_progress_desc")} +

+
-
- + +
+ + +
+
+ + +
+
- ${this.renderPlayerStats()}
`; } @@ -112,33 +292,62 @@ export class AccountModal extends LitElement { private renderLoggedInAs(): TemplateResult { const me = this.userMeResponse?.user; if (me?.discord) { - return html`

- ${translateText("account_modal.linked_account", { - account_name: me.discord.global_name ?? "", - })} -

- ${this.renderLogoutButton()}`; + return html` +
+ ${this.renderLogoutButton()} +
+ `; } else if (me?.email) { - return html`

- ${translateText("account_modal.linked_account", { - account_name: me.email, - })} -

- ${this.renderLogoutButton()}`; + return html` +
+
+ ${translateText("account_modal.linked_account", { + account_name: me.email, + })} +
+ ${this.renderLogoutButton()} +
+ `; } - return this.renderLoginOptions(); - } - private renderPlayerStats(): TemplateResult { + // "Mini" Login Options for linking account return html` - -
- this.viewGame(id)} - > +
+ + +
+
+ + +
+
+
`; } @@ -157,86 +366,133 @@ export class AccountModal extends LitElement { return html` `; } private renderLoginOptions() { return html` -
-
- -
+
+
+
+
+ + + + + +
+

+ ${translateText("account_modal.welcome_back")} +

+

+ ${translateText("account_modal.sign_in_desc")} +

+
+ +
+ -
- -
-
-
+ +
+
+ + ${translateText("account_modal.or")} + +
-
- or + + +
+
+ +
+ + + + +
+
+
- -
-
- -
- - -
- `; } @@ -267,8 +523,7 @@ export class AccountModal extends LitElement { discordLogin(); } - public open() { - this.modalEl?.open(); + protected onOpen(): void { this.isLoadingUser = true; void getUserMe() @@ -290,8 +545,10 @@ export class AccountModal extends LitElement { this.requestUpdate(); } - public close() { - this.modalEl?.close(); + protected onClose(): void { + this.dispatchEvent( + new CustomEvent("close", { bubbles: true, composed: true }), + ); } private async handleLogout() { @@ -319,104 +576,3 @@ export class AccountModal extends LitElement { } } } - -@customElement("account-button") -export class AccountButton extends LitElement { - @state() private loggedInEmail: string | null = null; - @state() private loggedInDiscord: string | null = null; - - private isVisible = true; - - @query("account-modal") private recoveryModal: AccountModal; - - constructor() { - super(); - - document.addEventListener("userMeResponse", (event: Event) => { - const customEvent = event as CustomEvent; - - if (customEvent.detail) { - const userMeResponse = customEvent.detail as UserMeResponse; - if (userMeResponse.user.email) { - this.loggedInEmail = userMeResponse.user.email; - this.requestUpdate(); - } else if (userMeResponse.user.discord) { - this.loggedInDiscord = userMeResponse.user.discord.id; - this.requestUpdate(); - } - } else { - // Clear the logged in states when user logs out - this.loggedInEmail = null; - this.loggedInDiscord = null; - this.requestUpdate(); - } - }); - } - - createRenderRoot() { - return this; - } - - render() { - if (isInIframe()) { - return html``; - } - - if (!this.isVisible) { - return html``; - } - - let buttonTitle = ""; - if (this.loggedInEmail) { - buttonTitle = translateText("account_modal.linked_account", { - account_name: this.loggedInEmail, - }); - } else if (this.loggedInDiscord) { - buttonTitle = translateText("account_modal.linked_account"); - } - - return html` -
- -
- - `; - } - - private renderIcon() { - if (this.loggedInDiscord) { - return html`Discord`; - } else if (this.loggedInEmail) { - return html`Email`; - } - return html`Logged Out`; - } - - private open() { - this.recoveryModal?.open(); - } - - public close() { - this.isVisible = false; - this.recoveryModal?.close(); - this.requestUpdate(); - } -} diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 27c2f01cb..995decabe 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -121,7 +121,26 @@ export function joinLobby( userSettings, terrainLoad, terrainMapFileLoader, - ).then((r) => r.start()); + ) + .then((r) => r.start()) + .catch((e) => { + console.error("error creating client game", e); + const startingModal = document.querySelector( + "game-starting-modal", + ) as HTMLElement; + if (startingModal) { + startingModal.classList.add("hidden"); + } + showErrorModal( + e.message, + e.stack, + lobbyConfig.gameID, + lobbyConfig.clientID, + true, + false, + "error_modal.connection_error", + ); + }); } if (message.type === "error") { if (message.error === "full-lobby") { diff --git a/src/client/DarkModeButton.ts b/src/client/DarkModeButton.ts deleted file mode 100644 index f301802af..000000000 --- a/src/client/DarkModeButton.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { LitElement, html } from "lit"; -import { customElement, state } from "lit/decorators.js"; -import { UserSettings } from "../core/game/UserSettings"; - -@customElement("dark-mode-button") -export class DarkModeButton extends LitElement { - private userSettings: UserSettings = new UserSettings(); - @state() private darkMode: boolean = this.userSettings.darkMode(); - - createRenderRoot() { - return this; - } - - connectedCallback() { - super.connectedCallback(); - window.addEventListener("dark-mode-changed", this.handleDarkModeChanged); - } - - disconnectedCallback() { - super.disconnectedCallback(); - window.removeEventListener("dark-mode-changed", this.handleDarkModeChanged); - } - - private handleDarkModeChanged = (e: Event) => { - const event = e as CustomEvent<{ darkMode: boolean }>; - this.darkMode = event.detail.darkMode; - }; - - toggleDarkMode() { - this.userSettings.toggleDarkMode(); - this.darkMode = this.userSettings.darkMode(); - } - - render() { - return html` - - `; - } -} diff --git a/src/client/FlagInput.ts b/src/client/FlagInput.ts index ddc2c9287..158c7c425 100644 --- a/src/client/FlagInput.ts +++ b/src/client/FlagInput.ts @@ -10,17 +10,7 @@ const flagKey: string = "flag"; export class FlagInput extends LitElement { @state() public flag: string = ""; - static styles = css` - @media (max-width: 768px) { - .flag-modal { - width: 80vw; - } - - .dropdown-item { - width: calc(100% / 3 - 15px); - } - } - `; + static styles = css``; public getCurrentFlag(): string { return this.flag; @@ -70,20 +60,18 @@ export class FlagInput extends LitElement { render() { return html` -
- -
+ `; } @@ -100,9 +88,7 @@ export class FlagInput extends LitElement { } else { const img = document.createElement("img"); img.src = this.flag ? `/flags/${this.flag}.svg` : `/flags/xx.svg`; - img.style.width = "100%"; - img.style.height = "100%"; - img.style.objectFit = "contain"; + img.className = "w-full h-full object-cover drop-shadow"; img.onerror = () => { if (!img.src.endsWith("/flags/xx.svg")) { img.src = "/flags/xx.svg"; diff --git a/src/client/FlagInputModal.ts b/src/client/FlagInputModal.ts index cfa3bf359..0fba25ccf 100644 --- a/src/client/FlagInputModal.ts +++ b/src/client/FlagInputModal.ts @@ -1,31 +1,62 @@ -import { LitElement, html } from "lit"; +import { html } from "lit"; import { customElement, query, state } from "lit/decorators.js"; import Countries from "resources/countries.json" with { type: "json" }; import { translateText } from "./Utils"; +import { BaseModal } from "./components/BaseModal"; @customElement("flag-input-modal") -export class FlagInputModal extends LitElement { - @query("o-modal") private modalEl!: HTMLElement & { - open: () => void; - close: () => void; - }; +export class FlagInputModal extends BaseModal { + @query("#flag-input-modal") private modalRef!: HTMLElement; @state() private search = ""; - @state() private isModalOpen = false; + public returnTo = ""; - createRenderRoot() { - return this; + updated(changedProperties: Map) { + super.updated(changedProperties); } render() { - return html` - -
+ const content = html` +
+
+
+ + + ${translateText("flag_input.title")} + +
+
+ +
- ${this.isModalOpen - ? Countries.filter( - (country) => - !country.restricted && this.includedInSearch(country), - ).map( - (country) => html` - - `, - ) - : html``} + + `, + )} +
+
+ `; + + if (this.inline) { + return content; + } + + return html` + + ${content} `; } @@ -95,29 +144,17 @@ export class FlagInputModal extends LitElement { ); } - public open() { - this.isModalOpen = true; - this.modalEl?.open(); - } - public close() { - this.isModalOpen = false; - this.modalEl?.close(); + protected onOpen(): void { + // No custom logic needed } - connectedCallback() { - super.connectedCallback(); - window.addEventListener("keydown", this.handleKeyDown); - } - - disconnectedCallback() { - window.removeEventListener("keydown", this.handleKeyDown); - super.disconnectedCallback(); - } - - private handleKeyDown = (e: KeyboardEvent) => { - if (e.code === "Escape") { - e.preventDefault(); - this.close(); + protected onClose(): void { + if (this.returnTo) { + const returnEl = document.querySelector(this.returnTo) as any; + if (returnEl?.open) { + returnEl.open(); + } + this.returnTo = ""; } - }; + } } diff --git a/src/client/GameStartingModal.ts b/src/client/GameStartingModal.ts index b03ff77a0..070684132 100644 --- a/src/client/GameStartingModal.ts +++ b/src/client/GameStartingModal.ts @@ -1,4 +1,4 @@ -import { LitElement, css, html } from "lit"; +import { LitElement, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { translateText } from "./Utils"; @@ -7,140 +7,39 @@ export class GameStartingModal extends LitElement { @state() isVisible = false; - static styles = css` - .overlay { - display: none; - position: fixed; - inset: 0; - background-color: rgba(0, 0, 0, 0.3); - backdrop-filter: blur(4px); - z-index: 9998; - } - - .overlay.visible { - display: block; - } - - .modal { - display: none; - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: rgba(30, 30, 30, 0.7); - padding: 25px; - border-radius: 10px; - z-index: 9999; - box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); - backdrop-filter: blur(5px); - color: white; - width: 300px; - text-align: center; - transition: - opacity 0.3s ease-in-out, - visibility 0.3s ease-in-out; - } - - .modal.visible { - display: block; - animation: fadeIn 0.3s ease-out; - } - - @keyframes fadeIn { - from { - opacity: 0; - transform: translate(-50%, -48%); - } - to { - opacity: 1; - transform: translate(-50%, -50%); - } - } - - .modal h2 { - margin-bottom: 15px; - font-size: 22px; - color: white; - } - - .modal p { - margin: 2px 0; - font-size: 14px; - } - - .modal .loading { - font-size: 16px; - margin-top: 20px; - margin-bottom: 20px; - background-color: rgba(0, 0, 0, 0.3); - padding: 10px; - border-radius: 5px; - } - - .button-container { - display: flex; - justify-content: center; - gap: 10px; - } - - .modal button { - padding: 12px; - font-size: 16px; - cursor: pointer; - background: rgba(255, 100, 100, 0.7); - color: white; - border: none; - border-radius: 5px; - transition: - background-color 0.2s ease, - transform 0.1s ease; - } - - .modal button:hover { - background: rgba(255, 100, 100, 0.9); - transform: translateY(-1px); - } - - .modal button:active { - transform: translateY(1px); - } - - .copyright { - font-size: 20px; - margin-top: 20px; - margin-bottom: 10px; - opacity: 1; - } - - .modal a { - display: block; - margin-top: 10px; - margin-bottom: 15px; - font-size: 20px; - color: #4a9eff; - text-decoration: none; - transition: color 0.2s ease; - } - - .modal a:hover { - color: #6bb0ff; - text-decoration: underline; - } - `; + createRenderRoot() { + return this; + } render() { + const isVisible = this.isVisible; return html` -
-