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}