init
This commit is contained in:
150
components/dashboard/stats-panel.tsx
Normal file
150
components/dashboard/stats-panel.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Users, Zap, Clock, Activity } from "lucide-react";
|
||||
|
||||
interface Stats {
|
||||
totalLobsters: number;
|
||||
activeLobsters: number;
|
||||
tasksToday: number;
|
||||
tasksTotal: number;
|
||||
avgTaskDuration: number;
|
||||
}
|
||||
|
||||
function AnimatedNumber({ value, suffix = "" }: { value: number; suffix?: string }) {
|
||||
const [display, setDisplay] = useState(0);
|
||||
const prevRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const start = prevRef.current;
|
||||
const diff = value - start;
|
||||
if (diff === 0) return;
|
||||
|
||||
const duration = 800;
|
||||
const startTime = performance.now();
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
const current = start + diff * eased;
|
||||
|
||||
setDisplay(current);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
prevRef.current = value;
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}, [value]);
|
||||
|
||||
const formatted = Number.isInteger(value)
|
||||
? Math.round(display).toLocaleString()
|
||||
: display.toFixed(1);
|
||||
|
||||
return (
|
||||
<span>
|
||||
{formatted}
|
||||
{suffix}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const statCards = [
|
||||
{
|
||||
key: "activeLobsters" as const,
|
||||
label: "Online Now",
|
||||
icon: Activity,
|
||||
color: "var(--accent-green)",
|
||||
glow: "0 0 20px rgba(16, 185, 129, 0.3)",
|
||||
},
|
||||
{
|
||||
key: "totalLobsters" as const,
|
||||
label: "Total Lobsters",
|
||||
icon: Users,
|
||||
color: "var(--accent-cyan)",
|
||||
glow: "var(--glow-cyan)",
|
||||
},
|
||||
{
|
||||
key: "tasksToday" as const,
|
||||
label: "Tasks Today",
|
||||
icon: Zap,
|
||||
color: "var(--accent-purple)",
|
||||
glow: "var(--glow-purple)",
|
||||
},
|
||||
{
|
||||
key: "avgTaskDuration" as const,
|
||||
label: "Avg Duration",
|
||||
icon: Clock,
|
||||
color: "var(--accent-orange)",
|
||||
glow: "0 0 20px rgba(245, 158, 11, 0.3)",
|
||||
suffix: "s",
|
||||
transform: (v: number) => v / 1000,
|
||||
},
|
||||
];
|
||||
|
||||
export function StatsPanel() {
|
||||
const [stats, setStats] = useState<Stats>({
|
||||
totalLobsters: 0,
|
||||
activeLobsters: 0,
|
||||
tasksToday: 0,
|
||||
tasksTotal: 0,
|
||||
avgTaskDuration: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/v1/stats");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setStats(data);
|
||||
}
|
||||
} catch {
|
||||
// will retry on next interval
|
||||
}
|
||||
};
|
||||
|
||||
fetchStats();
|
||||
const interval = setInterval(fetchStats, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{statCards.map(({ key, label, icon: Icon, color, glow, suffix, transform }) => {
|
||||
const raw = stats[key];
|
||||
const value = transform ? transform(raw) : raw;
|
||||
return (
|
||||
<Card
|
||||
key={key}
|
||||
className="overflow-hidden border-white/5 transition-all hover:border-white/10"
|
||||
style={{ boxShadow: glow }}
|
||||
>
|
||||
<CardContent className="flex items-center gap-3 p-4">
|
||||
<div
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg"
|
||||
style={{ backgroundColor: `${color}15` }}
|
||||
>
|
||||
<Icon className="h-5 w-5" style={{ color }} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs text-[var(--text-muted)]">{label}</p>
|
||||
<p
|
||||
className="font-mono text-xl font-bold"
|
||||
style={{ color, textShadow: glow }}
|
||||
>
|
||||
<AnimatedNumber value={value} suffix={suffix} />
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user