From 374f173a091fb979929270e50b682d9c170bbcf4 Mon Sep 17 00:00:00 2001 From: KM Koushik Date: Sat, 25 Oct 2025 05:37:16 +1100 Subject: [PATCH] add delete resource modal (#280) --- .../(dashboard)/campaigns/delete-campaign.tsx | 121 ++--------- .../[contactBookId]/delete-contact.tsx | 119 ++--------- .../contacts/delete-contact-book.tsx | 124 ++--------- .../dev-settings/api-keys/delete-api-key.tsx | 119 ++--------- .../domains/[domainId]/delete-domain.tsx | 115 ++-------- .../(dashboard)/templates/delete-template.tsx | 121 ++--------- apps/web/src/components/DeleteResource.tsx | 201 ++++++++++++++++++ packages/ui/src/input.tsx | 6 +- 8 files changed, 332 insertions(+), 594 deletions(-) create mode 100644 apps/web/src/components/DeleteResource.tsx diff --git a/apps/web/src/app/(dashboard)/campaigns/delete-campaign.tsx b/apps/web/src/app/(dashboard)/campaigns/delete-campaign.tsx index 916280a..e5b0b50 100644 --- a/apps/web/src/app/(dashboard)/campaigns/delete-campaign.tsx +++ b/apps/web/src/app/(dashboard)/campaigns/delete-campaign.tsx @@ -1,57 +1,31 @@ "use client"; import { Button } from "@usesend/ui/src/button"; -import { Input } from "@usesend/ui/src/input"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@usesend/ui/src/dialog"; +import { DeleteResource } from "~/components/DeleteResource"; import { api } from "~/trpc/react"; -import React, { useState } from "react"; +import { Campaign } from "@prisma/client"; import { toast } from "@usesend/ui/src/toaster"; import { Trash2 } from "lucide-react"; import { z } from "zod"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@usesend/ui/src/form"; -import { Campaign } from "@prisma/client"; - -const campaignSchema = z.object({ - name: z.string(), -}); export const DeleteCampaign: React.FC<{ campaign: Partial & { id: string }; }> = ({ campaign }) => { - const [open, setOpen] = useState(false); const deleteCampaignMutation = api.campaign.deleteCampaign.useMutation(); - const utils = api.useUtils(); - const campaignForm = useForm>({ - resolver: zodResolver(campaignSchema), - }); + const campaignSchema = z + .object({ + confirmation: z + .string() + .min(1, "Please type the campaign name to confirm"), + }) + .refine((data) => data.confirmation === campaign.name, { + message: "Campaign name does not match", + path: ["confirmation"], + }); async function onCampaignDelete(values: z.infer) { - if (values.name !== campaign.name) { - campaignForm.setError("name", { - message: "Name does not match", - }); - return; - } - deleteCampaignMutation.mutate( { campaignId: campaign.id, @@ -59,77 +33,26 @@ export const DeleteCampaign: React.FC<{ { onSuccess: () => { utils.campaign.getCampaigns.invalidate(); - setOpen(false); toast.success(`Campaign deleted`); }, }, ); } - const name = campaignForm.watch("name"); - return ( - (_open !== open ? setOpen(_open) : null)} - > - + - - - - Delete Campaign - - Are you sure you want to delete{" "} - - {campaign.name} - - ? You can't reverse this. - - -
-
- - ( - - name - - - - {formState.errors.name ? ( - - ) : ( - - . - - )} - - )} - /> -
- -
- - -
-
-
+ } + confirmLabel="Delete Campaign" + /> ); }; diff --git a/apps/web/src/app/(dashboard)/contacts/[contactBookId]/delete-contact.tsx b/apps/web/src/app/(dashboard)/contacts/[contactBookId]/delete-contact.tsx index 8d058ef..43dc929 100644 --- a/apps/web/src/app/(dashboard)/contacts/[contactBookId]/delete-contact.tsx +++ b/apps/web/src/app/(dashboard)/contacts/[contactBookId]/delete-contact.tsx @@ -1,57 +1,29 @@ "use client"; import { Button } from "@usesend/ui/src/button"; -import { Input } from "@usesend/ui/src/input"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@usesend/ui/src/dialog"; +import { DeleteResource } from "~/components/DeleteResource"; import { api } from "~/trpc/react"; -import React, { useState } from "react"; +import { Contact } from "@prisma/client"; import { toast } from "@usesend/ui/src/toaster"; import { Trash2 } from "lucide-react"; import { z } from "zod"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@usesend/ui/src/form"; -import { Contact } from "@prisma/client"; - -const contactSchema = z.object({ - email: z.string().email(), -}); export const DeleteContact: React.FC<{ contact: Partial & { id: string; contactBookId: string }; }> = ({ contact }) => { - const [open, setOpen] = useState(false); const deleteContactMutation = api.contacts.deleteContact.useMutation(); - const utils = api.useUtils(); - const contactForm = useForm>({ - resolver: zodResolver(contactSchema), - }); + const contactSchema = z + .object({ + confirmation: z.string().email("Please enter a valid email address"), + }) + .refine((data) => data.confirmation === contact.email, { + message: "Email does not match", + path: ["confirmation"], + }); async function onContactDelete(values: z.infer) { - if (values.email !== contact.email) { - contactForm.setError("email", { - message: "Email does not match", - }); - return; - } - deleteContactMutation.mutate( { contactId: contact.id, @@ -60,7 +32,6 @@ export const DeleteContact: React.FC<{ { onSuccess: () => { utils.contacts.contacts.invalidate(); - setOpen(false); toast.success(`Contact deleted`); }, onError: (e) => { @@ -70,70 +41,20 @@ export const DeleteContact: React.FC<{ ); } - const email = contactForm.watch("email"); - return ( - (_open !== open ? setOpen(_open) : null)} - > - + - - - - Delete Contact - - Are you sure you want to delete{" "} - - {contact.email} - - ? You can't reverse this. - - -
-
- - ( - - Email - - - - {formState.errors.email ? ( - - ) : ( - - . - - )} - - )} - /> -
- -
- - -
-
-
+ } + confirmLabel="Delete Contact" + /> ); }; diff --git a/apps/web/src/app/(dashboard)/contacts/delete-contact-book.tsx b/apps/web/src/app/(dashboard)/contacts/delete-contact-book.tsx index a8f29b8..0886d60 100644 --- a/apps/web/src/app/(dashboard)/contacts/delete-contact-book.tsx +++ b/apps/web/src/app/(dashboard)/contacts/delete-contact-book.tsx @@ -1,60 +1,34 @@ "use client"; import { Button } from "@usesend/ui/src/button"; -import { Input } from "@usesend/ui/src/input"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@usesend/ui/src/dialog"; +import { DeleteResource } from "~/components/DeleteResource"; import { api } from "~/trpc/react"; -import React, { useState } from "react"; +import { ContactBook } from "@prisma/client"; import { toast } from "@usesend/ui/src/toaster"; import { Trash2 } from "lucide-react"; import { z } from "zod"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@usesend/ui/src/form"; -import { ContactBook } from "@prisma/client"; - -const contactBookSchema = z.object({ - name: z.string(), -}); export const DeleteContactBook: React.FC<{ contactBook: Partial & { id: string }; }> = ({ contactBook }) => { - const [open, setOpen] = useState(false); const deleteContactBookMutation = api.contacts.deleteContactBook.useMutation(); - const utils = api.useUtils(); - const contactBookForm = useForm>({ - resolver: zodResolver(contactBookSchema), - }); + const contactBookSchema = z + .object({ + confirmation: z + .string() + .min(1, "Please type the contact book name to confirm"), + }) + .refine((data) => data.confirmation === contactBook.name, { + message: "Contact book name does not match", + path: ["confirmation"], + }); async function onContactBookDelete( values: z.infer, ) { - if (values.name !== contactBook.name) { - contactBookForm.setError("name", { - message: "Name does not match", - }); - return; - } - deleteContactBookMutation.mutate( { contactBookId: contactBook.id, @@ -62,80 +36,26 @@ export const DeleteContactBook: React.FC<{ { onSuccess: () => { utils.contacts.getContactBooks.invalidate(); - setOpen(false); toast.success(`Contact book deleted`); }, }, ); } - const name = contactBookForm.watch("name"); - return ( - (_open !== open ? setOpen(_open) : null)} - > - + - - - - Delete Contact Book - - Are you sure you want to delete{" "} - - {contactBook.name} - - ? You can't reverse this. - - -
-
- - ( - - Contact book name - - - - {formState.errors.name ? ( - - ) : ( - - . - - )} - - )} - /> -
- -
- - -
-
-
+ } + confirmLabel="Delete Contact Book" + /> ); }; diff --git a/apps/web/src/app/(dashboard)/dev-settings/api-keys/delete-api-key.tsx b/apps/web/src/app/(dashboard)/dev-settings/api-keys/delete-api-key.tsx index 49586e1..36e123f 100644 --- a/apps/web/src/app/(dashboard)/dev-settings/api-keys/delete-api-key.tsx +++ b/apps/web/src/app/(dashboard)/dev-settings/api-keys/delete-api-key.tsx @@ -1,57 +1,31 @@ "use client"; import { Button } from "@usesend/ui/src/button"; -import { Input } from "@usesend/ui/src/input"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@usesend/ui/src/dialog"; +import { DeleteResource } from "~/components/DeleteResource"; import { api } from "~/trpc/react"; -import React, { useState } from "react"; import { ApiKey } from "@prisma/client"; import { toast } from "@usesend/ui/src/toaster"; import { Trash2 } from "lucide-react"; import { z } from "zod"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@usesend/ui/src/form"; - -const apiKeySchema = z.object({ - name: z.string(), -}); export const DeleteApiKey: React.FC<{ apiKey: Partial & { id: number }; }> = ({ apiKey }) => { - const [open, setOpen] = useState(false); const deleteApiKeyMutation = api.apiKey.deleteApiKey.useMutation(); - const utils = api.useUtils(); - const apiKeyForm = useForm>({ - resolver: zodResolver(apiKeySchema), - }); - - async function onDomainDelete(values: z.infer) { - if (values.name !== apiKey.name) { - apiKeyForm.setError("name", { - message: "Name does not match", - }); - return; - } + const apiKeySchema = z + .object({ + confirmation: z + .string() + .min(1, "Please type the API key name to confirm"), + }) + .refine((data) => data.confirmation === apiKey.name, { + message: "API key name does not match", + path: ["confirmation"], + }); + async function onApiKeyDelete(values: z.infer) { deleteApiKeyMutation.mutate( { id: apiKey.id, @@ -59,75 +33,26 @@ export const DeleteApiKey: React.FC<{ { onSuccess: () => { utils.apiKey.invalidate(); - setOpen(false); toast.success(`API key deleted`); }, }, ); } - const name = apiKeyForm.watch("name"); - return ( - (_open !== open ? setOpen(_open) : null)} - > - + - - - - Delete API key - - Are you sure you want to delete{" "} - {apiKey.name} - ? You can't reverse this. - - -
-
- - ( - - name - - - - {formState.errors.name ? ( - - ) : ( - - . - - )} - - )} - /> -
- -
- - -
-
-
+ } + confirmLabel="Delete API key" + /> ); }; diff --git a/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx b/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx index 76eb4cd..b5e8fab 100644 --- a/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx +++ b/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx @@ -1,60 +1,28 @@ "use client"; import { Button } from "@usesend/ui/src/button"; -import { Input } from "@usesend/ui/src/input"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@usesend/ui/src/dialog"; - -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@usesend/ui/src/form"; - +import { DeleteResource } from "~/components/DeleteResource"; import { api } from "~/trpc/react"; -import React, { useState } from "react"; import { Domain } from "@prisma/client"; import { useRouter } from "next/navigation"; import { toast } from "@usesend/ui/src/toaster"; import { z } from "zod"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; - -const domainSchema = z.object({ - domain: z.string(), -}); export const DeleteDomain: React.FC<{ domain: Domain }> = ({ domain }) => { - const [open, setOpen] = useState(false); - const [domainName, setDomainName] = useState(""); const deleteDomainMutation = api.domain.deleteDomain.useMutation(); - - const domainForm = useForm>({ - resolver: zodResolver(domainSchema), - }); - const utils = api.useUtils(); - const router = useRouter(); - async function onDomainDelete(values: z.infer) { - if (values.domain !== domain.name) { - domainForm.setError("domain", { - message: "Domain name does not match", - }); - return; - } + const domainSchema = z + .object({ + confirmation: z.string().min(1, "Please type the domain name to confirm"), + }) + .refine((data) => data.confirmation === domain.name, { + message: "Domain name does not match", + path: ["confirmation"], + }); + async function onDomainDelete(values: z.infer) { deleteDomainMutation.mutate( { id: domain.id, @@ -62,7 +30,6 @@ export const DeleteDomain: React.FC<{ domain: Domain }> = ({ domain }) => { { onSuccess: () => { utils.domain.domains.invalidate(); - setOpen(false); toast.success(`Domain ${domain.name} deleted`); router.replace("/domains"); }, @@ -71,61 +38,19 @@ export const DeleteDomain: React.FC<{ domain: Domain }> = ({ domain }) => { } return ( - (_open !== open ? setOpen(_open) : null)} - > - + Delete domain - - - - Delete domain - - Are you sure you want to delete{" "} - {domain.name} - ? You can't reverse this. - - -
- - ( - - Domain - - - - {formState.errors.domain ? ( - - ) : ( - - . - - )} - - )} - /> -
- -
- - -
-
+ } + confirmLabel="Delete domain" + /> ); }; diff --git a/apps/web/src/app/(dashboard)/templates/delete-template.tsx b/apps/web/src/app/(dashboard)/templates/delete-template.tsx index 3a558da..51dc965 100644 --- a/apps/web/src/app/(dashboard)/templates/delete-template.tsx +++ b/apps/web/src/app/(dashboard)/templates/delete-template.tsx @@ -1,57 +1,31 @@ "use client"; import { Button } from "@usesend/ui/src/button"; -import { Input } from "@usesend/ui/src/input"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@usesend/ui/src/dialog"; +import { DeleteResource } from "~/components/DeleteResource"; import { api } from "~/trpc/react"; -import React, { useState } from "react"; +import { Template } from "@prisma/client"; import { toast } from "@usesend/ui/src/toaster"; import { Trash2 } from "lucide-react"; import { z } from "zod"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@usesend/ui/src/form"; -import { Template } from "@prisma/client"; - -const templateSchema = z.object({ - name: z.string(), -}); export const DeleteTemplate: React.FC<{ template: Partial