generated from nhcarrigan/template
feat: add public player profile page
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -4,9 +4,10 @@ import { formatNumber } from "../../utils/format.js";
|
||||
interface ResourceBarProps {
|
||||
resources: Resource;
|
||||
prestigeCount: number;
|
||||
profileUrl: string;
|
||||
}
|
||||
|
||||
export const ResourceBar = ({ resources, prestigeCount }: ResourceBarProps): React.JSX.Element => (
|
||||
export const ResourceBar = ({ resources, prestigeCount, profileUrl }: ResourceBarProps): React.JSX.Element => (
|
||||
<header className="resource-bar">
|
||||
<div className="resource">
|
||||
<span className="resource-icon">🪙</span>
|
||||
@@ -33,5 +34,14 @@ export const ResourceBar = ({ resources, prestigeCount }: ResourceBarProps): Rea
|
||||
⭐ Prestige {prestigeCount}
|
||||
</div>
|
||||
)}
|
||||
<a
|
||||
className="profile-link-button"
|
||||
href={profileUrl}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
title="View your public profile"
|
||||
>
|
||||
👤 Profile
|
||||
</a>
|
||||
</header>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user