diff --git a/src/client/ClanModal.ts b/src/client/ClanModal.ts index f838f7dba..a0989811b 100644 --- a/src/client/ClanModal.ts +++ b/src/client/ClanModal.ts @@ -60,8 +60,20 @@ export class ClanModal extends BaseModal { } | null = null; render() { - const content = this.renderInner(); - if (this.inline) return content; + const onListView = this.view === "list" && !this.selectedClanTag; + const tabs = onListView + ? [ + { key: "my-clans", label: translateText("clan_modal.my_clans") }, + { key: "browse", label: translateText("clan_modal.browse") }, + ] + : []; + const header = onListView + ? modalHeader({ + title: translateText("clan_modal.title"), + onBack: () => this.close(), + ariaLabel: translateText("common.back"), + }) + : null; return html` this.handleTabChange(key as Tab)} > - ${content} + ${header ? html`
${header}
` : ""} + ${this.renderInner()}
`; } + private handleTabChange(tab: Tab) { + this.activeTab = tab; + this.view = "list"; + this.selectedClan = null; + this.selectedClanTag = ""; + if (tab === "my-clans") { + this.loadMyClans(); + } + } + protected onOpen(): void { this.loadMyClans(); } @@ -131,16 +157,7 @@ export class ClanModal extends BaseModal { private renderInner() { if (this.loading) { - return html` -
- ${modalHeader({ - title: translateText("clan_modal.title"), - onBack: () => this.close(), - ariaLabel: translateText("common.back"), - })} - ${this.renderLoadingSpinner()} -
- `; + return this.renderLoadingSpinner(); } if (this.view === "my-requests") { @@ -289,30 +306,20 @@ export class ClanModal extends BaseModal { >`; } - // List view (tabs + my clans / browse) + // List view (my clans / browse) — header + tabs are rendered by o-modal return html` -
- ${modalHeader({ - title: translateText("clan_modal.title"), - onBack: () => this.close(), - ariaLabel: translateText("common.back"), - })} - ${this.renderTabs()} -
- ${this.activeTab === "my-clans" - ? this.renderMyClans() - : html`) => { - this.browseCache = e.detail; - }} - @clan-select=${(e: CustomEvent<{ tag: string }>) => - this.openDetail(e.detail.tag)} - >`} -
-
+ ${this.activeTab === "my-clans" + ? this.renderMyClans() + : html`) => { + this.browseCache = e.detail; + }} + @clan-select=${(e: CustomEvent<{ tag: string }>) => + this.openDetail(e.detail.tag)} + >`} `; } @@ -321,44 +328,6 @@ export class ClanModal extends BaseModal { this.view = "detail"; } - private renderTabs() { - const tabs: { key: Tab; label: string }[] = [ - { key: "my-clans", label: translateText("clan_modal.my_clans") }, - { key: "browse", label: translateText("clan_modal.browse") }, - ]; - - return html` -
- ${tabs.map( - (tab) => html` - - `, - )} -
- `; - } - private renderMyClans() { const hasClans = this.myClans.length > 0; const hasRequests = this.myPendingRequests.length > 0; diff --git a/src/client/LangSelector.ts b/src/client/LangSelector.ts index 6e2d1b00a..97d369d68 100644 --- a/src/client/LangSelector.ts +++ b/src/client/LangSelector.ts @@ -210,7 +210,6 @@ export class LangSelector extends LitElement { "join-lobby-modal", "emoji-table", "leader-board", - "leaderboard-tabs", "leaderboard-player-list", "leaderboard-clan-table", "build-menu", diff --git a/src/client/LeaderboardModal.ts b/src/client/LeaderboardModal.ts index d0de01e11..c167f8708 100644 --- a/src/client/LeaderboardModal.ts +++ b/src/client/LeaderboardModal.ts @@ -5,7 +5,6 @@ import "./components/leaderboard/LeaderboardClanTable"; import type { LeaderboardClanTable } from "./components/leaderboard/LeaderboardClanTable"; import "./components/leaderboard/LeaderboardPlayerList"; import type { LeaderboardPlayerList } from "./components/leaderboard/LeaderboardPlayerList"; -import "./components/leaderboard/LeaderboardTabs"; import { modalHeader } from "./components/ui/ModalHeader"; import { translateText } from "./Utils"; @@ -81,46 +80,13 @@ export class LeaderboardModal extends BaseModal { >(${translateText("leaderboard_modal.refresh_time")})`; - const content = html` -
- ${modalHeader({ - titleContent: html` -
- - ${translateText("leaderboard_modal.title")} - - ${this.activeTab === "clans" ? dateRange : ""} - ${this.activeTab === "players" ? refreshTime : ""} -
- `, - onBack: () => this.close(), - ariaLabel: translateText("common.close"), - })} - -
- ) => - this.handleTabChange(event.detail)} - > -
- - , - ) => this.handleClanDateRangeChange(event)} - > -
-
-
- `; - - if (this.inline) return content; + const tabs = [ + { + key: "players", + label: translateText("leaderboard_modal.ranked_tab"), + }, + { key: "clans", label: translateText("leaderboard_modal.clans_tab") }, + ]; return html` + this.handleTabChange(key as "players" | "clans")} > - ${content} +
+ ${modalHeader({ + titleContent: html` +
+ + ${translateText("leaderboard_modal.title")} + + ${this.activeTab === "clans" ? dateRange : ""} + ${this.activeTab === "players" ? refreshTime : ""} +
+ `, + onBack: () => this.close(), + ariaLabel: translateText("common.close"), + })} +
+
+ + , + ) => this.handleClanDateRangeChange(event)} + > +
`; } diff --git a/src/client/Store.ts b/src/client/Store.ts index f4e80b444..460c15a47 100644 --- a/src/client/Store.ts +++ b/src/client/Store.ts @@ -41,43 +41,12 @@ export class StoreModal extends BaseModal { } private renderHeader(): TemplateResult { - return html` - ${modalHeader({ - title: translateText("store.title"), - onBack: () => this.close(), - ariaLabel: translateText("common.back"), - rightContent: html``, - })} -
- - - -
- `; + return modalHeader({ + title: translateText("store.title"), + onBack: () => this.close(), + ariaLabel: translateText("common.back"), + rightContent: html``, + }); } private renderPatternGrid(): TemplateResult { @@ -188,22 +157,18 @@ export class StoreModal extends BaseModal { render() { if (!this.isActive && !this.inline) return html``; - const content = html` -
- ${this.renderHeader()} -
- ${this.activeTab === "patterns" - ? this.renderPatternGrid() - : this.activeTab === "flags" - ? this.renderFlagGrid() - : this.renderPackGrid()} -
-
- `; + const tabs = [ + { key: "packs", label: translateText("store.packs") }, + { key: "patterns", label: translateText("store.patterns") }, + { key: "flags", label: translateText("store.flags") }, + ]; - if (this.inline) { - return content; - } + const grid = + this.activeTab === "patterns" + ? this.renderPatternGrid() + : this.activeTab === "flags" + ? this.renderFlagGrid() + : this.renderPackGrid(); return html` + (this.activeTab = key as "patterns" | "flags" | "packs")} > - ${content} +
${this.renderHeader()}
+ ${grid}
`; } diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index 1d599ad0d..7860b9296 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -319,51 +319,10 @@ export class UserSettingModal extends BaseModal { ? this.renderBasicSettings() : this.renderKeybindSettings(); - const content = html` -
-
- ${modalHeader({ - title: translateText("user_setting.title"), - onBack: () => this.close(), - ariaLabel: translateText("common.back"), - showDivider: true, - })} - - -
- -
-
${activeContent}
-
-
- `; - - if (this.inline) { - return content; - } + const tabs = [ + { key: "basic", label: translateText("user_setting.tab_basic") }, + { key: "keybinds", label: translateText("user_setting.tab_keybinds") }, + ]; return html` + (this.activeTab = key as "basic" | "keybinds")} > - ${content} +
+ ${modalHeader({ + title: translateText("user_setting.title"), + onBack: () => this.close(), + ariaLabel: translateText("common.back"), + showDivider: true, + })} +
+
${activeContent}
`; } diff --git a/src/client/components/baseComponents/Modal.ts b/src/client/components/baseComponents/Modal.ts index 33bc7e861..fc4801064 100644 --- a/src/client/components/baseComponents/Modal.ts +++ b/src/client/components/baseComponents/Modal.ts @@ -2,6 +2,8 @@ import { LitElement, html, unsafeCSS } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import tailwindStyles from "../../styles.css?inline"; +export type OModalTab = { key: string; label: string }; + @customElement("o-modal") export class OModal extends LitElement { static styles = [unsafeCSS(tailwindStyles)]; @@ -28,6 +30,15 @@ export class OModal extends LitElement { @property({ type: String }) public maxWidth = ""; + @property({ type: Array }) + public tabs: OModalTab[] = []; + + @property({ type: String }) + public activeTab = ""; + + @property({ attribute: false }) + public onTabChange?: (key: string) => void; + public onClose?: () => void; public open() { @@ -60,7 +71,48 @@ export class OModal extends LitElement { super.disconnectedCallback(); } + private handleTabClick(key: string) { + this.onTabChange?.(key); + } + + private renderTabs() { + return html` +
+ ${this.tabs.map((tab) => { + const active = this.activeTab === tab.key; + return html` + + `; + })} +
+ `; + } + render() { + const shouldRender = this.isModalOpen || this.inline; + if (!shouldRender) { + return html``; + } + const backdropClass = this.inline ? "relative z-10 w-full h-full flex items-stretch bg-transparent" : "fixed inset-0 z-[9999] bg-black/60 flex items-center justify-center overflow-hidden"; @@ -73,42 +125,45 @@ export class OModal extends LitElement { const wrapperStyle = !this.inline && this.maxWidth ? `max-width: ${this.maxWidth};` : ""; + const hasTabs = this.tabs.length > 0; + const sectionClass = hasTabs + ? "relative flex-1 min-h-0 flex flex-col text-white bg-black/70 backdrop-blur-xl lg:rounded-2xl lg:border border-white/10 overflow-hidden" + : "relative flex-1 min-h-0 flex flex-col text-white bg-[#23232382] backdrop-blur-md lg:rounded-lg overflow-hidden"; + return html` - ${this.isModalOpen - ? html` - `; } } diff --git a/src/client/components/leaderboard/LeaderboardTabs.ts b/src/client/components/leaderboard/LeaderboardTabs.ts deleted file mode 100644 index 9401e200d..000000000 --- a/src/client/components/leaderboard/LeaderboardTabs.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { translateText } from "../../Utils"; - -export type LeaderboardTab = "players" | "clans"; - -@customElement("leaderboard-tabs") -export class LeaderboardTabs extends LitElement { - @property({ type: String }) activeTab: LeaderboardTab = "players"; - - createRenderRoot() { - return this; - } - - private baseTabClass = - "px-6 py-2 rounded-full text-sm font-bold uppercase tracking-wider transition-all cursor-pointer select-none"; - private activeTabClass = "bg-blue-600 text-white"; - private inactiveTabClass = - "text-white/40 hover:text-white/60 hover:bg-white/5"; - - private getTabClass(active: boolean) { - return [ - this.baseTabClass, - active ? this.activeTabClass : this.inactiveTabClass, - ].join(" "); - } - - @state() - private playerClass = this.getTabClass(this.activeTab === "players"); - @state() - private clanClass = this.getTabClass(this.activeTab === "clans"); - - private handleTabChange(tab: LeaderboardTab) { - this.dispatchEvent( - new CustomEvent("tab-change", { - detail: tab, - bubbles: true, - composed: true, - }), - ); - - this.playerClass = this.getTabClass(tab === "players"); - this.clanClass = this.getTabClass(tab === "clans"); - } - - render() { - return html` -
- - -
- `; - } -} diff --git a/tests/client/LeaderboardModal.test.ts b/tests/client/LeaderboardModal.test.ts index a46ef3f5e..2bef31f1b 100644 --- a/tests/client/LeaderboardModal.test.ts +++ b/tests/client/LeaderboardModal.test.ts @@ -101,6 +101,7 @@ beforeEach(() => { ); }); +import "../../src/client/components/baseComponents/Modal"; import { LeaderboardModal } from "../../src/client/LeaderboardModal"; describe("LeaderboardModal", () => { @@ -334,7 +335,14 @@ describe("LeaderboardModal", () => { }), }); - const tab = modal.querySelector("#clan-leaderboard-tab"); + modal.inline = true; + await modal.updateComplete; + const oModal = modal.querySelector("o-modal"); + await (oModal as unknown as { updateComplete: Promise }) + .updateComplete; + const tab = oModal!.shadowRoot!.querySelector( + 'button[role="tab"][data-key="clans"]', + ); expect(tab).toBeTruthy(); tab!.dispatchEvent(new MouseEvent("click", { bubbles: true }));