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 // Use import.meta.env.VITE_BASE_URL or fall back to window.location.origin 85 const baseUrl = import.meta.env.VITE_API_URL || window.location.origin 86 navigator.clipboard.writeText(`${baseUrl}/${shortCode}`) 87 toast({ 88 description: "Link copied to clipboard", 89 }) 90 } 91 92 if (loading && !links.length) { 93 return <div className="text-center py-4">Loading...</div> 94 } 95 96 return ( 97 <> 98 <Dialog open={deleteModal.isOpen} onOpenChange={(open) => setDeleteModal({ isOpen: open, linkId: null })}> 99 <DialogContent> 100 <DialogHeader> 101 <DialogTitle>Delete Link</DialogTitle> 102 <DialogDescription> 103 Are you sure you want to delete this link? This action cannot be undone. 104 </DialogDescription> 105 </DialogHeader> 106 <DialogFooter> 107 <Button variant="outline" onClick={() => setDeleteModal({ isOpen: false, linkId: null })}> 108 Cancel 109 </Button> 110 <Button variant="destructive" onClick={handleDelete}> 111 Delete 112 </Button> 113 </DialogFooter> 114 </DialogContent> 115 </Dialog> 116 117 <Card> 118 <CardHeader> 119 <CardTitle>Your Links</CardTitle> 120 <CardDescription>Manage and track your shortened links</CardDescription> 121 </CardHeader> 122 <CardContent> 123 <div className="rounded-md border"> 124 <Table> 125 <TableHeader> 126 <TableRow> 127 <TableHead>Short Code</TableHead> 128 <TableHead className="hidden md:table-cell">Original URL</TableHead> 129 <TableHead>Clicks</TableHead> 130 <TableHead className="hidden md:table-cell">Created</TableHead> 131 <TableHead>Actions</TableHead> 132 </TableRow> 133 </TableHeader> 134 <TableBody> 135 {links.map((link) => ( 136 <TableRow key={link.id}> 137 <TableCell className="font-medium">{link.short_code}</TableCell> 138 <TableCell className="hidden md:table-cell max-w-[300px] truncate"> 139 {link.original_url} 140 </TableCell> 141 <TableCell>{link.clicks}</TableCell> 142 <TableCell className="hidden md:table-cell"> 143 {new Date(link.created_at).toLocaleDateString()} 144 </TableCell> 145 <TableCell> 146 <div className="flex gap-2"> 147 <Button 148 variant="ghost" 149 size="icon" 150 className="h-8 w-8" 151 onClick={() => handleCopy(link.short_code)} 152 > 153 <Copy className="h-4 w-4" /> 154 <span className="sr-only">Copy link</span> 155 </Button> 156 <Button 157 variant="ghost" 158 size="icon" 159 className="h-8 w-8" 160 onClick={() => setStatsModal({ isOpen: true, linkId: link.id })} 161 > 162 <BarChart2 className="h-4 w-4" /> 163 <span className="sr-only">View statistics</span> 164 </Button> 165 <Button 166 variant="ghost" 167 size="icon" 168 className="h-8 w-8 text-destructive" 169 onClick={() => setDeleteModal({ isOpen: true, linkId: link.id })} 170 > 171 <Trash2 className="h-4 w-4" /> 172 <span className="sr-only">Delete link</span> 173 </Button> 174 </div> 175 </TableCell> 176 </TableRow> 177 ))} 178 </TableBody> 179 </Table> 180 </div> 181 </CardContent> 182 </Card> 183 <StatisticsModal 184 isOpen={statsModal.isOpen} 185 onClose={() => setStatsModal({ isOpen: false, linkId: null })} 186 linkId={statsModal.linkId!} 187 /> 188 </> 189 ) 190}