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`
`;
}
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`
`;
}
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();
}
}
}