feat: add public player profile page

This commit is contained in:
2026-03-06 13:49:14 -08:00
committed by Naomi Carrigan
parent f734176965
commit 32c13f73c4
5 changed files with 313 additions and 1 deletions
@@ -47,11 +47,14 @@ export const GameLayout = (): React.JSX.Element => {
if (!state) return <div className="loading-screen"><p>Loading...</p></div>;
const profileUrl = `/profile/${state.player.discordId}`;
return (
<div className="game-layout">
<ResourceBar
resources={state.resources}
prestigeCount={state.prestige.count}
profileUrl={profileUrl}
/>
<OfflineModal />
<AchievementToast />
@@ -0,0 +1,115 @@
import type { PublicProfileResponse } from "@elysium/types";
import { useEffect, useState } from "react";
import { formatNumber } from "../../utils/format.js";
interface ProfilePageProps {
discordId: string;
}
export const ProfilePage = ({ discordId }: ProfilePageProps): React.JSX.Element => {
const [profile, setProfile] = useState<PublicProfileResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
useEffect(() => {
fetch(`/api/profile/${discordId}`)
.then(async (res) => {
if (!res.ok) throw new Error("Player not found");
return res.json() as Promise<PublicProfileResponse>;
})
.then(setProfile)
.catch((err: unknown) => {
setError(err instanceof Error ? err.message : "Failed to load profile");
});
}, [discordId]);
const handleCopy = (): void => {
void navigator.clipboard.writeText(window.location.href).then(() => {
setCopied(true);
setTimeout(() => { setCopied(false); }, 2000);
});
};
if (error) {
return (
<div className="profile-page">
<div className="profile-error">
<p> {error}</p>
<a className="profile-play-link" href="/"> Play Elysium</a>
</div>
</div>
);
}
if (!profile) {
return (
<div className="profile-page">
<div className="profile-loading">Loading profile</div>
</div>
);
}
const avatarUrl = profile.avatar
? `https://cdn.discordapp.com/avatars/${discordId}/${profile.avatar}.png?size=128`
: `https://cdn.discordapp.com/embed/avatars/${parseInt(discordId, 10) % 5}.png`;
const memberSince = new Date(profile.createdAt).toLocaleDateString("en-GB", {
year: "numeric",
month: "long",
day: "numeric",
});
return (
<div className="profile-page">
<div className="profile-card">
<div className="profile-header">
<img
alt={`${profile.username}'s avatar`}
className="profile-avatar"
src={avatarUrl}
/>
<div className="profile-identity">
<h1 className="profile-character-name">{profile.characterName}</h1>
<p className="profile-username">@{profile.username}</p>
{profile.prestigeCount > 0 && (
<span className="profile-prestige-badge">
Prestige {profile.prestigeCount}
</span>
)}
</div>
</div>
<div className="profile-stats">
<div className="profile-stat">
<span className="profile-stat-icon">🪙</span>
<span className="profile-stat-value">{formatNumber(profile.totalGoldEarned)}</span>
<span className="profile-stat-label">Total Gold Earned</span>
</div>
<div className="profile-stat">
<span className="profile-stat-icon">👆</span>
<span className="profile-stat-value">{formatNumber(profile.totalClicks)}</span>
<span className="profile-stat-label">Total Clicks</span>
</div>
<div className="profile-stat">
<span className="profile-stat-icon">📅</span>
<span className="profile-stat-value profile-stat-date">{memberSince}</span>
<span className="profile-stat-label">Guild Founded</span>
</div>
</div>
<div className="profile-actions">
<button
className="profile-share-button"
onClick={handleCopy}
type="button"
>
{copied ? "✓ Copied!" : "🔗 Copy Profile Link"}
</button>
<a className="profile-play-link" href="/">
Play Elysium
</a>
</div>
</div>
</div>
);
};