Files
openclaw-market/components/dashboard/stats-panel.tsx

154 lines
3.9 KiB
TypeScript

"use client";
import { useEffect, useState, useRef } from "react";
import { useTranslations, useLocale } from "next-intl";
import { Card, CardContent } from "@/components/ui/card";
import { Users, Zap, Clock, Activity } from "lucide-react";
interface Stats {
totalClaws: number;
activeClaws: 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 locale = useLocale();
const formatted = Number.isInteger(value)
? Math.round(display).toLocaleString(locale)
: display.toFixed(1);
return (
<span>
{formatted}
{suffix}
</span>
);
}
const statCards = [
{
key: "activeClaws" as const,
labelKey: "onlineNow" as const,
icon: Activity,
color: "var(--accent-green)",
glow: "0 0 20px rgba(16, 185, 129, 0.3)",
},
{
key: "totalClaws" as const,
labelKey: "totalClaws" as const,
icon: Users,
color: "var(--accent-cyan)",
glow: "var(--glow-cyan)",
},
{
key: "tasksToday" as const,
labelKey: "tasksToday" as const,
icon: Zap,
color: "var(--accent-purple)",
glow: "var(--glow-purple)",
},
{
key: "avgTaskDuration" as const,
labelKey: "avgDuration" as const,
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 t = useTranslations("stats");
const [stats, setStats] = useState<Stats>({
totalClaws: 0,
activeClaws: 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, labelKey, 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)]">{t(labelKey)}</p>
<p
className="font-mono text-xl font-bold"
style={{ color, textShadow: glow }}
>
<AnimatedNumber value={value} suffix={suffix} />
</p>
</div>
</CardContent>
</Card>
);
})}
</div>
);
}