feat: integrate real data into dashboard

- Created dashboard service with DB queries for users, economy, events
- Added client stats provider with 30s caching for Discord metrics
- Implemented /api/stats endpoint aggregating all dashboard data
- Created useDashboardStats React hook with auto-refresh
- Updated Dashboard.tsx to display real data with loading/error states
- Added comprehensive test coverage (11 tests passing)
- Replaced all mock values with live Discord and database metrics
This commit is contained in:
syntaxbullet
2026-01-08 18:50:44 +01:00
parent a207d511be
commit 17cb70ec00
10 changed files with 861 additions and 35 deletions

View File

@@ -6,8 +6,49 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Activity, Server, Users, Zap } from "lucide-react";
import { useDashboardStats } from "@/hooks/use-dashboard-stats";
export function Dashboard() {
const { stats, loading, error } = useDashboardStats();
if (loading && !stats) {
return (
<div className="space-y-6">
<div>
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
<p className="text-muted-foreground">Loading dashboard data...</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{[1, 2, 3, 4].map((i) => (
<Card key={i}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Loading...</CardTitle>
</CardHeader>
<CardContent>
<div className="h-8 w-20 bg-muted animate-pulse rounded" />
</CardContent>
</Card>
))}
</div>
</div>
);
}
if (error) {
return (
<div className="space-y-6">
<div>
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
<p className="text-destructive">Error loading dashboard: {error}</p>
</div>
</div>
);
}
if (!stats) {
return null;
}
return (
<div className="space-y-6">
<div>
@@ -23,8 +64,8 @@ export function Dashboard() {
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">12</div>
<p className="text-xs text-muted-foreground">+2 from last month</p>
<div className="text-2xl font-bold">{stats.guilds.count}</div>
<p className="text-xs text-muted-foreground">Active guilds</p>
</CardContent>
</Card>
@@ -34,19 +75,21 @@ export function Dashboard() {
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">1,234</div>
<p className="text-xs text-muted-foreground">+10% from last month</p>
<div className="text-2xl font-bold">{stats.users.active.toLocaleString()}</div>
<p className="text-xs text-muted-foreground">
{stats.users.total.toLocaleString()} total registered
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Commands Run</CardTitle>
<CardTitle className="text-sm font-medium">Commands</CardTitle>
<Zap className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">12,345</div>
<p className="text-xs text-muted-foreground">+5% from last month</p>
<div className="text-2xl font-bold">{stats.commands.total}</div>
<p className="text-xs text-muted-foreground">Registered commands</p>
</CardContent>
</Card>
@@ -56,8 +99,8 @@ export function Dashboard() {
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">24ms</div>
<p className="text-xs text-muted-foreground">+2ms from last hour</p>
<div className="text-2xl font-bold">{stats.ping.avg}ms</div>
<p className="text-xs text-muted-foreground">WebSocket latency</p>
</CardContent>
</Card>
</div>
@@ -65,11 +108,25 @@ export function Dashboard() {
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Activity Overview</CardTitle>
<CardTitle>Economy Overview</CardTitle>
<CardDescription>Server economy statistics</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[200px] w-full bg-muted/20 flex items-center justify-center border-2 border-dashed border-muted rounded-md text-muted-foreground">
Chart Placeholder
<div className="space-y-4">
<div>
<p className="text-sm font-medium">Total Wealth</p>
<p className="text-2xl font-bold">{BigInt(stats.economy.totalWealth).toLocaleString()} AU</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm font-medium">Average Level</p>
<p className="text-xl font-bold">{stats.economy.avgLevel}</p>
</div>
<div>
<p className="text-sm font-medium">Top Streak</p>
<p className="text-xl font-bold">{stats.economy.topStreak} days</p>
</div>
</div>
</div>
</CardContent>
</Card>
@@ -81,27 +138,30 @@ export function Dashboard() {
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center">
<div className="w-2 h-2 rounded-full bg-emerald-500 mr-2" />
<div className="space-y-1">
<p className="text-sm font-medium leading-none">New guild joined</p>
<p className="text-sm text-muted-foreground">2 minutes ago</p>
</div>
</div>
<div className="flex items-center">
<div className="w-2 h-2 rounded-full bg-destructive mr-2" />
<div className="space-y-1">
<p className="text-sm font-medium leading-none">Error in verify command</p>
<p className="text-sm text-muted-foreground">15 minutes ago</p>
</div>
</div>
<div className="flex items-center">
<div className="w-2 h-2 rounded-full bg-blue-500 mr-2" />
<div className="space-y-1">
<p className="text-sm font-medium leading-none">Bot restarted</p>
<p className="text-sm text-muted-foreground">1 hour ago</p>
</div>
</div>
{stats.recentEvents.length === 0 ? (
<p className="text-sm text-muted-foreground">No recent events</p>
) : (
stats.recentEvents.slice(0, 5).map((event, i) => (
<div key={i} className="flex items-center">
<div
className={`w-2 h-2 rounded-full mr-2 ${event.type === 'success'
? 'bg-emerald-500'
: event.type === 'error'
? 'bg-destructive'
: 'bg-blue-500'
}`}
/>
<div className="space-y-1 flex-1">
<p className="text-sm font-medium leading-none">
{event.icon} {event.message}
</p>
<p className="text-sm text-muted-foreground">
{new Date(event.timestamp).toLocaleString()}
</p>
</div>
</div>
))
)}
</div>
</CardContent>
</Card>