Deletion verification modal

This commit is contained in:
2026-03-11 20:28:19 +02:00
parent 9ccf4dd365
commit a4c9810de7
4 changed files with 129 additions and 1 deletions

View File

@@ -0,0 +1,83 @@
import { useEffect } from "react";
type ConfirmDialogProps = {
open: boolean;
title: string;
description?: string;
contextText?: string;
confirmLabel: string;
cancelLabel: string;
busy?: boolean;
busyLabel?: string;
onConfirm: () => void;
onCancel: () => void;
};
export default function ConfirmDialog({
open,
title,
description,
contextText,
confirmLabel,
cancelLabel,
busy = false,
busyLabel,
onConfirm,
onCancel,
}: ConfirmDialogProps) {
useEffect(() => {
if (!open) return;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && !busy) {
onCancel();
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [open, busy, onCancel]);
if (!open) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-[#2B170A]/60 px-4"
onClick={() => {
if (!busy) onCancel();
}}
>
<div
role="dialog"
aria-modal="true"
className="w-full max-w-md rounded-xl border border-[#C99763] bg-[#F5D1A9] p-5 shadow-xl"
onClick={(event) => event.stopPropagation()}
>
<h3 className="text-lg font-semibold text-[#4C250E]">{title}</h3>
{description && <p className="mt-2 text-sm text-[#70421E]">{description}</p>}
{contextText && <p className="mt-1 text-sm font-medium text-[#4C250E]">{contextText}</p>}
<div className="mt-4 flex flex-wrap gap-2">
<button
type="button"
onClick={onCancel}
disabled={busy}
className="rounded-md border border-[#A56C38] bg-[#EED5B8] px-3 py-1.5 text-sm text-[#70421E] hover:bg-[#E3A977] disabled:cursor-not-allowed disabled:opacity-50"
>
{cancelLabel}
</button>
<button
type="button"
onClick={onConfirm}
disabled={busy}
className="rounded-md border border-[#70421E] bg-[#8E4F24] px-3 py-1.5 text-sm text-[#FFF7EE] hover:bg-[#70421E] disabled:cursor-not-allowed disabled:opacity-50"
>
{busy ? busyLabel ?? confirmLabel : confirmLabel}
</button>
</div>
</div>
</div>
);
}

View File

@@ -8,6 +8,7 @@ import {
updateLokOpenHours,
type LokOpenHours,
} from "~/api";
import ConfirmDialog from "~/components/ConfirmDialog";
import { useT } from "~/i18n";
import { openHoursAtom, toastsAtom, type Toast } from "~/state/appState";
@@ -38,6 +39,7 @@ export default function OpenHoursForm() {
const [isEditing, setIsEditing] = useState(false);
const [editingVersionId, setEditingVersionId] = useState("");
const [deletingId, setDeletingId] = useState("");
const [confirmDeleteVersion, setConfirmDeleteVersion] = useState<LokOpenHours | null>(null);
const [activatingId, setActivatingId] = useState("");
const [saving, setSaving] = useState(false);
const [form, setForm] = useState<FormState>(EMPTY_FORM);
@@ -217,6 +219,21 @@ export default function OpenHoursForm() {
}
};
const askDeleteConfirmation = (version: LokOpenHours) => {
setConfirmDeleteVersion(version);
};
const cancelDeleteConfirmation = () => {
if (deletingId) return;
setConfirmDeleteVersion(null);
};
const confirmDelete = async () => {
if (!confirmDeleteVersion) return;
await onDelete(confirmDeleteVersion);
setConfirmDeleteVersion(null);
};
const onSetActive = async (version: LokOpenHours) => {
if (version.isActive) return;
@@ -328,7 +345,7 @@ export default function OpenHoursForm() {
disabled={active || deleting}
onClick={(event) => {
event.stopPropagation();
void onDelete(version);
askDeleteConfirmation(version);
}}
className={`rounded-md border border-[#8E4F24] bg-[#EED5B8] px-3 py-1.5 text-sm text-[#70421E] disabled:cursor-not-allowed disabled:opacity-50 ${!active ? "hover:bg-[#E3A977]" : ""
}`}
@@ -416,6 +433,21 @@ export default function OpenHoursForm() {
</div>
</form>
</div>
<ConfirmDialog
open={Boolean(confirmDeleteVersion)}
title={t("home.openHours.deleteConfirmTitle")}
description={t("home.openHours.deleteConfirmMessage")}
contextText={confirmDeleteVersion?.name || t("home.openHours.latest")}
cancelLabel={t("home.openHours.cancel")}
confirmLabel={t("home.openHours.deleteConfirmAction")}
busy={Boolean(deletingId)}
busyLabel={t("home.openHours.deleting")}
onCancel={cancelDeleteConfirmation}
onConfirm={() => {
void confirmDelete();
}}
/>
</section>
);
}

View File

@@ -44,6 +44,10 @@ const translations = {
"home.openHours.saved": "New version saved",
"home.openHours.updated": "Version updated",
"home.openHours.deleted": "Version deleted",
"home.openHours.deleting": "Deleting...",
"home.openHours.deleteConfirmTitle": "Delete version?",
"home.openHours.deleteConfirmMessage": "This action cannot be undone.",
"home.openHours.deleteConfirmAction": "Delete version",
"about.title": "About",
"about.description":
"Livonsaaren Tietokonepaja is a local project providing IT services for our dear archipelago.",
@@ -125,6 +129,10 @@ const translations = {
"home.openHours.saved": "Uusi versio tallennettu",
"home.openHours.updated": "Versio päivitetty",
"home.openHours.deleted": "Versio poistettu",
"home.openHours.deleting": "Poistetaan...",
"home.openHours.deleteConfirmTitle": "Poistetaanko versio?",
"home.openHours.deleteConfirmMessage": "Tätä toimintoa ei voi perua.",
"home.openHours.deleteConfirmAction": "Poista versio",
"about.title": "Tietoja",
"about.description":
"Livonsaaren Tietokonepaja on paikallisprojekti, joka tuottaa IT-palveluita rakkaalle lähisaaristollemme.",
@@ -207,6 +215,11 @@ const translations = {
"home.openHours.saved": "Nová verzia uložená",
"home.openHours.updated": "Verzia bola aktualizovaná",
"home.openHours.deleted": "Verzia bola odstránená",
"home.openHours.deleting": "Odstraňuje sa...",
"home.openHours.deleteConfirmTitle": "Odstrániť verziu?",
"home.openHours.deleteConfirmMessage":
"Túto akciu nie je možné vrátiť späť.",
"home.openHours.deleteConfirmAction": "Odstrániť verziu",
"about.title": "O aplikácii",
"about.description":
"Livonsaaren Tietokonepaja je lokálny projekt poskytujúci IT služby pre naše milované súostrovie.",