/** * DeleteConfirmationDialog — mandatory two-tap destructive delete confirmation (Plan 03-06). * * Threat T-03-17: All deletes require explicit confirmation. No single-tap inline delete. * No "don't ask again". Every delete from the EventDetailPopover flows through this dialog. * * Trigger: DeleteConfirmationDialog opens when deleteDialogOpen (Zustand) is true. * On confirm: calls deleteEvent(uid), sets lastSyncedUid → SyncStateToast tracks the outbox row. * On cancel/Escape: closes dialog without deleting. Popover remains open. * * Layout: * - Centered modal, max-width 320px, all breakpoints * - Backdrop: --color-overlay * - Focus trap while open * - Escape to cancel * * Accessibility: * - role="dialog", aria-modal="true" * - Heading: "Delete event?" (18px/600) * - Focus moves to dialog on open * * Security: T-03-15 — all text rendered as plain-text JSX children. */ import { useEffect, useRef } from 'react' import { useMutation } from '@tanstack/react-query' import { Trash2 } from 'lucide-react' import { useCalendarStore } from '../store/calendarStore.js' import { deleteEvent } from '../api/client.js' // ── Component ────────────────────────────────────────────────────────────────── export function DeleteConfirmationDialog() { const deleteDialogOpen = useCalendarStore((s) => s.deleteDialogOpen) const deleteDialogUid = useCalendarStore((s) => s.deleteDialogUid) const setDeleteDialog = useCalendarStore((s) => s.setDeleteDialog) const setLastSyncedUid = useCalendarStore((s) => s.setLastSyncedUid) const setOpenEventId = useCalendarStore((s) => s.setOpenEventId) const dialogRef = useRef(null) // Focus trap — focus the dialog when it opens useEffect(() => { if (deleteDialogOpen && dialogRef.current) { dialogRef.current.focus() } }, [deleteDialogOpen]) // Escape key listener — cancel without deleting (T-03-17: no accidental delete) useEffect(() => { if (!deleteDialogOpen) return const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { handleCancel() } } document.addEventListener('keydown', onKeyDown) return () => document.removeEventListener('keydown', onKeyDown) }, [deleteDialogOpen]) // eslint-disable-line react-hooks/exhaustive-deps const handleCancel = () => { setDeleteDialog(false) } // TanStack mutation — DELETE /api/events/:uid (enqueued to outbox, D-05) const mutation = useMutation({ mutationFn: (uid: string) => deleteEvent(uid), onSuccess: () => { // Wire sync-toast: set the UID so SyncStateToast starts polling (D-05/D-09) if (deleteDialogUid) { setLastSyncedUid(deleteDialogUid) } // Close both dialog and popover (per UI-SPEC delete interaction step 4) setDeleteDialog(false) setOpenEventId(null) }, }) const handleDelete = () => { if (!deleteDialogUid) return mutation.mutate(deleteDialogUid) } // Nothing to show when closed if (!deleteDialogOpen) return null return ( <> {/* Backdrop */}