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}