151 lines
3.8 KiB
TypeScript
151 lines
3.8 KiB
TypeScript
"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>
|
|
);
|
|
}
|