From b514d490d22d24c97bd0574ae206fc64c39ede9c Mon Sep 17 00:00:00 2001 From: Wavering Ana Date: Sat, 1 Feb 2025 02:18:32 -0500 Subject: [PATCH] Add edit link functionality --- frontend/src/api/client.ts | 6 + frontend/src/components/EditModal.tsx | 139 ++++++++++++++++++++ frontend/src/components/LinkList.tsx | 63 ++++++--- frontend/src/components/StatisticsModal.tsx | 6 +- frontend/vite.config.ts | 43 +++--- src/handlers.rs | 139 +++++++++++++++++++- src/main.rs | 1 + 7 files changed, 359 insertions(+), 38 deletions(-) create mode 100644 frontend/src/components/EditModal.tsx diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index b338de0..9aa35b7 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -58,6 +58,12 @@ export const getAllLinks = async () => { return response.data; }; +export const editLink = async (id: number, data: Partial) => { + const response = await api.patch(`/links/${id}`, data); + return response.data; +}; + + export const deleteLink = async (id: number) => { await api.delete(`/links/${id}`); }; diff --git a/frontend/src/components/EditModal.tsx b/frontend/src/components/EditModal.tsx new file mode 100644 index 0000000..fc48a8c --- /dev/null +++ b/frontend/src/components/EditModal.tsx @@ -0,0 +1,139 @@ +// src/components/EditModal.tsx +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { Link } from '../types/api'; +import { editLink } from '../api/client'; +import { useToast } from '@/hooks/use-toast'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; + +const formSchema = z.object({ + url: z + .string() + .min(1, 'URL is required') + .url('Must be a valid URL') + .refine((val) => val.startsWith('http://') || val.startsWith('https://'), { + message: 'URL must start with http:// or https://', + }), + custom_code: z + .string() + .regex(/^[a-zA-Z0-9_-]{1,32}$/, { + message: + 'Custom code must be 1-32 characters and contain only letters, numbers, underscores, and hyphens', + }) + .optional(), +}); + +interface EditModalProps { + isOpen: boolean; + onClose: () => void; + link: Link; + onSuccess: () => void; +} + +export function EditModal({ isOpen, onClose, link, onSuccess }: EditModalProps) { + const [loading, setLoading] = useState(false); + const { toast } = useToast(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + url: link.original_url, + custom_code: link.short_code, + }, + }); + + const onSubmit = async (values: z.infer) => { + try { + setLoading(true); + await editLink(link.id, values); + toast({ + description: 'Link updated successfully', + }); + onSuccess(); + onClose(); + } catch (err: unknown) { + const error = err as { response?: { data?: { error?: string } } }; + toast({ + variant: 'destructive', + title: 'Error', + description: error.response?.data?.error || 'Failed to update link', + }); + } finally { + setLoading(false); + } + }; + + return ( + + + + Edit Link + + +
+ + ( + + Destination URL + + + + + + )} + /> + + ( + + Short Code + + + + + + )} + /> + + + + + + + +
+
+ ); +} diff --git a/frontend/src/components/LinkList.tsx b/frontend/src/components/LinkList.tsx index 6f768cf..337ecf2 100644 --- a/frontend/src/components/LinkList.tsx +++ b/frontend/src/components/LinkList.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { Link } from '../types/api' import { getAllLinks, deleteLink } from '../api/client' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" @@ -12,7 +12,7 @@ import { } from "@/components/ui/table" import { Button } from "@/components/ui/button" import { useToast } from "@/hooks/use-toast" -import { Copy, Trash2, BarChart2 } from "lucide-react" +import { Copy, Trash2, BarChart2, Pencil } from "lucide-react" import { Dialog, DialogContent, @@ -23,6 +23,7 @@ import { } from "@/components/ui/dialog" import { StatisticsModal } from "./StatisticsModal" +import { EditModal } from './EditModal' interface LinkListProps { refresh?: number; @@ -39,27 +40,32 @@ export function LinkList({ refresh = 0 }: LinkListProps) { isOpen: false, linkId: null, }); + const [editModal, setEditModal] = useState<{ isOpen: boolean; link: Link | null }>({ + isOpen: false, + link: null, + }); const { toast } = useToast() - const fetchLinks = async () => { + const fetchLinks = useCallback(async () => { try { setLoading(true) const data = await getAllLinks() setLinks(data) - } catch (err) { + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; toast({ title: "Error", - description: "Failed to load links", + description: `Failed to load links: ${errorMessage}`, variant: "destructive", }) } finally { setLoading(false) } - } + }, [toast, setLinks, setLoading]) useEffect(() => { fetchLinks() - }, [refresh]) // Re-fetch when refresh counter changes + }, [fetchLinks, refresh]) // Re-fetch when refresh counter changes const handleDelete = async () => { if (!deleteModal.linkId) return @@ -71,10 +77,11 @@ export function LinkList({ refresh = 0 }: LinkListProps) { toast({ description: "Link deleted successfully", }) - } catch (err) { + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; toast({ title: "Error", - description: "Failed to delete link", + description: `Failed to delete link: ${errorMessage}`, variant: "destructive", }) } @@ -85,13 +92,13 @@ export function LinkList({ refresh = 0 }: LinkListProps) { const baseUrl = window.location.origin navigator.clipboard.writeText(`${baseUrl}/${shortCode}`) toast({ - description: ( - <> - Link copied to clipboard -
- You can add ?source=TextHere to the end of the link to track the source of clicks - - ), + description: ( + <> + Link copied to clipboard +
+ You can add ?source=TextHere to the end of the link to track the source of clicks + + ), }) } @@ -127,6 +134,7 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
+ @@ -134,7 +142,7 @@ export function LinkList({ refresh = 0 }: LinkListProps) { Original URL Clicks Created - Actions + Actions @@ -148,8 +156,8 @@ export function LinkList({ refresh = 0 }: LinkListProps) { {new Date(link.created_at).toLocaleDateString()} - -
+ +
+