| 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> | |
| ); | |
| } |