A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.
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}