Add PlayerInfoModal (#2058)

## Description:

This PR adds the Player Status display to the AccountModal.
<img width="573" height="640" alt="スクリーンショット 2025-10-11 6 44 25"
src="https://github.com/user-attachments/assets/54c36bde-7c9c-4431-8ac9-e7f4089971f4"
/>


(origin pr:https://github.com/openfrontio/OpenFrontIO/pull/1758)

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

aotumuri

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
Aotumuri
2025-10-17 12:01:19 +09:00
committed by GitHub
parent 4eaf3de5de
commit 19081bf21b
2 changed files with 116 additions and 11 deletions
+1
View File
@@ -155,6 +155,7 @@
"account_modal": {
"title": "Account",
"logged_in_as": "Logged in as {email}",
"fetching_account": "Fetching account information...",
"logged_in_with_discord": "Logged in with Discord",
"recovery_email_sent": "Recovery email sent to {email}"
},
+115 -11
View File
@@ -1,9 +1,23 @@
import { html, LitElement, TemplateResult } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { UserMeResponse } from "../core/ApiSchemas";
import {
PlayerGame,
PlayerStatsTree,
UserMeResponse,
} from "../core/ApiSchemas";
import "./components/baseComponents/stats/DiscordUserHeader";
import "./components/baseComponents/stats/GameList";
import "./components/baseComponents/stats/PlayerStatsTable";
import "./components/baseComponents/stats/PlayerStatsTree";
import "./components/Difficulties";
import "./components/PatternButton";
import { discordLogin, getApiBase, getUserMe, logOut } from "./jwt";
import {
discordLogin,
fetchPlayerById,
getApiBase,
getUserMe,
logOut,
} from "./jwt";
import { isInIframe, translateText } from "./Utils";
@customElement("account-modal")
@@ -14,12 +28,33 @@ export class AccountModal extends LitElement {
};
@state() private email: string = "";
@state() private isLoadingUser: boolean = false;
private loggedInEmail: string | null = null;
private loggedInDiscord: string | null = null;
private userMeResponse: UserMeResponse | null = null;
private playerId: string | null = null;
private statsTree: PlayerStatsTree | null = null;
private recentGames: PlayerGame[] = [];
constructor() {
super();
document.addEventListener("userMeResponse", (event: Event) => {
const customEvent = event as CustomEvent;
if (customEvent.detail) {
this.userMeResponse = customEvent.detail as UserMeResponse;
this.playerId = this.userMeResponse?.player?.publicId;
if (this.playerId === undefined) {
this.statsTree = null;
this.recentGames = [];
}
} else {
this.statsTree = null;
this.recentGames = [];
this.requestUpdate();
}
});
}
createRenderRoot() {
@@ -38,6 +73,16 @@ export class AccountModal extends LitElement {
}
private renderInner() {
if (this.isLoadingUser) {
return html`
<div class="flex flex-col items-center justify-center p-6 text-white">
<p class="mb-2">${translateText("account_modal.fetching_account")}</p>
<div
class="w-6 h-6 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"
></div>
</div>
`;
}
if (this.loggedInDiscord) {
return this.renderLoggedInDiscord();
} else if (this.loggedInEmail) {
@@ -47,15 +92,39 @@ export class AccountModal extends LitElement {
}
}
private viewGame(gameId: string): void {
this.close();
const path = location.pathname;
const { search } = location;
const hash = `#join=${encodeURIComponent(gameId)}`;
const newUrl = `${path}${search}${hash}`;
history.pushState({ join: gameId }, "", newUrl);
window.dispatchEvent(new HashChangeEvent("hashchange"));
}
private renderLoggedInDiscord() {
return html`
<div class="p-6">
<div class="mb-4">
<p class="text-white text-center mb-4">
<div class="mb-4 text-center">
<p class="text-white mb-4">
Logged in with Discord as ${this.loggedInDiscord}
</p>
${this.logoutButton()}
</div>
<div class="flex flex-col items-center mt-2 mb-4">
<discord-user-header
.data=${this.userMeResponse?.user?.discord ?? null}
></discord-user-header>
<player-stats-tree-view
.statsTree=${this.statsTree}
></player-stats-tree-view>
<hr class="w-2/3 border-gray-600 my-2" />
<game-list
.games=${this.recentGames}
.onViewGame=${(id: string) => this.viewGame(id)}
></game-list>
</div>
${this.logoutButton()}
</div>
`;
}
@@ -208,13 +277,30 @@ export class AccountModal extends LitElement {
discordLogin();
}
public async open() {
const userMe = await getUserMe();
if (userMe) {
this.loggedInEmail = userMe.user.email ?? null;
this.loggedInDiscord = userMe.user.discord?.global_name ?? null;
}
public open() {
this.modalEl?.open();
this.isLoadingUser = true;
void getUserMe()
.then((userMe) => {
if (userMe) {
this.loggedInEmail = userMe.user.email ?? null;
this.loggedInDiscord = userMe.user.discord?.global_name ?? null;
if (this.playerId) {
this.loadFromApi(this.playerId);
}
} else {
this.loggedInEmail = null;
this.loggedInDiscord = null;
}
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();
}
@@ -228,6 +314,24 @@ export class AccountModal extends LitElement {
// Refresh the page after logout to update the UI state
window.location.reload();
}
private async loadFromApi(playerId: string): Promise<void> {
try {
const data = await fetchPlayerById(playerId);
if (!data) {
this.requestUpdate();
return;
}
this.recentGames = data.games;
this.statsTree = data.stats;
this.requestUpdate();
} catch (err) {
console.warn("Failed to load player data:", err);
this.requestUpdate();
}
}
}
@customElement("account-button")