"use client"; /** * ConfirmModal — palette-themed replacement for native `window.confirm`. * * Mirrors the structure of AuthModal (portal + backdrop + Esc-to-close + * click-outside dismiss + body-scroll lock) but with a single primary * action button. The default tone is "destructive" since that's the * common case (delete this thing), but the `tone` prop swaps to neutral * for less-scary confirms. * * Usage: * * const [open, setOpen] = useState(false); * setOpen(false)} * title="Delete this collection?" * message="Posts inside will stay on your feed." * confirmLabel="Delete" * onConfirm={async () => { await collections.delete(id); ... }} * /> * * The `onConfirm` may return a promise — the dialog shows a pending state * and stays open until it resolves, then auto-closes on success. Errors * thrown from onConfirm surface inline below the buttons. */ import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { AlertTriangle, X } from "lucide-react"; import { cn } from "@/lib/utils"; type Tone = "destructive" | "neutral"; export function ConfirmModal({ open, onClose, title, message, confirmLabel = "Confirm", cancelLabel = "Cancel", tone = "destructive", onConfirm, }: { open: boolean; onClose: () => void; title: string; message?: string; confirmLabel?: string; cancelLabel?: string; tone?: Tone; /** Called when the user confirms. May return a promise. Throw to keep the modal open with an error message. */ onConfirm: () => void | Promise; }) { const [working, setWorking] = useState(false); const [error, setError] = useState(null); // Reset transient state every time the modal opens — otherwise an error // from a previous attempt would linger on the next open. useEffect(() => { if (open) { setWorking(false); setError(null); } }, [open]); useEffect(() => { if (!open) return; function onKey(e: KeyboardEvent) { if (e.key === "Escape" && !working) onClose(); } document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open, working, onClose]); // Body-scroll lock while open. useEffect(() => { if (!open) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, [open]); if (!open) return null; if (typeof document === "undefined") return null; async function handleConfirm() { setError(null); setWorking(true); try { await onConfirm(); onClose(); } catch (err) { setError( err instanceof Error ? err.message : "Something went wrong.", ); } finally { setWorking(false); } } return createPortal( <>
!working && onClose()} aria-hidden />
{tone === "destructive" ? ( ) : null}

{title}

{message ? (

{message}

) : null}
{error ? (

{error}

) : null}
, document.body, ); }