A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.
1import { useState } from 'react'
2import { useForm } from 'react-hook-form'
3import { zodResolver } from '@hookform/resolvers/zod'
4import * as z from 'zod'
5import { CreateLinkRequest } from '../types/api'
6import { createShortLink } from '../api/client'
7import { Button } from "@/components/ui/button"
8import { Input } from "@/components/ui/input"
9import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
10import { LinkIcon } from "lucide-react"
11import {
12 Form,
13 FormControl,
14 FormField,
15 FormLabel,
16 FormMessage,
17} from "@/components/ui/form"
18import { useToast } from "@/hooks/use-toast"
19
20const formSchema = z.object({
21 url: z.string()
22 .min(1, 'URL is required')
23 .url('Must be a valid URL')
24 .refine(val => val.startsWith('http://') || val.startsWith('https://'), {
25 message: 'URL must start with http:// or https://'
26 }),
27 custom_code: z.string()
28 .regex(/^[a-zA-Z0-9_-]{0,32}$/, 'Custom code must contain only letters, numbers, underscores, and hyphens')
29 .optional()
30})
31
32interface LinkFormProps {
33 onSuccess: () => void;
34}
35
36export function LinkForm({ onSuccess }: LinkFormProps) {
37 const [loading, setLoading] = useState(false)
38 const { toast } = useToast()
39
40 const form = useForm<z.infer<typeof formSchema>>({
41 resolver: zodResolver(formSchema),
42 defaultValues: {
43 url: '',
44 custom_code: '',
45 },
46 })
47
48 const onSubmit = async (values: z.infer<typeof formSchema>) => {
49 try {
50 setLoading(true)
51 await createShortLink(values as CreateLinkRequest)
52 form.reset()
53 onSuccess() // Call the onSuccess callback to trigger refresh
54 toast({
55 description: "Short link created successfully",
56 })
57 } catch (err: any) {
58 toast({
59 variant: "destructive",
60 title: "Error",
61 description: err.response?.data?.error || 'An error occurred',
62 })
63 } finally {
64 setLoading(false)
65 }
66 }
67
68 return (
69 <Card className="mb-8">
70 <CardHeader>
71 <CardTitle>Create Short Link</CardTitle>
72 <CardDescription>Enter a URL to generate a shortened link</CardDescription>
73 </CardHeader>
74 <CardContent>
75 <Form {...form}>
76 <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4 md:flex-row md:items-end">
77 <FormField
78 control={form.control}
79 name="url"
80 render={({ field }) => (
81 <div className="flex-1 space-y-2">
82 <FormLabel>URL</FormLabel>
83 <FormControl>
84 <div className="relative">
85 <LinkIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
86 <Input placeholder="https://example.com" className="pl-9" {...field} />
87 </div>
88 </FormControl>
89 <FormMessage />
90 </div>
91 )}
92 />
93
94 <FormField
95 control={form.control}
96 name="custom_code"
97 render={({ field }) => (
98 <div className="w-full md:w-1/4 space-y-2">
99 <FormLabel>Custom Code <span className="text-muted-foreground">(optional)</span></FormLabel>
100 <FormControl>
101 <Input placeholder="custom-code" {...field} />
102 </FormControl>
103 <FormMessage />
104 </div>
105 )}
106 />
107
108 <Button type="submit" disabled={loading} className="md:w-auto">
109 {loading ? "Creating..." : "Create Short Link"}
110 </Button>
111 </form>
112 </Form>
113 </CardContent>
114 </Card>
115 )
116}