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}