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}