mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-19 17:02:59 +00:00
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:
parent
639cee6c4c
commit
fdae966461
1 changed files with 62 additions and 9 deletions
|
|
@ -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'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue