import { html, TemplateResult } from "lit"; import { customElement, state } from "lit/decorators.js"; import { ClientEnv } from "src/client/ClientEnv"; import { AchievementsResponse, PlayerGame, PlayerStatsTree, UserMeResponse, } from "../core/ApiSchemas"; import { assetUrl } from "../core/AssetUrls"; import { Cosmetics } from "../core/CosmeticSchemas"; import { fetchPlayerById, getUserMe } from "./Api"; import { discordLogin, logOut, sendMagicLink } from "./Auth"; import "./components/baseComponents/stats/DiscordUserHeader"; import "./components/baseComponents/stats/GameList"; import "./components/baseComponents/stats/PlayerAchievements"; import "./components/baseComponents/stats/PlayerStatsTable"; import "./components/baseComponents/stats/PlayerStatsTree"; import { BaseModal } from "./components/BaseModal"; import "./components/CopyButton"; import "./components/CurrencyDisplay"; import "./components/Difficulties"; import "./components/FriendsList"; import "./components/SubscriptionPanel"; import { modalHeader } from "./components/ui/ModalHeader"; import { fetchCosmetics } from "./Cosmetics"; import { translateText } from "./Utils"; @customElement("account-modal") export class AccountModal extends BaseModal { protected routerName = "account"; @state() private email: string = ""; @state() private isLoadingUser: boolean = false; private userMeResponse: UserMeResponse | null = null; private statsTree: PlayerStatsTree | null = null; private recentGames: PlayerGame[] = []; private achievementGroups: AchievementsResponse = []; private cosmetics: Cosmetics | null = null; constructor() { super(); document.addEventListener("userMeResponse", (event: Event) => { const customEvent = event as CustomEvent; if (customEvent.detail) { this.userMeResponse = customEvent.detail as UserMeResponse; if (this.userMeResponse?.player?.publicId === undefined) { this.statsTree = null; this.recentGames = []; this.achievementGroups = []; } else { this.achievementGroups = this.getUserMeAchievementGroups( this.userMeResponse, ); } } else { this.statsTree = null; this.recentGames = []; this.achievementGroups = []; this.requestUpdate(); } }); } private getUserMeAchievementGroups( userMeResponse: UserMeResponse | null, ): AchievementsResponse { const achievements = userMeResponse?.player?.achievements; if (!achievements) return []; return [ { type: "singleplayer-map", data: achievements.singleplayerMap, }, { type: "player", data: achievements.player ?? [], }, ]; } 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, ) ); } protected renderHeaderSlot() { const isLoggedIn = !!this.userMeResponse?.user; const publicId = this.userMeResponse?.player?.publicId ?? ""; const displayId = publicId || translateText("account_modal.not_found"); return modalHeader({ title: translateText("account_modal.title"), onBack: () => this.close(), ariaLabel: translateText("common.back"), rightContent: isLoggedIn && !this.isLoadingUser ? html`
${translateText("account_modal.public_player_id")}
` : undefined, }); } private isLinkedAccount(): boolean { const me = this.userMeResponse?.user; return !!(me?.discord ?? me?.email); } protected modalConfig() { if (this.isLoadingUser || !this.isLinkedAccount()) { return {}; } return { tabs: [ { key: "account", label: translateText("account_modal.tab_account") }, { key: "stats", label: translateText("account_modal.tab_stats") }, { key: "games", label: translateText("account_modal.tab_games") }, { key: "achievements", label: translateText("account_modal.tab_achievements"), }, { key: "friends", label: translateText("account_modal.tab_friends") }, ], }; } protected renderBody(tab: string) { if (this.isLoadingUser) { return this.renderLoadingSpinner( translateText("account_modal.fetching_account"), ); } if (!this.isLinkedAccount()) { return html`
${this.renderLoginOptions()}
`; } return html`
${this.renderTab(tab)}
`; } private renderTab(tab: string): TemplateResult { switch (tab) { case "stats": return this.renderStatsTab(); case "games": return this.renderGamesTab(); case "achievements": return this.renderAchievementsTab(); case "friends": return this.renderFriendsTab(); default: return this.renderAccountTab(); } } private renderFriendsTab(): TemplateResult { const myPublicId = this.userMeResponse?.player?.publicId ?? ""; return html``; } private renderAccountTab(): TemplateResult { return html`
${translateText("account_modal.connected_as")}
${this.renderLoggedInAs()}
${this.renderSubscriptionPanel()}
`; } private renderAchievementsTab(): TemplateResult { const achievements = this.achievementGroups.length > 0 ? this.achievementGroups : this.getUserMeAchievementGroups(this.userMeResponse); return html`

${translateText("account_modal.achievements")}

`; } private renderStatsTab(): TemplateResult { if (!this.hasAnyStats()) { return this.renderEmptyState( "📊", translateText("account_modal.no_stats"), ); } return html`

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

`; } private renderGamesTab(): TemplateResult { if (this.recentGames.length === 0) { return this.renderEmptyState( "🎮", translateText("account_modal.no_games"), ); } return html`

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

void this.viewGame(id)} >
`; } private renderEmptyState(icon: string, message: string): TemplateResult { return html`
${icon}

${message}

`; } private renderSubscriptionPanel(): TemplateResult | "" { const sub = this.userMeResponse?.player?.subscription; if (!sub) return ""; const cosmetic = this.cosmetics?.subscriptions?.[sub.tier] ?? null; return html``; } private renderCurrency(): TemplateResult { const currency = this.userMeResponse?.player?.currency; if (!currency) return html``; return html` `; } private renderLoggedInAs(): TemplateResult { const me = this.userMeResponse?.user; if (me?.discord) { return html`
${this.renderCurrency()} ${this.renderLogoutButton()}
`; } else if (me?.email) { return html`
${translateText("account_modal.linked_account", { account_name: me.email, })}
${this.renderCurrency()} ${this.renderLogoutButton()}
`; } return html``; } private async viewGame(gameId: string): Promise { this.close(); const encodedGameId = encodeURIComponent(gameId); const newUrl = `/${ClientEnv.workerPath(gameId)}/game/${encodedGameId}`; history.pushState({ join: gameId }, "", newUrl); window.dispatchEvent( new CustomEvent("join-changed", { detail: { gameId: encodedGameId } }), ); } private renderLogoutButton(): TemplateResult { return html` `; } private renderLoginOptions() { return html`

${translateText("account_modal.sign_in_desc")}

${this.renderCurrency()}
${translateText("account_modal.or")}
`; } private handleEmailInput(e: Event) { const target = e.target as HTMLInputElement; this.email = target.value; } private async handleSubmit() { if (!this.email) { alert(translateText("account_modal.enter_email_address")); return; } const success = await sendMagicLink(this.email); if (success) { alert( translateText("account_modal.recovery_email_sent", { email: this.email, }), ); } else { alert(translateText("account_modal.failed_to_send_recovery_email")); } } private handleDiscordLogin() { discordLogin(); } protected onOpen(): void { this.isLoadingUser = true; void fetchCosmetics().then((cosmetics) => { this.cosmetics = cosmetics; this.requestUpdate(); }); void getUserMe() .then((userMe) => { if (userMe) { this.userMeResponse = userMe; this.achievementGroups = this.getUserMeAchievementGroups(userMe); if (this.userMeResponse?.player?.publicId) { this.loadPlayerProfile(this.userMeResponse.player.publicId); } } this.isLoadingUser = false; this.requestUpdate(); }) .catch((err) => { console.warn("Failed to fetch user info in AccountModal.open():", err); this.isLoadingUser = false; this.requestUpdate(); }); this.requestUpdate(); } protected onClose(): void { this.dispatchEvent( new CustomEvent("close", { bubbles: true, composed: true }), ); } private async handleLogout() { await logOut(); this.close(); // Refresh the page after logout to update the UI state window.location.reload(); } private async loadPlayerProfile(publicId: string): Promise { try { const data = await fetchPlayerById(publicId); if (!data) { this.requestUpdate(); return; } this.recentGames = data.games; this.statsTree = data.stats; this.achievementGroups = data.achievements ?? this.getUserMeAchievementGroups(this.userMeResponse); this.requestUpdate(); } catch (err) { console.warn("Failed to load player data:", err); this.requestUpdate(); } } }