diff --git a/app/(dashboard)/lists/[id]/page.tsx b/app/(dashboard)/lists/[id]/page.tsx index 2da69ec..5d3cbca 100644 --- a/app/(dashboard)/lists/[id]/page.tsx +++ b/app/(dashboard)/lists/[id]/page.tsx @@ -30,6 +30,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' +import { Trash2 } from 'lucide-react' import { useToast } from '@/components/ui/use-toast' import { DuplicatesTab } from '@/components/lists/DuplicatesTab' import { EmailCheckerTab } from '@/components/lists/EmailCheckerTab' @@ -103,6 +104,9 @@ export default function ListDetailPage() { const [togglingOptIn, setTogglingOptIn] = useState(false) + const [deleteTarget, setDeleteTarget] = useState(null) + const [deleting, setDeleting] = useState(false) + const { toast } = useToast() async function handleGdprExport(contact: Contact) { @@ -156,6 +160,34 @@ export default function ListDetailPage() { } } + async function handleDeleteContact() { + if (!deleteTarget) return + setDeleting(true) + try { + const res = await fetch(`/api/internal/contacts/${deleteTarget.id}`, { + method: 'DELETE', + }) + if (!res.ok) { + const data = await res.json().catch(() => ({})) + toast({ title: data.error || 'Failed to delete contact', variant: 'destructive' }) + return + } + toast({ title: `${deleteTarget.email} removed from this list` }) + setDeleteTarget(null) + if (contacts.length === 1 && page > 1) { + setPage(page - 1) + } else { + fetchContacts() + } + const listRes = await fetch(`/api/internal/lists/${listId}`) + if (listRes.ok) setListInfo(await listRes.json()) + } catch { + toast({ title: 'Failed to delete contact', variant: 'destructive' }) + } finally { + setDeleting(false) + } + } + async function handleToggleDoubleOptIn() { if (!listInfo) return const next = !listInfo.requireDoubleOptIn @@ -577,28 +609,40 @@ export default function ListDetailPage() { {format(new Date(contact.createdAt), 'MMM d, yyyy')} - - - - - - handleGdprExport(contact)}> - Export GDPR data - - { - setGdprDeleteContact(contact) - setGdprConfirmEmail('') - }} - > - Delete (GDPR) - - - +
+ + + + + + + handleGdprExport(contact)}> + Export GDPR data + + { + setGdprDeleteContact(contact) + setGdprConfirmEmail('') + }} + > + Delete (GDPR) + + + +
)) @@ -659,6 +703,43 @@ export default function ListDetailPage() { + { + if (!open && !deleting) setDeleteTarget(null) + }} + > + + + Delete contact + +
+

+ Delete {deleteTarget?.email} from this list? +

+

+ The contact and its send and engagement history are permanently removed. This cannot be undone. +

+
+ + + + +
+
+ { diff --git a/app/api/internal/contacts/[id]/route.ts b/app/api/internal/contacts/[id]/route.ts new file mode 100644 index 0000000..c94e0fc --- /dev/null +++ b/app/api/internal/contacts/[id]/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from 'next/server' +import { hardDeleteContact } from '@/lib/gdpr' +import { auditFromSession, logAudit } from '@/lib/audit' + +export async function DELETE( + req: NextRequest, + { params }: { params: { id: string } } +) { + const result = await hardDeleteContact(params.id) + + if (!result) { + return NextResponse.json({ error: 'Contact not found' }, { status: 404 }) + } + + await logAudit( + await auditFromSession(req), + 'contact.delete', + { type: 'contact', id: params.id }, + { + email: result.email, + listId: result.listId, + sendCount: result.sendCount, + eventCount: result.eventCount, + }, + ) + + return NextResponse.json({ + deleted: true, + id: params.id, + email: result.email, + sendCount: result.sendCount, + eventCount: result.eventCount, + }) +} \ No newline at end of file