From c8e0838b1543a1e12367ef8622827e660de2493d Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Sat, 17 Jan 2026 01:08:13 +0000 Subject: [PATCH] CopyButton, extract into component (#2934) ## Description: Extracted the CopyButton into its own component, and now reusing it in "Account" too. ## 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 --- resources/lang/en.json | 3 +- src/client/AccountModal.ts | 34 ++--- src/client/HostLobbyModal.ts | 112 +-------------- src/client/JoinPrivateLobbyModal.ts | 106 +------------- src/client/components/CopyButton.ts | 206 ++++++++++++++++++++++++++++ 5 files changed, 228 insertions(+), 233 deletions(-) create mode 100644 src/client/components/CopyButton.ts diff --git a/resources/lang/en.json b/resources/lang/en.json index 6027fc282..c9adfcf33 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -198,7 +198,8 @@ "not_found": "Not Found", "clear_session": "Clear Session", "failed_to_send_recovery_email": "Failed to send recovery email", - "enter_email_address": "Please enter an email address" + "enter_email_address": "Please enter an email address", + "personal_player_id": "Personal Player ID:" }, "stats_modal": { "title": "Stats", diff --git a/src/client/AccountModal.ts b/src/client/AccountModal.ts index 505537be6..8169e9566 100644 --- a/src/client/AccountModal.ts +++ b/src/client/AccountModal.ts @@ -13,16 +13,16 @@ import "./components/baseComponents/stats/GameList"; import "./components/baseComponents/stats/PlayerStatsTable"; import "./components/baseComponents/stats/PlayerStatsTree"; import { BaseModal } from "./components/BaseModal"; +import "./components/CopyButton"; import "./components/Difficulties"; import "./components/PatternButton"; import { modalHeader } from "./components/ui/ModalHeader"; -import { copyToClipboard, translateText } from "./Utils"; +import { translateText } from "./Utils"; @customElement("account-modal") 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; @@ -47,17 +47,6 @@ export class AccountModal extends BaseModal { }); } - 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 @@ -106,6 +95,8 @@ export class AccountModal extends BaseModal { private renderInner() { const isLoggedIn = !!this.userMeResponse?.user; const title = translateText("account_modal.title"); + const publicId = this.userMeResponse?.player?.publicId ?? ""; + const displayId = publicId || translateText("account_modal.not_found"); return html`
ID:${translateText("account_modal.personal_player_id")} - +
` : undefined, diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index e7d709c12..2ca75b207 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -1,6 +1,6 @@ import { TemplateResult, html } from "lit"; import { customElement, state } from "lit/decorators.js"; -import { copyToClipboard, translateText } from "../client/Utils"; +import { translateText } from "../client/Utils"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { Difficulty, @@ -15,7 +15,6 @@ import { mapCategories, } from "../core/game/Game"; import { getCompactMapNationCount } from "../core/game/NationCreation"; -import { UserSettings } from "../core/game/UserSettings"; import { ClientInfo, GameConfig, @@ -26,6 +25,7 @@ import { import { generateID } from "../core/Util"; import "./components/baseComponents/Modal"; import { BaseModal } from "./components/BaseModal"; +import "./components/CopyButton"; import "./components/Difficulties"; import "./components/FluentSlider"; import "./components/LobbyTeamView"; @@ -65,19 +65,16 @@ export class HostLobbyModal extends BaseModal { @state() private startingGold: boolean = false; @state() private startingGoldValue: number | undefined = undefined; @state() private lobbyId = ""; - @state() private copySuccess = false; @state() private lobbyUrlSuffix = ""; @state() private clients: ClientInfo[] = []; @state() private useRandomMap: boolean = false; @state() private disabledUnits: UnitType[] = []; @state() private lobbyCreatorClientID: string = ""; - @state() private lobbyIdVisible: boolean = true; @state() private nationCount: number = 0; private playersInterval: NodeJS.Timeout | null = null; // Add a new timer for debouncing bot changes private botsUpdateTimer: number | null = null; - private userSettings: UserSettings = new UserSettings(); private mapLoader = terrainMapFileLoader; private leaveLobbyOnClose = true; @@ -144,91 +141,11 @@ export class HostLobbyModal extends BaseModal { }, ariaLabel: translateText("common.back"), rightContent: html` - -
- - - -
+ `, })} @@ -997,10 +914,6 @@ export class HostLobbyModal extends BaseModal { protected onOpen(): void { this.lobbyCreatorClientID = generateID(); - this.lobbyIdVisible = this.userSettings.get( - "settings.lobbyIdVisibility", - true, - ); createLobby(this.lobbyCreatorClientID) .then(async (lobby) => { @@ -1119,10 +1032,8 @@ export class HostLobbyModal extends BaseModal { this.useRandomMap = false; this.disabledUnits = []; this.lobbyId = ""; - this.copySuccess = false; this.clients = []; this.lobbyCreatorClientID = ""; - this.lobbyIdVisible = true; this.nationCount = 0; this.goldMultiplier = false; this.goldMultiplierValue = undefined; @@ -1403,15 +1314,6 @@ export class HostLobbyModal extends BaseModal { return response; } - private async copyToClipboard() { - const url = await this.buildLobbyUrl(); - await copyToClipboard( - url, - () => (this.copySuccess = true), - () => (this.copySuccess = false), - ); - } - private async pollPlayers() { const config = await getServerConfigFromClient(); fetch(`/${config.workerPath(this.lobbyId)}/api/game/${this.lobbyId}`, { diff --git a/src/client/JoinPrivateLobbyModal.ts b/src/client/JoinPrivateLobbyModal.ts index cb65b428f..117e6d753 100644 --- a/src/client/JoinPrivateLobbyModal.ts +++ b/src/client/JoinPrivateLobbyModal.ts @@ -1,6 +1,6 @@ import { html, TemplateResult } from "lit"; import { customElement, query, state } from "lit/decorators.js"; -import { copyToClipboard, translateText } from "../client/Utils"; +import { translateText } from "../client/Utils"; import { ClientInfo, GAME_ID_REGEX, @@ -11,10 +11,10 @@ import { import { generateID } from "../core/Util"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { GameMode } from "../core/game/Game"; -import { UserSettings } from "../core/game/UserSettings"; import { getApiBase } from "./Api"; import { JoinLobbyEvent } from "./Main"; import { BaseModal } from "./components/BaseModal"; +import "./components/CopyButton"; import "./components/Difficulties"; import "./components/LobbyTeamView"; import { modalHeader } from "./components/ui/ModalHeader"; @@ -26,12 +26,9 @@ export class JoinPrivateLobbyModal extends BaseModal { @state() private players: ClientInfo[] = []; @state() private gameConfig: GameConfig | null = null; @state() private lobbyCreatorClientID: string | null = null; - @state() private lobbyIdVisible: boolean = true; - @state() private copySuccess: boolean = false; @state() private currentLobbyId: string = ""; private playersInterval: NodeJS.Timeout | null = null; - private userSettings: UserSettings = new UserSettings(); private leaveLobbyOnClose = true; @@ -50,91 +47,7 @@ export class JoinPrivateLobbyModal extends BaseModal { ariaLabel: translateText("common.close"), rightContent: this.hasJoined ? html` - -
- -
{ - (e.currentTarget as HTMLElement).classList.add( - "select-all", - ); - }} - @mouseleave=${(e: Event) => { - (e.currentTarget as HTMLElement).classList.remove( - "select-all", - ); - }} - class="font-mono text-xs font-bold text-white px-2 cursor-pointer select-none min-w-[80px] text-center truncate tracking-wider" - title="${translateText("common.click_to_copy")}" - > - ${this.copySuccess - ? translateText("common.copied") - : this.lobbyIdVisible - ? this.currentLobbyId - : "••••••••"} -
- -
+ ` : undefined, })} @@ -347,10 +260,6 @@ export class JoinPrivateLobbyModal extends BaseModal { public open(id: string = "") { super.open(); - this.lobbyIdVisible = this.userSettings.get( - "settings.lobbyIdVisibility", - true, - ); if (id) { this.setLobbyId(id); this.joinLobby(); @@ -396,15 +305,6 @@ export class JoinPrivateLobbyModal extends BaseModal { this.close(); } - private async copyToClipboard() { - const config = await getServerConfigFromClient(); - await copyToClipboard( - `${location.origin}/${config.workerPath(this.currentLobbyId)}/game/${this.currentLobbyId}`, - () => (this.copySuccess = true), - () => (this.copySuccess = false), - ); - } - private isValidLobbyId(value: string): boolean { return GAME_ID_REGEX.test(value); } diff --git a/src/client/components/CopyButton.ts b/src/client/components/CopyButton.ts new file mode 100644 index 000000000..13742cc58 --- /dev/null +++ b/src/client/components/CopyButton.ts @@ -0,0 +1,206 @@ +import { LitElement, html } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { getServerConfigFromClient } from "../../core/configuration/ConfigLoader"; +import { UserSettings } from "../../core/game/UserSettings"; +import { copyToClipboard, translateText } from "../Utils"; + +@customElement("copy-button") +export class CopyButton extends LitElement { + @property({ type: String, attribute: "lobby-id" }) lobbyId = ""; + @property({ type: String, attribute: "lobby-suffix" }) lobbySuffix = ""; + @property({ type: Boolean, attribute: "include-lobby-query" }) + includeLobbyQuery = false; + @property({ type: String, attribute: "copy-text" }) copyText = ""; + @property({ type: String, attribute: "display-text" }) displayText = ""; + @property({ type: Boolean, attribute: "show-visibility-toggle" }) + showVisibilityToggle = true; + @property({ type: Boolean, attribute: "show-copy-icon" }) + showCopyIcon = true; + @property({ type: Boolean }) compact = false; + + @state() private copySuccess = false; + @state() private lobbyIdVisible = true; + + private userSettings: UserSettings = new UserSettings(); + private maskLabel = html`••••••••`; + + createRenderRoot() { + return this; + } + + protected willUpdate( + changedProperties: Map, + ) { + if (changedProperties.has("lobbyId")) { + this.lobbyIdVisible = this.userSettings.get( + "settings.lobbyIdVisibility", + true, + ); + this.copySuccess = false; + } + if (changedProperties.has("copyText")) { + this.copySuccess = false; + } + if ( + changedProperties.has("showVisibilityToggle") || + changedProperties.has("compact") + ) { + if (!this.showVisibilityToggle || this.compact) { + this.lobbyIdVisible = true; + } + } + } + + private toggleVisibility() { + if (!this.showVisibilityToggle || this.compact) return; + this.lobbyIdVisible = !this.lobbyIdVisible; + } + + private enableSelectAll(e: Event) { + (e.currentTarget as HTMLElement).classList.add("select-all"); + } + + private clearSelectAll(e: Event) { + (e.currentTarget as HTMLElement).classList.remove("select-all"); + } + + private async buildCopyUrl(): Promise { + const config = await getServerConfigFromClient(); + let url = `${window.location.origin}/${config.workerPath(this.lobbyId)}/game/${this.lobbyId}`; + if (this.includeLobbyQuery) { + url += `?lobby&s=${encodeURIComponent(this.lobbySuffix)}`; + } + return url; + } + + private async resolveCopyText(): Promise { + if (this.copyText) return this.copyText; + if (!this.lobbyId) return ""; + return await this.buildCopyUrl(); + } + + private async handleCopy() { + const text = await this.resolveCopyText(); + if (!text) return; + await copyToClipboard( + text, + () => (this.copySuccess = true), + () => (this.copySuccess = false), + ); + } + + private canCopy() { + return Boolean(this.copyText || this.lobbyId); + } + + render() { + const canCopy = this.canCopy(); + const allowMask = this.showVisibilityToggle && !this.compact; + const rawLabel = this.displayText || this.lobbyId || this.copyText; + const label = this.copySuccess + ? translateText("common.copied") + : allowMask && !this.lobbyIdVisible + ? this.maskLabel + : rawLabel; + const disabledClass = canCopy ? "" : "opacity-60 cursor-not-allowed"; + const toggleDisabled = !this.lobbyId; + const toggleClass = toggleDisabled ? "opacity-60 cursor-not-allowed" : ""; + + if (this.compact) { + return html` + + `; + } + + return html` +
+ ${this.showVisibilityToggle + ? html`` + : ""} + + ${this.showCopyIcon + ? html`` + : ""} +
+ `; + } +}