diff --git a/apps/web/src/components/game/ProfilePage.tsx b/apps/web/src/components/game/ProfilePage.tsx
new file mode 100644
index 0000000..67cb820
--- /dev/null
+++ b/apps/web/src/components/game/ProfilePage.tsx
@@ -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
(null);
+ const [error, setError] = useState(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;
+ })
+ .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 (
+
+ );
+ }
+
+ if (!profile) {
+ return (
+
+ );
+ }
+
+ 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 (
+
+
+
+

+
+
{profile.characterName}
+
@{profile.username}
+ {profile.prestigeCount > 0 && (
+
+ ⭐ Prestige {profile.prestigeCount}
+
+ )}
+
+
+
+
+
+ 🪙
+ {formatNumber(profile.totalGoldEarned)}
+ Total Gold Earned
+
+
+ 👆
+ {formatNumber(profile.totalClicks)}
+ Total Clicks
+
+
+ 📅
+ {memberSince}
+ Guild Founded
+
+
+
+
+
+
+ );
+};
diff --git a/apps/web/src/components/ui/ResourceBar.tsx b/apps/web/src/components/ui/ResourceBar.tsx
index 93ea415..ebaf800 100644
--- a/apps/web/src/components/ui/ResourceBar.tsx
+++ b/apps/web/src/components/ui/ResourceBar.tsx
@@ -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 => (
🪙
@@ -33,5 +34,14 @@ export const ResourceBar = ({ resources, prestigeCount }: ResourceBarProps): Rea
⭐ Prestige {prestigeCount}
)}
+
+ 👤 Profile
+
);
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css
index 8f9cd4b..14698f1 100644
--- a/apps/web/src/styles.css
+++ b/apps/web/src/styles.css
@@ -1107,6 +1107,179 @@ body {
font-size: 0.85rem;
}
+/* ── Profile link button in ResourceBar ────────────────────────────────── */
+
+.profile-link-button {
+ align-items: center;
+ background: rgba(255, 255, 255, 0.06);
+ border: 1px solid rgba(147, 51, 234, 0.4);
+ border-radius: 1rem;
+ color: var(--colour-text-muted);
+ display: flex;
+ font-size: 0.8rem;
+ gap: 0.3rem;
+ margin-left: auto;
+ padding: 0.3rem 0.8rem;
+ text-decoration: none;
+ transition: all 0.2s;
+ white-space: nowrap;
+}
+
+.profile-link-button:hover {
+ background: rgba(147, 51, 234, 0.2);
+ border-color: var(--colour-primary);
+ color: var(--colour-text);
+}
+
+/* ── Public Profile Page ────────────────────────────────────────────────── */
+
+.profile-page {
+ align-items: center;
+ background: var(--colour-bg);
+ display: flex;
+ justify-content: center;
+ min-height: 100vh;
+ padding: 2rem 1rem;
+}
+
+.profile-card {
+ background: var(--colour-surface);
+ border: 1px solid rgba(147, 51, 234, 0.3);
+ border-radius: 1rem;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+ max-width: 520px;
+ padding: 2rem;
+ width: 100%;
+}
+
+.profile-header {
+ align-items: center;
+ display: flex;
+ gap: 1.5rem;
+ margin-bottom: 1.75rem;
+}
+
+.profile-avatar {
+ border: 3px solid var(--colour-primary);
+ border-radius: 50%;
+ height: 96px;
+ width: 96px;
+}
+
+.profile-identity {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.profile-character-name {
+ font-size: 1.6rem;
+ font-weight: 700;
+ margin: 0;
+}
+
+.profile-username {
+ color: var(--colour-text-muted);
+ font-size: 0.9rem;
+ margin: 0;
+}
+
+.profile-prestige-badge {
+ background: rgba(255, 215, 0, 0.15);
+ border: 1px solid rgba(255, 215, 0, 0.4);
+ border-radius: 1rem;
+ color: gold;
+ font-size: 0.8rem;
+ padding: 0.2rem 0.6rem;
+ width: fit-content;
+}
+
+.profile-stats {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(3, 1fr);
+ margin-bottom: 1.75rem;
+}
+
+.profile-stat {
+ align-items: center;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid rgba(147, 51, 234, 0.2);
+ border-radius: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ padding: 0.75rem 0.5rem;
+ text-align: center;
+}
+
+.profile-stat-icon {
+ font-size: 1.4rem;
+}
+
+.profile-stat-value {
+ font-size: 1rem;
+ font-weight: 700;
+}
+
+.profile-stat-date {
+ font-size: 0.72rem;
+}
+
+.profile-stat-label {
+ color: var(--colour-text-muted);
+ font-size: 0.7rem;
+}
+
+.profile-actions {
+ display: flex;
+ gap: 0.75rem;
+}
+
+.profile-share-button {
+ background: rgba(147, 51, 234, 0.2);
+ border: 1px solid var(--colour-primary);
+ border-radius: 0.5rem;
+ color: var(--colour-text);
+ cursor: pointer;
+ flex: 1;
+ font-family: inherit;
+ font-size: 0.9rem;
+ padding: 0.65rem 1rem;
+ transition: background 0.2s;
+}
+
+.profile-share-button:hover {
+ background: rgba(147, 51, 234, 0.35);
+}
+
+.profile-play-link {
+ background: var(--colour-primary);
+ border-radius: 0.5rem;
+ color: #fff;
+ flex: 1;
+ font-size: 0.9rem;
+ font-weight: 600;
+ padding: 0.65rem 1rem;
+ text-align: center;
+ text-decoration: none;
+ transition: opacity 0.2s;
+}
+
+.profile-play-link:hover {
+ opacity: 0.85;
+}
+
+.profile-loading,
+.profile-error {
+ color: var(--colour-text-muted);
+ display: flex;
+ flex-direction: column;
+ font-size: 1rem;
+ gap: 1rem;
+ text-align: center;
+}
+
/* ── Panel Header (title + lock toggle row) ────────────────────────────── */
.panel-header {