feat(scenarios): type-to-confirm delete modal, replacing browser confirm()

Swap the native confirm() popup for the app's shadcn Dialog pattern
(as in Settings → Danger Zone): the destructive button stays disabled
until the user types "delete" (case-insensitive), with Enter-to-submit
and a proper explanation of what's removed. On a delete error the
dialog closes so the inline error is visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-06 08:51:24 +00:00
parent 639cee6c4c
commit fdae966461

View file

@ -4,6 +4,13 @@ import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import {
Dialog,
DialogContent,
DialogTitle,
DialogFooter,
} from "@/app/shadcn_components/ui/dialog";
import { Button } from "@/app/shadcn_components/ui/button";
const btn =
"rounded-xl border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-brandblue transition hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-45";
@ -36,6 +43,9 @@ export function DetailActions({
const [editing, setEditing] = useState(false);
const [draftName, setDraftName] = useState(initialName);
const [error, setError] = useState<string | null>(null);
const [deleteOpen, setDeleteOpen] = useState(false);
const [deleteConfirmation, setDeleteConfirmation] = useState("");
const deleteConfirmed = deleteConfirmation.trim().toLowerCase() === "delete";
const rename = useMutation({
mutationFn: async (newName: string) => {
@ -68,7 +78,10 @@ export function DetailActions({
if (!res.ok) throw new Error((await res.json()).error ?? "Delete failed");
},
onSuccess: () => router.push(`/portfolio/${portfolioId}/scenarios`),
onError: (e: Error) => setError(e.message),
onError: (e: Error) => {
setError(e.message);
setDeleteOpen(false);
},
});
return (
@ -162,17 +175,12 @@ export function DetailActions({
<>
<button
className={`${btn} border-red-100 !text-red-800 hover:!bg-red-50`}
disabled={remove.isLoading}
onClick={() => {
if (
confirm(
`Delete "${name}"?\n\nNothing has been modelled against it — only the configuration is removed. This can't be undone.`,
)
)
remove.mutate();
setDeleteConfirmation("");
setDeleteOpen(true);
}}
>
{remove.isLoading ? "Deleting…" : "Delete scenario"}
Delete scenario
</button>
<span className="max-w-[52ch] text-[13px] text-gray-400">
Nothing has been modelled against this scenario deleting removes
@ -182,6 +190,51 @@ export function DetailActions({
)}
</div>
{error && <div className="mt-3 text-[13px] text-red-800">{error}</div>}
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
<DialogContent className="max-w-md">
<DialogTitle className="font-manrope text-lg font-extrabold text-brandblue">
Delete {name}?
</DialogTitle>
<p className="text-sm text-gray-600">
Nothing has been modelled against this scenario deleting removes
only the configuration. This can&apos;t be undone.
</p>
<div>
<label
htmlFor="delete-confirmation"
className="mb-1.5 block text-sm text-gray-600"
>
To confirm, type <strong className="text-brandblue">delete</strong>
</label>
<input
id="delete-confirmation"
autoFocus
type="text"
value={deleteConfirmation}
placeholder="delete"
onChange={(e) => setDeleteConfirmation(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && deleteConfirmed && !remove.isLoading)
remove.mutate();
}}
className="w-full rounded-xl border border-gray-200 px-3 py-2 text-sm outline-none transition focus:border-red-300 focus:shadow-[0_0_0_3px_rgba(185,28,28,.10)]"
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteOpen(false)}>
Cancel
</Button>
<Button
className="bg-red-700 hover:bg-red-800"
disabled={!deleteConfirmed || remove.isLoading}
onClick={() => remove.mutate()}
>
{remove.isLoading ? "Deleting…" : "Delete scenario"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}