From 59a146ef1080408b2c196db0fc00998901add5f1 Mon Sep 17 00:00:00 2001 From: DavidHerran Date: Tue, 5 May 2026 21:16:02 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20HQ-wide=20Toast=20+=20Confirm=20?= =?UTF-8?q?=E2=80=94=20no=20more=20browser=20alert()/confirm()=20popups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Inbox panel got the polished Toast + Confirm primitives a few commits back. This commit propagates them across every other panel in HQ Command so the editor experience is uniformly on-brand. No more 1990s browser dialogs interrupting the dark CMS look. NINE PANELS, ELEVEN CALL SITES UPGRADED - health/page.tsx — DB export errors → toast - network/page.tsx — Delete deployment → confirm + toast - heritage/page.tsx — Delete section → confirm + toast - users/page.tsx — Revoke architect → confirm + error toast - news/page.tsx — Delete article → confirm + toast - applications/page.tsx — Save error toast + delete-app confirm - parts/page.tsx — Delete component → confirm + toast - hero/page.tsx — Delete slide → confirm + toast - timeline/page.tsx — Delete milestone → confirm + toast Each destructive confirm now spells out what it does ('Permanently remove this case from the global map. The asset folder on disk is kept for safety') instead of a generic 'Delete?' prompt — much clearer for editors who aren't sure whether files get nuked too. Each success toast names the action ('Component deleted', 'Slide deleted', 'Architect access revoked') so the editor sees exactly what fired. Errors come in as red toasts with the actual error text. NO BACKEND CHANGES. Pure UX layer on top of existing actions. The HqUiProvider was already mounted in src/app/hq-command/layout.tsx, so wiring up was just useHqUi() per page + the replacement calls. --- .../dashboard/applications/page.tsx | 25 ++++++++++++++++--- src/app/hq-command/dashboard/health/page.tsx | 4 ++- .../hq-command/dashboard/heritage/page.tsx | 11 +++++++- src/app/hq-command/dashboard/hero/page.tsx | 11 +++++++- src/app/hq-command/dashboard/network/page.tsx | 15 ++++++++++- src/app/hq-command/dashboard/news/page.tsx | 13 +++++++++- src/app/hq-command/dashboard/parts/page.tsx | 15 ++++++++++- .../hq-command/dashboard/timeline/page.tsx | 11 +++++++- src/app/hq-command/dashboard/users/page.tsx | 18 ++++++++++--- 9 files changed, 109 insertions(+), 14 deletions(-) diff --git a/src/app/hq-command/dashboard/applications/page.tsx b/src/app/hq-command/dashboard/applications/page.tsx index c533670..1e2cd78 100644 --- a/src/app/hq-command/dashboard/applications/page.tsx +++ b/src/app/hq-command/dashboard/applications/page.tsx @@ -10,6 +10,7 @@ import { Maximize2, Minimize2, ChevronDown, FolderOpen, Upload, FolderPlus, ChevronRight, File, ArrowUpFromLine, Search, Grid3X3, LayoutList, Copy, Check } from "lucide-react"; import { getApplications, createApplication, updateApplicationData, toggleApplication, deleteApplication } from "./actions"; +import { useHqUi } from "@/components/hq/Toast"; // AssetBucketBrowser is the unified picker. The applications page uses an @@ -245,6 +246,7 @@ function MarkdownEditor({ name, defaultValue = "", required, rows = 12, placehol // ───────────────────────────────────────────────────────────────────────────── export default function ApplicationsManager() { + const ui = useHqUi(); const router = useRouter(); const [apps, setApps] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -273,8 +275,14 @@ export default function ApplicationsManager() { formData.append("sectionsJson", JSON.stringify(sections)); formData.append("dashboardMetricsJson", JSON.stringify(dashboardMetrics)); const res = await updateApplicationData(formData); - if (res.error) { alert("Error saving data: " + res.error); } - else { setEditingApp(null); await fetchApps(); router.refresh(); } + if (res.error) { + ui.toast(`Error saving data: ${res.error}`, "error"); + } else { + ui.toast("Application saved.", "success"); + setEditingApp(null); + await fetchApps(); + router.refresh(); + } setIsSubmitting(false); }; @@ -311,7 +319,18 @@ export default function ApplicationsManager() {

{app.title}

/{app.slug}

{isPopulated ? Populated : Pending Setup} -
+
); })} diff --git a/src/app/hq-command/dashboard/health/page.tsx b/src/app/hq-command/dashboard/health/page.tsx index d5a2221..4217829 100644 --- a/src/app/hq-command/dashboard/health/page.tsx +++ b/src/app/hq-command/dashboard/health/page.tsx @@ -7,8 +7,10 @@ import { DownloadCloud, ShieldAlert, UploadCloud, Loader2, CheckCircle2 } from "lucide-react"; import { getSystemMetrics, exportDatabase, restoreDatabase } from "./actions"; +import { useHqUi } from "@/components/hq/Toast"; export default function SystemHealth() { + const ui = useHqUi(); const [metrics, setMetrics] = useState(null); const [isExporting, setIsExporting] = useState(false); @@ -54,7 +56,7 @@ export default function SystemHealth() { document.body.removeChild(a); URL.revokeObjectURL(url); } else { - alert(res.error || "Export failed."); + ui.toast(res.error || "Export failed.", "error"); } setIsExporting(false); }; diff --git a/src/app/hq-command/dashboard/heritage/page.tsx b/src/app/hq-command/dashboard/heritage/page.tsx index 8c36d02..9c8ed20 100644 --- a/src/app/hq-command/dashboard/heritage/page.tsx +++ b/src/app/hq-command/dashboard/heritage/page.tsx @@ -15,6 +15,7 @@ import { reorderHeritageSections, createHeritageStub, } from "./actions"; +import { useHqUi } from "@/components/hq/Toast"; interface SectionRow { id: string; @@ -33,6 +34,7 @@ const TYPE_META: Record([]); const [loading, setLoading] = useState(true); const [savingId, setSavingId] = useState(null); @@ -72,8 +74,15 @@ export default function HeritageManager() { }; const handleDelete = async (id: string) => { - if (!confirm("Delete this section? This cannot be undone.")) return; + const ok = await ui.confirm({ + title: "Delete section", + message: "Permanently remove this section from the heritage page. This cannot be undone.", + confirmLabel: "Delete section", + destructive: true, + }); + if (!ok) return; await deleteHeritageSection(id); + ui.toast("Section deleted.", "success"); await load(); }; diff --git a/src/app/hq-command/dashboard/hero/page.tsx b/src/app/hq-command/dashboard/hero/page.tsx index 5dfcce7..ddec46d 100644 --- a/src/app/hq-command/dashboard/hero/page.tsx +++ b/src/app/hq-command/dashboard/hero/page.tsx @@ -19,6 +19,7 @@ import { importFootageFiles, type FootageFile, } from "./actions"; +import { useHqUi } from "@/components/hq/Toast"; interface SlideRow { id: string; @@ -38,6 +39,7 @@ function safeParseJson(json: string | null | undefined, fallback: T): any { } export default function HeroDashboard() { + const ui = useHqUi(); const [slides, setSlides] = useState([]); const [loading, setLoading] = useState(true); const [savingId, setSavingId] = useState(null); @@ -142,8 +144,15 @@ export default function HeroDashboard() { }; const handleDelete = async (id: string) => { - if (!confirm("Delete this slide? The image file stays on disk and can be re-added.")) return; + const ok = await ui.confirm({ + title: "Delete slide", + message: "Removes the slide from the carousel. The image file stays on disk and can be re-imported later.", + confirmLabel: "Delete slide", + destructive: true, + }); + if (!ok) return; await deleteHeroSlide(id); + ui.toast("Slide deleted.", "success"); await loadSlides(); }; diff --git a/src/app/hq-command/dashboard/network/page.tsx b/src/app/hq-command/dashboard/network/page.tsx index 009e269..2ba2098 100644 --- a/src/app/hq-command/dashboard/network/page.tsx +++ b/src/app/hq-command/dashboard/network/page.tsx @@ -17,6 +17,7 @@ import { getApplications } from "../applications/actions"; // AssetBucketBrowser is the unified picker — single source of truth across HQ. // Aliased to AssetManager so existing JSX call sites remain untouched. import AssetBucketBrowser from "@/components/hq/AssetBucketBrowser"; +import { useHqUi } from "@/components/hq/Toast"; const AssetManager = AssetBucketBrowser; // ───────────────────────────────────────────────────────────────────────────── @@ -176,6 +177,7 @@ function MarkdownEditorCyan({ name, defaultValue = "", required, rows = 10, plac // ───────────────────────────────────────────────────────────────────────────── export default function NetworkManager() { + const ui = useHqUi(); const [nodes, setNodes] = useState([]); const [appsList, setAppsList] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -262,7 +264,18 @@ export default function NetworkManager() { setIsSavingCaseStudy(false); }; - const handleDelete = async (id: string) => { if (confirm("Delete this deployment?")) { await deleteNode(id); fetchNodesAndApps(); } }; + const handleDelete = async (id: string) => { + const ok = await ui.confirm({ + title: "Delete deployment", + message: "Permanently remove this case from the global map. The asset folder on disk is kept for safety.", + confirmLabel: "Delete deployment", + destructive: true, + }); + if (!ok) return; + await deleteNode(id); + ui.toast("Deployment deleted.", "success"); + fetchNodesAndApps(); + }; const handleToggle = async (id: string, status: boolean) => { await toggleNodeStatus(id, status); fetchNodesAndApps(); }; const availableTabs = [ diff --git a/src/app/hq-command/dashboard/news/page.tsx b/src/app/hq-command/dashboard/news/page.tsx index d042753..b53c812 100644 --- a/src/app/hq-command/dashboard/news/page.tsx +++ b/src/app/hq-command/dashboard/news/page.tsx @@ -17,6 +17,7 @@ import { getNewsArticles, createNewsArticle, updateNewsArticle, deleteNewsArticl // AssetBucketBrowser is the unified picker — single source of truth across HQ. // Aliased to AssetManager so existing JSX call sites remain untouched. import AssetBucketBrowser from "@/components/hq/AssetBucketBrowser"; +import { useHqUi } from "@/components/hq/Toast"; const AssetManager = AssetBucketBrowser; // ───────────────────────────────────────────────────────────────────────────── @@ -153,6 +154,7 @@ function MarkdownEditorCyan({ name, defaultValue = "", required, rows = 12, plac // ───────────────────────────────────────────────────────────────────────────── export default function NewsManager() { + const ui = useHqUi(); const [articles, setArticles] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); @@ -197,7 +199,16 @@ export default function NewsManager() { }; const handleDelete = async (id: string) => { - if (confirm("Delete this article?")) { await deleteNewsArticle(id); fetchArticles(); } + const ok = await ui.confirm({ + title: "Delete article", + message: "Permanently remove this news article. The asset folder on disk is preserved for safety.", + confirmLabel: "Delete article", + destructive: true, + }); + if (!ok) return; + await deleteNewsArticle(id); + ui.toast("Article deleted.", "success"); + fetchArticles(); }; return ( diff --git a/src/app/hq-command/dashboard/parts/page.tsx b/src/app/hq-command/dashboard/parts/page.tsx index c930960..eccd8bc 100644 --- a/src/app/hq-command/dashboard/parts/page.tsx +++ b/src/app/hq-command/dashboard/parts/page.tsx @@ -15,6 +15,7 @@ import { getParts, createPart, updatePart, deletePart, togglePartStatus, getPart // AssetBucketBrowser is the unified picker — single source of truth across HQ. // Aliased to AssetManager so existing JSX call sites remain untouched. import AssetBucketBrowser from "@/components/hq/AssetBucketBrowser"; +import { useHqUi } from "@/components/hq/Toast"; const AssetManager = AssetBucketBrowser; // ───────────────────────────────────────────────────────────────────────────── @@ -98,6 +99,7 @@ function MarkdownEditorAmber({ name, defaultValue = "", required, rows = 8, plac // ───────────────────────────────────────────────────────────────────────────── export default function PartsManager() { + const ui = useHqUi(); const [parts, setParts] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isCreateOpen, setIsCreateOpen] = useState(false); @@ -189,7 +191,18 @@ export default function PartsManager() {

{part.title}

{part.sku}

{part.showPrice && part.price ? €{part.price.toFixed(2)} : Quote Based} -
+
))} diff --git a/src/app/hq-command/dashboard/timeline/page.tsx b/src/app/hq-command/dashboard/timeline/page.tsx index 8a13c6f..d75ad39 100644 --- a/src/app/hq-command/dashboard/timeline/page.tsx +++ b/src/app/hq-command/dashboard/timeline/page.tsx @@ -16,6 +16,7 @@ import { createTimelineStub, seedTimeline, } from "./actions"; +import { useHqUi } from "@/components/hq/Toast"; interface EventRow { id: string; @@ -28,6 +29,7 @@ interface EventRow { } export default function TimelineManager() { + const ui = useHqUi(); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const [seeding, setSeeding] = useState(false); @@ -65,8 +67,15 @@ export default function TimelineManager() { }; const handleDelete = async (id: string) => { - if (!confirm("Delete this milestone? This cannot be undone.")) return; + const ok = await ui.confirm({ + title: "Delete milestone", + message: "Permanently remove this milestone from the company timeline. This cannot be undone.", + confirmLabel: "Delete milestone", + destructive: true, + }); + if (!ok) return; await deleteTimelineEvent(id); + ui.toast("Milestone deleted.", "success"); await load(); }; diff --git a/src/app/hq-command/dashboard/users/page.tsx b/src/app/hq-command/dashboard/users/page.tsx index c5deb88..a049c30 100644 --- a/src/app/hq-command/dashboard/users/page.tsx +++ b/src/app/hq-command/dashboard/users/page.tsx @@ -5,8 +5,10 @@ import Link from "next/link"; import Image from "next/image"; import { ArrowLeft, Users, Plus, Trash2, ShieldCheck, KeyRound, Loader2, X, Settings } from "lucide-react"; import { getUsers, createUser, deleteUser, updateUser } from "./actions"; +import { useHqUi } from "@/components/hq/Toast"; export default function UsersManager() { + const ui = useHqUi(); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -74,10 +76,18 @@ export default function UsersManager() { }; const handleDelete = async (id: string) => { - if (confirm("Are you sure you want to revoke this architect's access? This cannot be undone.")) { - const res = await deleteUser(id); - if (res.error) alert(res.error); - else fetchUsers(); + const ok = await ui.confirm({ + title: "Revoke architect access", + message: "Permanently remove this admin account. They will lose access to the Command Center immediately. This cannot be undone.", + confirmLabel: "Revoke access", + destructive: true, + }); + if (!ok) return; + const res = await deleteUser(id); + if (res.error) ui.toast(res.error, "error"); + else { + ui.toast("Architect access revoked.", "success"); + fetchUsers(); } };