yuki-api / src /components /VisitorChart.tsx
OhMyDitzzy
Set visitor to 7D at first
e84b48e
import { useEffect, useState } from "react";
import { Card } from "@/components/ui/card";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
import { Users, Loader2 } from "lucide-react";
interface VisitorData {
timestamp: number;
count: number;
}
export function VisitorChart() {
const [data, setData] = useState<VisitorData[]>([]);
const [loading, setLoading] = useState(true);
const [days, setDays] = useState(7);
useEffect(() => {
fetchVisitorData();
const interval = setInterval(fetchVisitorData, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [days]);
const fetchVisitorData = async () => {
try {
const res = await fetch(`/api/stats/visitors?days=${days}`);
const json = await res.json();
if (json.success) {
setData(json.data);
}
} catch (error) {
console.error("Failed to fetch visitor data:", error);
} finally {
setLoading(false);
}
};
const formatXAxis = (timestamp: number) => {
const date = new Date(timestamp);
if (days <= 7) {
return date.toLocaleDateString('id-ID', { month: 'short', day: 'numeric' });
} else if (days <= 30) {
return date.toLocaleDateString('id-ID', { month: 'short', day: 'numeric' });
} else {
return date.toLocaleDateString('id-ID', { month: 'short', day: 'numeric' });
}
};
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
const date = new Date(data.timestamp);
const dateStr = date.toLocaleDateString('id-ID', {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric',
});
return (
<div className="bg-black/90 border border-white/20 rounded-lg p-3 backdrop-blur-sm">
<p className="text-xs text-gray-400 mb-1">{dateStr}</p>
<p className="text-sm font-semibold text-purple-400">
{data.count} visitor{data.count !== 1 ? 's' : ''}
</p>
</div>
);
}
return null;
};
const totalVisitors = data.reduce((sum, d) => sum + d.count, 0);
return (
<Card className="p-6 bg-white/[0.02] border-white/10">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Users className="w-5 h-5 text-purple-400" />
<h3 className="text-lg font-semibold text-white">Visitor Activity</h3>
</div>
<div className="flex gap-2">
<button
onClick={() => setDays(7)}
className={`px-3 py-1 rounded text-sm transition-colors ${
days === 7
? 'bg-purple-500 text-white'
: 'bg-white/5 text-gray-400 hover:bg-white/10'
}`}
>
7D
</button>
<button
onClick={() => setDays(30)}
className={`px-3 py-1 rounded text-sm transition-colors ${
days === 30
? 'bg-purple-500 text-white'
: 'bg-white/5 text-gray-400 hover:bg-white/10'
}`}
>
30D
</button>
<button
onClick={() => setDays(90)}
className={`px-3 py-1 rounded text-sm transition-colors ${
days === 90
? 'bg-purple-500 text-white'
: 'bg-white/5 text-gray-400 hover:bg-white/10'
}`}
>
90D
</button>
</div>
</div>
<div className="mb-4">
<p className="text-2xl font-bold text-white">{totalVisitors}</p>
<p className="text-sm text-gray-400">Total visitors in last {days} days</p>
</div>
{loading ? (
<div className="h-64 flex items-center justify-center">
<Loader2 className="w-6 h-6 text-purple-400 animate-spin" />
</div>
) : (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data} margin={{ top: 5, right: 30, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis
dataKey="timestamp"
tickFormatter={formatXAxis}
stroke="rgba(255,255,255,0.5)"
style={{ fontSize: '12px' }}
interval="preserveStartEnd"
/>
<YAxis
stroke="rgba(255,255,255,0.5)"
style={{ fontSize: '12px' }}
allowDecimals={false}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="count"
stroke="#a855f7"
strokeWidth={2}
dot={{ fill: '#a855f7', r: 3 }}
activeDot={{ r: 5 }}
/>
</LineChart>
</ResponsiveContainer>
)}
</Card>
);
}