A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.
at master 6.6 kB view raw
1import { useCallback, 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, Pencil } 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" 26import { EditModal } from './EditModal' 27 28interface LinkListProps { 29 refresh?: number; 30} 31 32export function LinkList({ refresh = 0 }: LinkListProps) { 33 const [links, setLinks] = useState<Link[]>([]) 34 const [loading, setLoading] = useState(true) 35 const [deleteModal, setDeleteModal] = useState<{ isOpen: boolean; linkId: number | null }>({ 36 isOpen: false, 37 linkId: null, 38 }) 39 const [statsModal, setStatsModal] = useState<{ isOpen: boolean; linkId: number | null }>({ 40 isOpen: false, 41 linkId: null, 42 }); 43 const [editModal, setEditModal] = useState<{ isOpen: boolean; link: Link | null }>({ 44 isOpen: false, 45 link: null, 46 }); 47 const { toast } = useToast() 48 49 const fetchLinks = useCallback(async () => { 50 try { 51 setLoading(true) 52 const data = await getAllLinks() 53 setLinks(data) 54 } catch (err: unknown) { 55 const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; 56 toast({ 57 title: "Error", 58 description: `Failed to load links: ${errorMessage}`, 59 variant: "destructive", 60 }) 61 } finally { 62 setLoading(false) 63 } 64 }, [toast, setLinks, setLoading]) 65 66 useEffect(() => { 67 fetchLinks() 68 }, [fetchLinks, refresh]) // Re-fetch when refresh counter changes 69 70 const handleDelete = async () => { 71 if (!deleteModal.linkId) return 72 73 try { 74 await deleteLink(deleteModal.linkId) 75 await fetchLinks() 76 setDeleteModal({ isOpen: false, linkId: null }) 77 toast({ 78 description: "Link deleted successfully", 79 }) 80 } catch (err: unknown) { 81 const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; 82 toast({ 83 title: "Error", 84 description: `Failed to delete link: ${errorMessage}`, 85 variant: "destructive", 86 }) 87 } 88 } 89 90 const handleCopy = (shortCode: string) => { 91 // Use import.meta.env.VITE_BASE_URL or fall back to window.location.origin 92 const baseUrl = window.location.origin 93 navigator.clipboard.writeText(`${baseUrl}/${shortCode}`) 94 toast({ 95 description: ( 96 <> 97 Link copied to clipboard 98 <br /> 99 You can add ?source=TextHere to the end of the link to track the source of clicks 100 </> 101 ), 102 }) 103 } 104 105 if (loading && !links.length) { 106 return <div className="text-center py-4">Loading...</div> 107 } 108 109 return ( 110 <> 111 <Dialog open={deleteModal.isOpen} onOpenChange={(open) => setDeleteModal({ isOpen: open, linkId: null })}> 112 <DialogContent> 113 <DialogHeader> 114 <DialogTitle>Delete Link</DialogTitle> 115 <DialogDescription> 116 Are you sure you want to delete this link? This action cannot be undone. 117 </DialogDescription> 118 </DialogHeader> 119 <DialogFooter> 120 <Button variant="outline" onClick={() => setDeleteModal({ isOpen: false, linkId: null })}> 121 Cancel 122 </Button> 123 <Button variant="destructive" onClick={handleDelete}> 124 Delete 125 </Button> 126 </DialogFooter> 127 </DialogContent> 128 </Dialog> 129 130 <Card> 131 <CardHeader> 132 <CardTitle>Your Links</CardTitle> 133 <CardDescription>Manage and track your shortened links</CardDescription> 134 </CardHeader> 135 <CardContent> 136 <div className="rounded-md border"> 137 138 <Table> 139 <TableHeader> 140 <TableRow> 141 <TableHead>Short Code</TableHead> 142 <TableHead className="hidden md:table-cell">Original URL</TableHead> 143 <TableHead>Clicks</TableHead> 144 <TableHead className="hidden md:table-cell">Created</TableHead> 145 <TableHead className="w-[1%] whitespace-nowrap pr-4">Actions</TableHead> 146 </TableRow> 147 </TableHeader> 148 <TableBody> 149 {links.map((link) => ( 150 <TableRow key={link.id}> 151 <TableCell className="font-medium">{link.short_code}</TableCell> 152 <TableCell className="hidden md:table-cell max-w-[300px] truncate"> 153 {link.original_url} 154 </TableCell> 155 <TableCell>{link.clicks}</TableCell> 156 <TableCell className="hidden md:table-cell"> 157 {new Date(link.created_at).toLocaleDateString()} 158 </TableCell> 159 <TableCell className="p-2 pr-4"> 160 <div className="flex items-center gap-1"> 161 <Button 162 variant="ghost" 163 size="icon" 164 className="h-8 w-8" 165 onClick={() => handleCopy(link.short_code)} 166 > 167 <Copy className="h-4 w-4" /> 168 <span className="sr-only">Copy link</span> 169 </Button> 170 <Button 171 variant="ghost" 172 size="icon" 173 className="h-8 w-8" 174 onClick={() => setStatsModal({ isOpen: true, linkId: link.id })} 175 > 176 <BarChart2 className="h-4 w-4" /> 177 <span className="sr-only">View statistics</span> 178 </Button> 179 <Button 180 variant="ghost" 181 size="icon" 182 className="h-8 w-8" 183 onClick={() => setEditModal({ isOpen: true, link })} 184 > 185 <Pencil className="h-4 w-4" /> 186 <span className="sr-only">Edit Link</span> 187 </Button> 188 <Button 189 variant="ghost" 190 size="icon" 191 className="h-8 w-8 text-destructive" 192 onClick={() => setDeleteModal({ isOpen: true, linkId: link.id })} 193 > 194 <Trash2 className="h-4 w-4" /> 195 <span className="sr-only">Delete link</span> 196 </Button> 197 </div> 198 </TableCell> 199 </TableRow> 200 ))} 201 </TableBody> 202 </Table> 203 </div> 204 </CardContent> 205 </Card> 206 <StatisticsModal 207 isOpen={statsModal.isOpen} 208 onClose={() => setStatsModal({ isOpen: false, linkId: null })} 209 linkId={statsModal.linkId!} 210 /> 211 {editModal.link && ( 212 <EditModal 213 isOpen={editModal.isOpen} 214 onClose={() => setEditModal({ isOpen: false, link: null })} 215 link={editModal.link} 216 onSuccess={fetchLinks} 217 /> 218 )} 219 </> 220 ) 221}