From ce3c88ec541e8b7013b26b5fab8cb0036e62242d Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Thu, 30 Apr 2026 20:48:28 +0800 Subject: [PATCH] feat(manage): add dashboard overview and charts components - DashboardOverview: stat cards, top contributors, playability distribution, and Steam sync health sections - DashboardCharts: placeholder for future ECharts integration --- components/manage/dashboard-charts.tsx | 7 + components/manage/dashboard-overview.tsx | 211 +++++++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 components/manage/dashboard-charts.tsx create mode 100644 components/manage/dashboard-overview.tsx diff --git a/components/manage/dashboard-charts.tsx b/components/manage/dashboard-charts.tsx new file mode 100644 index 0000000..998abea --- /dev/null +++ b/components/manage/dashboard-charts.tsx @@ -0,0 +1,7 @@ +"use client"; + +// Future: ECharts integration for trends over time +// For now, the overview stats are sufficient +export function DashboardCharts() { + return null; +} diff --git a/components/manage/dashboard-overview.tsx b/components/manage/dashboard-overview.tsx new file mode 100644 index 0000000..e1cc294 --- /dev/null +++ b/components/manage/dashboard-overview.tsx @@ -0,0 +1,211 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Gamepad2, + BarChart3, + Users, + AlertTriangle, + MessageSquarePlus, + TrendingUp, + Shield, + ShieldX, + ShieldCheck, +} from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface DashboardStats { + overview: { + totalGames: number; + totalBenchmarks: number; + totalUsers: number; + pendingReports: number; + pendingSuggestions: number; + }; + recent: { + benchmarksLast30Days: number; + gamesLast30Days: number; + }; + topContributors: Array<{ + userId: string; + name: string; + count: number; + }>; + syncHealth: Record; + gamesBySource: Record; + playabilityDistribution: Record; +} + +export function DashboardOverview() { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch("/api/dashboard/stats") + .then((res) => res.json()) + .then((data) => { + setStats(data); + setLoading(false); + }) + .catch(() => setLoading(false)); + }, []); + + if (loading) { + return
; + } + + if (!stats) return null; + + const statCards = [ + { + label: "Total Games", + value: stats.overview.totalGames, + icon: Gamepad2, + color: "text-blue-400", + }, + { + label: "Total Benchmarks", + value: stats.overview.totalBenchmarks, + icon: BarChart3, + color: "text-green-400", + }, + { + label: "Total Users", + value: stats.overview.totalUsers, + icon: Users, + color: "text-purple-400", + }, + { + label: "Pending Reports", + value: stats.overview.pendingReports, + icon: AlertTriangle, + color: "text-red-400", + highlight: stats.overview.pendingReports > 0, + }, + { + label: "Pending Suggestions", + value: stats.overview.pendingSuggestions, + icon: MessageSquarePlus, + color: "text-amber-400", + highlight: stats.overview.pendingSuggestions > 0, + }, + { + label: "Benchmarks (30d)", + value: stats.recent.benchmarksLast30Days, + icon: TrendingUp, + color: "text-emerald-400", + }, + { + label: "New Games (30d)", + value: stats.recent.gamesLast30Days, + icon: Gamepad2, + color: "text-cyan-400", + }, + ]; + + return ( +
+ {/* Stat Cards */} +
+ {statCards.map((card) => { + const Icon = card.icon; + return ( +
+
+ + {card.label} +
+

{card.value.toLocaleString()}

+
+ ); + })} +
+ + {/* Grid: Top Contributors + Playability Distribution */} +
+ {/* Top Contributors */} +
+

Top Contributors

+
+ {stats.topContributors.slice(0, 5).map((contributor, i) => ( +
+
+ #{i + 1} + {contributor.name} +
+ + {contributor.count} entries + +
+ ))} +
+
+ + {/* Playability Distribution */} +
+

Playability Distribution

+
+ {Object.entries(stats.playabilityDistribution).map(([status, count]) => { + const config: Record = { + great: { label: "Plays Great", color: "bg-emerald-500" }, + playable: { label: "Playable", color: "bg-blue-500" }, + needs_tweaks: { label: "Needs Tweaks", color: "bg-amber-500" }, + unplayable: { label: "Unplayable", color: "bg-red-500" }, + unknown: { label: "Unknown", color: "bg-zinc-500" }, + }; + const c = config[status] ?? config.unknown; + + return ( +
+ + {c.label} + {count} +
+ ); + })} +
+
+
+ + {/* Sync Health */} +
+

Steam Sync Health

+
+ {Object.entries(stats.syncHealth).map(([status, count]) => { + const icons: Record = { + synced: ShieldCheck, + failed: ShieldX, + pending: Shield, + }; + const colors: Record = { + synced: "text-green-400", + failed: "text-red-400", + pending: "text-yellow-400", + }; + const Icon = icons[status] ?? Shield; + const color = colors[status] ?? "text-zinc-400"; + + return ( +
+ +
+

{count}

+

{status}

+
+
+ ); + })} +
+
+
+ ); +}