A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.
1import { useEffect, useState } from 'react'
2import { Link } from '../types/api'
3import { getAllLinks, deleteLink } from '../api/client'
4import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
5import {
6 Table,
7 TableBody,
8 TableCell,
9 TableHead,
10 TableHeader,
11 TableRow,
12} from "@/components/ui/table"
13import { Button } from "@/components/ui/button"
14import { useToast } from "@/hooks/use-toast"
15import { Copy, Trash2, BarChart2 } from "lucide-react"
16import {
17 Dialog,
18 DialogContent,
19 DialogHeader,
20 DialogTitle,
21 DialogDescription,
22 DialogFooter,
23} from "@/components/ui/dialog"
24
25import { StatisticsModal } from "./StatisticsModal"
26
27interface LinkListProps {
28 refresh?: number;
29}
30
31export function LinkList({ refresh = 0 }: LinkListProps) {
32 const [links, setLinks] = useState<Link[]>([])
33 const [loading, setLoading] = useState(true)
34 const [deleteModal, setDeleteModal] = useState<{ isOpen: boolean; linkId: number | null }>({
35 isOpen: false,
36 linkId: null,
37 })
38 const [statsModal, setStatsModal] = useState<{ isOpen: boolean; linkId: number | null }>({
39 isOpen: false,
40 linkId: null,
41 });
42 const { toast } = useToast()
43
44 const fetchLinks = async () => {
45 try {
46 setLoading(true)
47 const data = await getAllLinks()
48 setLinks(data)
49 } catch (err) {
50 toast({
51 title: "Error",
52 description: "Failed to load links",
53 variant: "destructive",
54 })
55 } finally {
56 setLoading(false)
57 }
58 }
59
60 useEffect(() => {
61 fetchLinks()
62 }, [refresh]) // Re-fetch when refresh counter changes
63
64 const handleDelete = async () => {
65 if (!deleteModal.linkId) return
66
67 try {
68 await deleteLink(deleteModal.linkId)
69 await fetchLinks()
70 setDeleteModal({ isOpen: false, linkId: null })
71 toast({
72 description: "Link deleted successfully",
73 })
74 } catch (err) {
75 toast({
76 title: "Error",
77 description: "Failed to delete link",
78 variant: "destructive",
79 })
80 }
81 }
82
83 const handleCopy = (shortCode: string) => {
84 navigator.clipboard.writeText(`http://localhost:8080/${shortCode}`)
85 toast({
86 description: "Link copied to clipboard",
87 })
88 }
89
90 if (loading && !links.length) {
91 return <div className="text-center py-4">Loading...</div>
92 }
93
94 return (
95 <>
96 <Dialog open={deleteModal.isOpen} onOpenChange={(open) => setDeleteModal({ isOpen: open, linkId: null })}>
97 <DialogContent>
98 <DialogHeader>
99 <DialogTitle>Delete Link</DialogTitle>
100 <DialogDescription>
101 Are you sure you want to delete this link? This action cannot be undone.
102 </DialogDescription>
103 </DialogHeader>
104 <DialogFooter>
105 <Button variant="outline" onClick={() => setDeleteModal({ isOpen: false, linkId: null })}>
106 Cancel
107 </Button>
108 <Button variant="destructive" onClick={handleDelete}>
109 Delete
110 </Button>
111 </DialogFooter>
112 </DialogContent>
113 </Dialog>
114
115 <Card>
116 <CardHeader>
117 <CardTitle>Your Links</CardTitle>
118 <CardDescription>Manage and track your shortened links</CardDescription>
119 </CardHeader>
120 <CardContent>
121 <div className="rounded-md border">
122 <Table>
123 <TableHeader>
124 <TableRow>
125 <TableHead>Short Code</TableHead>
126 <TableHead className="hidden md:table-cell">Original URL</TableHead>
127 <TableHead>Clicks</TableHead>
128 <TableHead className="hidden md:table-cell">Created</TableHead>
129 <TableHead>Actions</TableHead>
130 </TableRow>
131 </TableHeader>
132 <TableBody>
133 {links.map((link) => (
134 <TableRow key={link.id}>
135 <TableCell className="font-medium">{link.short_code}</TableCell>
136 <TableCell className="hidden md:table-cell max-w-[300px] truncate">
137 {link.original_url}
138 </TableCell>
139 <TableCell>{link.clicks}</TableCell>
140 <TableCell className="hidden md:table-cell">
141 {new Date(link.created_at).toLocaleDateString()}
142 </TableCell>
143 <TableCell>
144 <div className="flex gap-2">
145 <Button
146 variant="ghost"
147 size="icon"
148 className="h-8 w-8"
149 onClick={() => handleCopy(link.short_code)}
150 >
151 <Copy className="h-4 w-4" />
152 <span className="sr-only">Copy link</span>
153 </Button>
154 <Button
155 variant="ghost"
156 size="icon"
157 className="h-8 w-8"
158 onClick={() => setStatsModal({ isOpen: true, linkId: link.id })}
159 >
160 <BarChart2 className="h-4 w-4" />
161 <span className="sr-only">View statistics</span>
162 </Button>
163 <Button
164 variant="ghost"
165 size="icon"
166 className="h-8 w-8 text-destructive"
167 onClick={() => setDeleteModal({ isOpen: true, linkId: link.id })}
168 >
169 <Trash2 className="h-4 w-4" />
170 <span className="sr-only">Delete link</span>
171 </Button>
172 </div>
173 </TableCell>
174 </TableRow>
175 ))}
176 </TableBody>
177 </Table>
178 </div>
179 </CardContent>
180 </Card>
181 <StatisticsModal
182 isOpen={statsModal.isOpen}
183 onClose={() => setStatsModal({ isOpen: false, linkId: null })}
184 linkId={statsModal.linkId!}
185 />
186 </>
187 )
188}