feat(panel): implement player dashboard with stats and inventory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
syntaxbullet
2026-04-02 13:31:09 +02:00
parent 3b53c9cb5f
commit f4b36a745e

View File

@@ -1,3 +1,138 @@
export default function PlayerDashboard({ userId }: { userId: string }) { import { useState, useEffect } from "react";
return <div className="text-text-tertiary">Player Dashboard loading...</div>; import { get } from "../lib/api";
import { Loader2 } from "lucide-react";
interface UserData {
id: string;
username: string;
level: number;
xp: string;
balance: string;
className: string | null;
}
interface InventoryItem {
itemId: string;
name: string;
quantity: number;
rarity: string;
}
export default function PlayerDashboard({ userId }: { userId: string }) {
const [user, setUser] = useState<UserData | null>(null);
const [inventory, setInventory] = useState<InventoryItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function load() {
try {
const [userData, invData] = await Promise.all([
get<UserData>(`/api/users/${userId}`),
get<{ items: InventoryItem[] }>(`/api/users/${userId}/inventory`).catch(() => ({ items: [] })),
]);
setUser(userData);
setInventory(invData.items ?? []);
} catch (e) {
setError("Failed to load profile");
} finally {
setLoading(false);
}
}
load();
}, [userId]);
if (loading) {
return (
<div className="flex items-center justify-center py-16">
<Loader2 className="w-6 h-6 animate-spin text-text-tertiary" />
</div>
);
}
if (error || !user) {
return (
<div className="text-center py-16 text-sm text-text-tertiary">
{error ?? "Could not load your profile."}
</div>
);
}
return (
<div>
<h1 className="font-display text-lg font-semibold mb-6">Dashboard</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<StatCard label="Level" value={String(user.level)} accent="primary" subtitle={user.className ?? undefined} />
<StatCard label="Gold" value={formatNumber(user.balance)} accent="gold" />
<StatCard label="XP" value={formatNumber(user.xp)} accent="info" />
<StatCard label="Items" value={String(inventory.length)} accent="success" />
</div>
<div className="bg-card rounded-lg border border-border">
<div className="flex items-center gap-2 px-5 py-3 border-b border-border">
<span className="text-sm font-semibold">Inventory</span>
<span className="text-xs text-text-disabled">({inventory.length})</span>
</div>
{inventory.length === 0 ? (
<div className="px-5 py-6 text-center text-sm text-text-tertiary">No items yet</div>
) : (
<div className="divide-y divide-border">
{inventory.slice(0, 10).map((item, i) => (
<div key={i} className="flex items-center justify-between px-5 py-3 hover:bg-raised/40 transition-colors">
<div className="text-sm font-medium">{item.name}</div>
<div className="flex items-center gap-2">
<span className={`text-[10px] font-semibold px-1.5 py-0.5 rounded ${rarityColor(item.rarity)}`}>
{item.rarity}
</span>
{item.quantity > 1 && (
<span className="text-xs text-text-tertiary font-mono">x{item.quantity}</span>
)}
</div>
</div>
))}
{inventory.length > 10 && (
<div className="px-5 py-2 text-xs text-text-disabled text-center">
and {inventory.length - 10} more
</div>
)}
</div>
)}
</div>
</div>
);
}
function StatCard({ label, value, accent, subtitle }: { label: string; value: string; accent: string; subtitle?: string }) {
const borderColor: Record<string, string> = {
primary: "border-l-primary",
gold: "border-l-gold",
info: "border-l-info",
success: "border-l-success",
};
return (
<div className={`bg-gradient-to-br from-card to-surface border border-border rounded-lg p-5 border-l-4 ${borderColor[accent] ?? ""}`}>
<div className="text-[11px] font-semibold uppercase tracking-wider text-text-tertiary">{label}</div>
<div className="text-2xl font-bold font-display tracking-tight mt-1">{value}</div>
{subtitle && <div className="text-sm text-text-tertiary mt-0.5">{subtitle}</div>}
</div>
);
}
function formatNumber(val: string): string {
const n = Number(val);
if (isNaN(n)) return val;
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
return n.toLocaleString();
}
function rarityColor(rarity: string): string {
switch (rarity?.toUpperCase()) {
case "C": return "bg-gray-500/20 text-gray-400";
case "R": return "bg-blue-500/20 text-blue-400";
case "SR": return "bg-purple-500/20 text-purple-400";
case "SSR": return "bg-amber-500/20 text-amber-400";
default: return "bg-gray-500/20 text-gray-400";
}
} }