split account modal into multiple tabs (#3986)

## Description:

The account modal was getting too large, too much scrolling.


<img width="1091" height="779" alt="Screenshot 2026-05-22 at 2 23 23 PM"
src="https://github.com/user-attachments/assets/9553c80d-7d17-4cfd-8992-88bb335a972e"
/>
<img width="970" height="781" alt="Screenshot 2026-05-22 at 2 23 33 PM"
src="https://github.com/user-attachments/assets/847e70eb-57b7-440b-adb6-bb7c18ada43c"
/>
<img width="1100" height="718" alt="Screenshot 2026-05-22 at 2 23 42 PM"
src="https://github.com/user-attachments/assets/5b08d44c-743a-4245-86a3-bf62e01008e9"
/>


## 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:

evan
This commit is contained in:
Evan
2026-05-22 15:12:01 +01:00
committed by GitHub
parent 42feb36825
commit 458d04e278
2 changed files with 107 additions and 59 deletions
+6 -1
View File
@@ -380,7 +380,12 @@
"clear_session": "Clear Session",
"failed_to_send_recovery_email": "Failed to send recovery email",
"enter_email_address": "Please enter an email address",
"public_player_id": "Public Player ID:"
"public_player_id": "Public Player ID:",
"tab_account": "Account",
"tab_stats": "Stats",
"tab_games": "Games",
"no_stats": "No stats available yet. Play some games to start tracking.",
"no_games": "No games played yet."
},
"leaderboard_modal": {
"title": "Leaderboard",
+101 -58
View File
@@ -93,81 +93,124 @@ export class AccountModal extends BaseModal {
});
}
protected renderBody() {
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") },
],
};
}
protected renderBody(tab: string) {
if (this.isLoadingUser) {
return this.renderLoadingSpinner(
translateText("account_modal.fetching_account"),
);
}
const isLoggedIn = !!this.userMeResponse?.user;
if (!this.isLinkedAccount()) {
return html`<div class="custom-scrollbar mr-1">
${this.renderLoginOptions()}
</div>`;
}
return html`
<div class="custom-scrollbar mr-1">
${isLoggedIn ? this.renderAccountInfo() : this.renderLoginOptions()}
<div class="p-6">${this.renderTab(tab)}</div>
</div>
`;
}
private renderAccountInfo() {
const me = this.userMeResponse?.user;
const isLinked = me?.discord ?? me?.email;
if (!isLinked) {
return this.renderLoginOptions();
private renderTab(tab: string): TemplateResult {
switch (tab) {
case "stats":
return this.renderStatsTab();
case "games":
return this.renderGamesTab();
default:
return this.renderAccountTab();
}
}
private renderAccountTab(): TemplateResult {
return html`
<div class="p-6">
<div class="flex flex-col gap-6">
<!-- Top Row: Connected As -->
<div class="bg-white/5 rounded-xl border border-white/10 p-6">
<div class="flex flex-col items-center gap-4">
<div
class="text-xs text-white/40 uppercase tracking-widest font-bold border-b border-white/5 pb-2 px-8"
>
${translateText("account_modal.connected_as")}
</div>
<div class="flex items-center gap-8 justify-center flex-wrap">
<discord-user-header
.data=${this.userMeResponse?.user?.discord ?? null}
></discord-user-header>
${this.renderLoggedInAs()}
</div>
<div class="flex flex-col gap-6">
<div class="bg-white/5 rounded-xl border border-white/10 p-6">
<div class="flex flex-col items-center gap-4">
<div
class="text-xs text-white/40 uppercase tracking-widest font-bold border-b border-white/5 pb-2 px-8"
>
${translateText("account_modal.connected_as")}
</div>
<div class="flex items-center gap-8 justify-center flex-wrap">
<discord-user-header
.data=${this.userMeResponse?.user?.discord ?? null}
></discord-user-header>
${this.renderLoggedInAs()}
</div>
</div>
${this.renderSubscriptionPanel()}
<!-- Middle Row: Stats Section -->
${this.hasAnyStats()
? html`<div
class="bg-white/5 rounded-xl border border-white/10 p-6"
>
<h3
class="text-lg font-bold text-white mb-4 flex items-center gap-2"
>
<span class="text-blue-400">📊</span>
${translateText("account_modal.stats_overview")}
</h3>
<player-stats-tree-view
.statsTree=${this.statsTree}
></player-stats-tree-view>
</div>`
: ""}
<!-- Bottom Row: Recent Games Section -->
<div class="bg-white/5 rounded-xl border border-white/10 p-6">
<h3
class="text-lg font-bold text-white mb-4 flex items-center gap-2"
>
<span class="text-blue-400">🎮</span>
${translateText("game_list.recent_games")}
</h3>
<game-list
.games=${this.recentGames}
.onViewGame=${(id: string) => void this.viewGame(id)}
></game-list>
</div>
</div>
${this.renderSubscriptionPanel()}
</div>
`;
}
private renderStatsTab(): TemplateResult {
if (!this.hasAnyStats()) {
return this.renderEmptyState(
"📊",
translateText("account_modal.no_stats"),
);
}
return html`
<div class="bg-white/5 rounded-xl border border-white/10 p-6">
<h3 class="text-lg font-bold text-white mb-4 flex items-center gap-2">
<span class="text-blue-400">📊</span>
${translateText("account_modal.stats_overview")}
</h3>
<player-stats-tree-view
.statsTree=${this.statsTree}
></player-stats-tree-view>
</div>
`;
}
private renderGamesTab(): TemplateResult {
if (this.recentGames.length === 0) {
return this.renderEmptyState(
"🎮",
translateText("account_modal.no_games"),
);
}
return html`
<div class="bg-white/5 rounded-xl border border-white/10 p-6">
<h3 class="text-lg font-bold text-white mb-4 flex items-center gap-2">
<span class="text-blue-400">🎮</span>
${translateText("game_list.recent_games")}
</h3>
<game-list
.games=${this.recentGames}
.onViewGame=${(id: string) => void this.viewGame(id)}
></game-list>
</div>
`;
}
private renderEmptyState(icon: string, message: string): TemplateResult {
return html`
<div
class="bg-white/5 rounded-xl border border-white/10 p-12 flex flex-col items-center justify-center text-center"
>
<div class="text-4xl mb-3">${icon}</div>
<p class="text-white/60 text-sm">${message}</p>
</div>
`;
}