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