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 24interface EnhancedClickStats extends ClickStats { 25 sources?: { source: string; count: number }[]; 26} 27 28const CustomTooltip = ({ 29 active, 30 payload, 31 label, 32}: { 33 active?: boolean; 34 payload?: any[]; 35 label?: string; 36}) => { 37 if (active && payload && payload.length > 0) { 38 const data = payload[0].payload; 39 return ( 40 <div className="bg-background text-foreground p-4 rounded-lg shadow-lg border"> 41 <p className="font-medium">{label}</p> 42 <p className="text-sm">Clicks: {data.clicks}</p> 43 {data.sources && data.sources.length > 0 && ( 44 <div className="mt-2"> 45 <p className="font-medium text-sm">Sources:</p> 46 <ul className="text-sm"> 47 {data.sources.map((source: { source: string; count: number }) => ( 48 <li key={source.source}> 49 {source.source}: {source.count} 50 </li> 51 ))} 52 </ul> 53 </div> 54 )} 55 </div> 56 ); 57 } 58 return null; 59}; 60 61export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProps) { 62 const [clicksOverTime, setClicksOverTime] = useState<EnhancedClickStats[]>([]); 63 const [sourcesData, setSourcesData] = useState<SourceStats[]>([]); 64 const [loading, setLoading] = useState(true); 65 66 useEffect(() => { 67 if (isOpen && linkId) { 68 const fetchData = async () => { 69 try { 70 setLoading(true); 71 const [clicksData, sourcesData] = await Promise.all([ 72 getLinkClickStats(linkId), 73 getLinkSourceStats(linkId), 74 ]); 75 76 // Enhance clicks data with source information 77 const enhancedClicksData = clicksData.map((clickData) => ({ 78 ...clickData, 79 sources: sourcesData.filter((source) => source.date === clickData.date), 80 })); 81 82 setClicksOverTime(enhancedClicksData); 83 setSourcesData(sourcesData); 84 } catch (error: any) { 85 console.error("Failed to fetch statistics:", error); 86 toast({ 87 variant: "destructive", 88 title: "Error", 89 description: error.response?.data || "Failed to load statistics", 90 }); 91 } finally { 92 setLoading(false); 93 } 94 }; 95 96 fetchData(); 97 } 98 }, [isOpen, linkId]); 99 100 return ( 101 <Dialog open={isOpen} onOpenChange={onClose}> 102 <DialogContent className="max-w-3xl"> 103 <DialogHeader> 104 <DialogTitle>Link Statistics</DialogTitle> 105 </DialogHeader> 106 107 {loading ? ( 108 <div className="flex items-center justify-center h-64">Loading...</div> 109 ) : ( 110 <div className="grid gap-4"> 111 <Card> 112 <CardHeader> 113 <CardTitle>Clicks Over Time</CardTitle> 114 </CardHeader> 115 <CardContent> 116 <div className="h-[300px]"> 117 <ResponsiveContainer width="100%" height="100%"> 118 <LineChart data={clicksOverTime}> 119 <CartesianGrid strokeDasharray="3 3" /> 120 <XAxis dataKey="date" /> 121 <YAxis /> 122 <Tooltip content={<CustomTooltip />} /> 123 <Line 124 type="monotone" 125 dataKey="clicks" 126 stroke="#8884d8" 127 strokeWidth={2} 128 /> 129 </LineChart> 130 </ResponsiveContainer> 131 </div> 132 </CardContent> 133 </Card> 134 135 <Card> 136 <CardHeader> 137 <CardTitle>Top Sources</CardTitle> 138 </CardHeader> 139 <CardContent> 140 <ul className="space-y-2"> 141 {sourcesData.map((source, index) => ( 142 <li 143 key={source.source} 144 className="flex items-center justify-between py-2 border-b last:border-0" 145 > 146 <span className="text-sm"> 147 <span className="font-medium text-muted-foreground mr-2"> 148 {index + 1}. 149 </span> 150 {source.source} 151 </span> 152 <span className="text-sm font-medium"> 153 {source.count} clicks 154 </span> 155 </li> 156 ))} 157 </ul> 158 </CardContent> 159 </Card> 160 </div> 161 )} 162 </DialogContent> 163 </Dialog> 164 ); 165}