Adds × delete button per row in both tables (desktop column + mobile card header). Click → native <dialog> confirm modal with Esc/backdrop-cancel, destructive red button, error inline. Wires deleteAccount/deleteUser Server Actions calling the new api-server routes; revalidatePath refreshes the list on success. EditableCell input switches to text-base (16px) on phone (sm:text-[13px] above 640px), preventing iOS Safari auto-zoom-on-focus that was shifting the layout when the soft keyboard appeared.
89 lines
2.4 KiB
TypeScript
89 lines
2.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
|
|
type Props = {
|
|
open: boolean;
|
|
onCancel: () => void;
|
|
onConfirm: () => void;
|
|
title: string;
|
|
message: React.ReactNode;
|
|
confirmLabel?: string;
|
|
destructive?: boolean;
|
|
pending?: boolean;
|
|
};
|
|
|
|
/**
|
|
* Centered modal confirmation dialog. Uses the native <dialog> element
|
|
* so we get Esc-to-close, focus trapping, and the ::backdrop pseudo for
|
|
* the scrim — no a11y tax we'd pay rolling our own. Backdrop click
|
|
* cancels.
|
|
*/
|
|
export default function ConfirmDialog({
|
|
open,
|
|
onCancel,
|
|
onConfirm,
|
|
title,
|
|
message,
|
|
confirmLabel = "Confirm",
|
|
destructive = false,
|
|
pending = false,
|
|
}: Props) {
|
|
const ref = useRef<HTMLDialogElement>(null);
|
|
|
|
useEffect(() => {
|
|
const dialog = ref.current;
|
|
if (!dialog) return;
|
|
if (open && !dialog.open) {
|
|
dialog.showModal();
|
|
} else if (!open && dialog.open) {
|
|
dialog.close();
|
|
}
|
|
}, [open]);
|
|
|
|
return (
|
|
<dialog
|
|
ref={ref}
|
|
onClose={onCancel}
|
|
onClick={(e) => {
|
|
// Click on the dialog background (not the inner form) cancels.
|
|
if (e.target === ref.current) onCancel();
|
|
}}
|
|
className="m-auto w-[min(92vw,440px)] rounded-2xl bg-white p-0 ring-1 ring-zinc-200/60 backdrop:bg-zinc-900/50 backdrop:backdrop-blur-sm"
|
|
>
|
|
<form
|
|
method="dialog"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
onConfirm();
|
|
}}
|
|
className="flex flex-col gap-4 p-6"
|
|
>
|
|
<h2 className="text-lg font-semibold tracking-tight text-zinc-900">
|
|
{title}
|
|
</h2>
|
|
<div className="text-sm leading-relaxed text-zinc-600">{message}</div>
|
|
<div className="mt-2 flex items-center justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={onCancel}
|
|
disabled={pending}
|
|
className="rounded-full px-4 py-2 text-xs font-medium text-zinc-700 transition-colors hover:bg-zinc-100 hover:text-zinc-900 disabled:opacity-60"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={pending}
|
|
className={`rounded-full px-4 py-2 text-xs font-medium text-white transition-colors disabled:opacity-60 ${
|
|
destructive ? "bg-red-600 hover:bg-red-700" : "bg-zinc-900 hover:bg-zinc-700"
|
|
}`}
|
|
>
|
|
{pending ? "…" : confirmLabel}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</dialog>
|
|
);
|
|
}
|