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}