A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.
1import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; 2import { 3 LineChart, 4 Line, 5 XAxis, 6 YAxis, 7 CartesianGrid, 8 Tooltip, 9 ResponsiveContainer, 10} from "recharts"; 11import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 12import { toast } from "@/hooks/use-toast" 13import { useState, useEffect } from "react"; 14 15import { getLinkClickStats, getLinkSourceStats } from '../api/client'; 16import { ClickStats, SourceStats } from '../types/api'; 17 18interface StatisticsModalProps { 19 isOpen: boolean; 20 onClose: () => void; 21 linkId: number; 22} 23 24export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProps) { 25 const [clicksOverTime, setClicksOverTime] = useState<ClickStats[]>([]); 26 const [sourcesData, setSourcesData] = useState<SourceStats[]>([]); 27 const [loading, setLoading] = useState(true); 28 29 useEffect(() => { 30 if (isOpen && linkId) { 31 const fetchData = async () => { 32 try { 33 setLoading(true); 34 const [clicksData, sourcesData] = await Promise.all([ 35 getLinkClickStats(linkId), 36 getLinkSourceStats(linkId), 37 ]); 38 setClicksOverTime(clicksData); 39 setSourcesData(sourcesData); 40 } catch (error: any) { 41 console.error("Failed to fetch statistics:", error); 42 toast({ 43 variant: "destructive", 44 title: "Error", 45 description: error.response?.data || "Failed to load statistics", 46 }); 47 } finally { 48 setLoading(false); 49 } 50 }; 51 52 fetchData(); 53 } 54 }, [isOpen, linkId]); 55 56 return ( 57 <Dialog open={isOpen} onOpenChange={onClose}> 58 <DialogContent className="max-w-3xl"> 59 <DialogHeader> 60 <DialogTitle>Link Statistics</DialogTitle> 61 </DialogHeader> 62 63 {loading ? ( 64 <div className="flex items-center justify-center h-64">Loading...</div> 65 ) : ( 66 <div className="grid gap-4"> 67 <Card> 68 <CardHeader> 69 <CardTitle>Clicks Over Time</CardTitle> 70 </CardHeader> 71 <CardContent> 72 <div className="h-[300px]"> 73 <ResponsiveContainer width="100%" height="100%"> 74 <LineChart data={clicksOverTime}> 75 <CartesianGrid strokeDasharray="3 3" /> 76 <XAxis dataKey="date" /> 77 <YAxis /> 78 <Tooltip /> 79 <Line 80 type="monotone" 81 dataKey="clicks" 82 stroke="#8884d8" 83 strokeWidth={2} 84 /> 85 </LineChart> 86 </ResponsiveContainer> 87 </div> 88 </CardContent> 89 </Card> 90 91 <Card> 92 <CardHeader> 93 <CardTitle>Top Sources</CardTitle> 94 </CardHeader> 95 <CardContent> 96 <ul className="space-y-2"> 97 {sourcesData.map((source, index) => ( 98 <li 99 key={source.source} 100 className="flex items-center justify-between py-2 border-b last:border-0" 101 > 102 <span className="text-sm"> 103 <span className="font-medium text-muted-foreground mr-2"> 104 {index + 1}. 105 </span> 106 {source.source} 107 </span> 108 <span className="text-sm font-medium"> 109 {source.count} clicks 110 </span> 111 </li> 112 ))} 113 </ul> 114 </CardContent> 115 </Card> 116 </div> 117 )} 118 </DialogContent> 119 </Dialog> 120 ); 121}