Files
AuroraBot-discord/web/src/components/activity-chart.tsx
2026-01-09 19:28:14 +01:00

165 lines
7.2 KiB
TypeScript

import React, { useEffect, useState } from "react";
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
import { Card, CardHeader, CardTitle, CardContent } from "./ui/card";
import { Activity } from "lucide-react";
import { cn } from "../lib/utils";
import type { ActivityData } from "@shared/modules/dashboard/dashboard.types";
interface ActivityChartProps {
className?: string;
data?: ActivityData[];
}
export function ActivityChart({ className, data: providedData }: ActivityChartProps) {
const [data, setData] = useState<any[]>([]); // using any[] for the displayTime extension
const [isLoading, setIsLoading] = useState(!providedData);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (providedData) {
// Process provided data
const formatted = providedData.map((item) => ({
...item,
displayTime: new Date(item.hour).getHours().toString().padStart(2, '0') + ':00',
}));
setData(formatted);
return;
}
let mounted = true;
async function fetchActivity() {
try {
const response = await fetch("/api/stats/activity");
if (!response.ok) throw new Error("Failed to fetch activity data");
const result = await response.json();
if (mounted) {
// Normalize data: ensure we have 24 hours format
// The API returns { hour: ISOString, commands: number, transactions: number }
// We want to format hour to readable time
const formatted = result.map((item: ActivityData) => ({
...item,
displayTime: new Date(item.hour).getHours().toString().padStart(2, '0') + ':00',
}));
// Sort by time just in case, though API should handle it
setData(formatted);
// Only set loading to false on the first load to avoid flickering
setIsLoading(false);
}
} catch (err) {
if (mounted) {
console.error(err);
setError("Failed to load activity data");
setIsLoading(false);
}
}
}
fetchActivity();
// Refresh every 60 seconds
const interval = setInterval(fetchActivity, 60000);
return () => {
mounted = false;
clearInterval(interval);
};
}, [providedData]);
if (error) {
return (
<Card className={cn("glass-card", className)}>
<CardContent className="flex items-center justify-center h-[300px] text-destructive">
{error}
</CardContent>
</Card>
);
}
return (
<Card className={cn("glass-card overflow-hidden", className)}>
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<Activity className="w-5 h-5 text-primary" />
<CardTitle>24h Activity</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="h-[250px] w-full">
{isLoading ? (
<div className="h-full w-full flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={data}
margin={{
top: 10,
right: 10,
left: 0,
bottom: 0,
}}
>
<defs>
<linearGradient id="colorCommands" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--primary)" stopOpacity={0.8} />
<stop offset="95%" stopColor="var(--primary)" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorTx" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--secondary)" stopOpacity={0.8} />
<stop offset="95%" stopColor="var(--secondary)" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--border)" />
<XAxis
dataKey="displayTime"
stroke="var(--muted-foreground)"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="var(--muted-foreground)"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value}`}
/>
<Tooltip
contentStyle={{
backgroundColor: "var(--card)",
borderColor: "var(--border)",
borderRadius: "calc(var(--radius) + 2px)",
color: "var(--foreground)"
}}
itemStyle={{ color: "var(--foreground)" }}
/>
<Area
type="monotone"
dataKey="commands"
name="Commands"
stroke="var(--primary)"
fillOpacity={1}
fill="url(#colorCommands)"
/>
<Area
type="monotone"
dataKey="transactions"
name="Transactions"
stroke="var(--secondary)"
fillOpacity={1}
fill="url(#colorTx)"
/>
</AreaChart>
</ResponsiveContainer>
)}
</div>
</CardContent>
</Card>
);
}