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:
@@ -1,3 +1,138 @@
|
||||
export default function PlayerDashboard({ userId }: { userId: string }) {
|
||||
return <div className="text-text-tertiary">Player Dashboard — loading...</div>;
|
||||
import { useState, useEffect } from "react";
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user