diff --git a/frontend/src/components/dashboard/dashboard-view.tsx b/frontend/src/components/dashboard/dashboard-view.tsx index 77bb8453..5903cb2e 100644 --- a/frontend/src/components/dashboard/dashboard-view.tsx +++ b/frontend/src/components/dashboard/dashboard-view.tsx @@ -49,6 +49,7 @@ import { import { TopUpModal } from "../stream-creation/TopUpModal"; import { useQueryClient } from "@tanstack/react-query"; import { CancelConfirmModal } from "../stream-creation/CancelConfirmModal"; +import { ConfirmModal } from "../stream-creation/ConfirmModal"; import { StreamDetailsModal } from "./StreamDetailsModal"; import { Button } from "../ui/Button"; @@ -68,7 +69,8 @@ type ModalState = | null | { type: "topup"; stream: Stream } | { type: "cancel"; stream: Stream } - | { type: "details"; stream: Stream }; + | { type: "details"; stream: Stream } + | { type: "deleteTemplate"; templateId: string; templateName: string }; interface StreamFormValues { recipient: string; @@ -771,13 +773,17 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { const handleDeleteTemplate = (templateId: string) => { const template = templates.find((t) => t.id === templateId); if (!template) return; - if (!window.confirm(`Delete stream template "${template.name}"?`)) return; + setModal({ type: "deleteTemplate", templateId: template.id, templateName: template.name }); + }; + + const handleDeleteTemplateConfirm = (templateId: string) => { setTemplates((prev) => prev.filter((t) => t.id !== templateId)); if (selectedTemplateId === templateId) setSelectedTemplateId(null); if (editingTemplateId === templateId) { setEditingTemplateId(null); setTemplateNameInput(""); } + setModal(null); }; const handleResetStreamForm = () => { @@ -1489,6 +1495,17 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { onTopUpClick={() => setModal({ type: "topup", stream: modal.stream })} /> )} + {modal?.type === "deleteTemplate" && ( + handleDeleteTemplateConfirm(modal.templateId)} + onClose={() => setModal(null)} + /> + )} ); } diff --git a/frontend/src/components/stream-creation/ConfirmModal.tsx b/frontend/src/components/stream-creation/ConfirmModal.tsx new file mode 100644 index 00000000..e486f804 --- /dev/null +++ b/frontend/src/components/stream-creation/ConfirmModal.tsx @@ -0,0 +1,175 @@ +"use client"; + +/** + * ConfirmModal.tsx + * + * Reusable confirmation dialog for destructive actions throughout the app. + * Generalises the CancelConfirmModal pattern so any destructive action + * (template deletion, stream cancellation, etc.) can use a consistent, + * styled confirmation instead of a native `window.confirm`. + */ + +import React, { useState } from "react"; +import { Button } from "@/components/ui/Button"; +import { useModalDialog } from "@/hooks/useModalDialog"; + +interface ConfirmModalProps { + title: string; + message: string; + /** Text shown on the destructive confirm button. Defaults to "Confirm". */ + confirmLabel?: string; + /** Text shown on the safe cancel button. Defaults to "Cancel". */ + cancelLabel?: string; + /** Visual variant controls the confirm button colour. Defaults to "danger". */ + variant?: "danger" | "warning" | "info"; + onConfirm: () => void | Promise; + onClose: () => void; +} + +const VARIANT_CLASSES: Record = { + danger: "bg-red-600 hover:bg-red-500 text-white", + warning: "bg-amber-600 hover:bg-amber-500 text-white", + info: "bg-accent hover:brightness-110 text-white", +}; + +const ICON_CLASSES: Record = { + danger: { ring: "bg-red-500/15", icon: "text-red-400" }, + warning: { ring: "bg-amber-500/15", icon: "text-amber-400" }, + info: { ring: "bg-accent/15", icon: "text-accent" }, +}; + +export const ConfirmModal: React.FC = ({ + title, + message, + confirmLabel = "Confirm", + cancelLabel = "Cancel", + variant = "danger", + onConfirm, + onClose, +}) => { + const [isSubmitting, setIsSubmitting] = useState(false); + + const dialogRef = useModalDialog({ onClose, isCloseDisabled: isSubmitting }); + + const handleConfirm = async () => { + setIsSubmitting(true); + try { + await onConfirm(); + } catch { + setIsSubmitting(false); + } + }; + + const fallback = ICON_CLASSES.danger!; + const iconStyle = ICON_CLASSES[variant] ?? fallback; + const buttonClass = + VARIANT_CLASSES[variant] ?? VARIANT_CLASSES.danger; + + return ( +
{ + if (e.target === e.currentTarget && !isSubmitting) onClose(); + }} + > +
+ {/* Header */} +
+
+ + + +
+
+

+ {title} +

+

{message}

+
+ +
+ + {/* Actions */} +
+ + +
+
+
+ ); +};