From a882d108fe90c68dcd7ae7da40746defbb63d875 Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Fri, 17 Jul 2026 14:04:55 +0530 Subject: [PATCH] feat(hq-web): favicon, per-route tab titles, success-toast sweep Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/hq-web/index.html | 2 ++ apps/hq-web/public/favicon.svg | 4 ++++ apps/hq-web/src/Layout.tsx | 8 ++++++++ apps/hq-web/src/components/ReminderQueue.tsx | 12 ++++++++---- apps/hq-web/src/pages/ClientDetail.tsx | 6 ++++-- apps/hq-web/src/pages/Modules.tsx | 6 ++---- 6 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 apps/hq-web/public/favicon.svg diff --git a/apps/hq-web/index.html b/apps/hq-web/index.html index d21cb9e..df55a91 100644 --- a/apps/hq-web/index.html +++ b/apps/hq-web/index.html @@ -3,6 +3,8 @@ + + SiMS HQ diff --git a/apps/hq-web/public/favicon.svg b/apps/hq-web/public/favicon.svg new file mode 100644 index 0000000..1ed85a6 --- /dev/null +++ b/apps/hq-web/public/favicon.svg @@ -0,0 +1,4 @@ + + + HQ + diff --git a/apps/hq-web/src/Layout.tsx b/apps/hq-web/src/Layout.tsx index 2a9edf9..0254638 100644 --- a/apps/hq-web/src/Layout.tsx +++ b/apps/hq-web/src/Layout.tsx @@ -59,6 +59,14 @@ export function Layout() { return () => window.removeEventListener('keydown', onKey) }, []) + // Tab title tracks the active nav section (spec §3); record routes fall back to + // their section title since they aren't separate nav items. + useEffect(() => { + const item = NAV_GROUPS.flatMap((g) => g.items).find((i) => + i.to === '/' ? location.pathname === '/' : location.pathname.startsWith(i.to)) + document.title = item !== undefined && item.to !== '/' ? `${item.label} · SiMS HQ` : 'SiMS HQ' + }, [location.pathname]) + const staticItems = useMemo(() => { const isOwner = role() === 'owner' const navItems = NAV_GROUPS.flatMap((g) => g.items) diff --git a/apps/hq-web/src/components/ReminderQueue.tsx b/apps/hq-web/src/components/ReminderQueue.tsx index 1400c6f..872f5f8 100644 --- a/apps/hq-web/src/components/ReminderQueue.tsx +++ b/apps/hq-web/src/components/ReminderQueue.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { Button } from '@sims/ui' +import { Button, useToast } from '@sims/ui' import { dismissReminder, getReminderPreview, sendReminder, type Reminder } from '../api' /** Fallback kind labels for queue rows whose server `label` is empty. */ @@ -23,13 +23,17 @@ export const reminderTone = (status: string): 'ok' | 'warn' | 'err' | undefined * and the row must flip live. */ export function QueueActions(props: { rem: Reminder; onDone: () => void; onError: (m: string) => void }) { + const toast = useToast() const [busy, setBusy] = useState(false) const [mail, setMail] = useState<{ subject: string; body: string } | undefined>() const [loading, setLoading] = useState(false) - const run = (start: () => Promise) => { + // Toast on success only — onDone still fires on failure too (the server has + // already parked the reminder as 'failed' and the row must flip live). + const run = (start: () => Promise, okMsg: string) => { if (busy) return // Button has no disabled prop — guard re-entry here setBusy(true); props.onError('') start() + .then(() => toast.ok(okMsg)) .catch((e: Error) => props.onError(e.message)) .then(props.onDone) .finally(() => setBusy(false)) @@ -49,10 +53,10 @@ export function QueueActions(props: { rem: Reminder; onDone: () => void; onError {SENDABLE.has(props.rem.ruleKind) && ( <> - + )} - + {mail !== undefined && (
diff --git a/apps/hq-web/src/pages/ClientDetail.tsx b/apps/hq-web/src/pages/ClientDetail.tsx index 069d726..f67fc3b 100644 --- a/apps/hq-web/src/pages/ClientDetail.tsx +++ b/apps/hq-web/src/pages/ClientDetail.tsx @@ -137,7 +137,8 @@ export function ClientDetail() { onChange={(e) => { setActionErr(undefined) patchClient(id, { status: e.target.value as ClientStatus }) - .then(client.reload).catch((err: Error) => setActionErr(err.message)) + .then(() => { toast.ok('Status updated'); client.reload() }) + .catch((err: Error) => setActionErr(err.message)) }} > {CLIENT_STATUSES.map((s) => )} @@ -156,7 +157,8 @@ export function ClientDetail() { onChange={(ownerId) => { setActionErr(undefined) setClientOwner(id, ownerId) - .then(client.reload).catch((err: Error) => setActionErr(err.message)) + .then(() => { toast.ok('Owner updated'); client.reload() }) + .catch((err: Error) => setActionErr(err.message)) }} /> diff --git a/apps/hq-web/src/pages/Modules.tsx b/apps/hq-web/src/pages/Modules.tsx index dd2b1a8..cefa408 100644 --- a/apps/hq-web/src/pages/Modules.tsx +++ b/apps/hq-web/src/pages/Modules.tsx @@ -166,7 +166,6 @@ function ModuleClients(props: { module: Module }) { function QuoteContent(props: { module: Module; isOwner: boolean; onSaved: () => void }) { const toast = useToast() const [text, setText] = useState(props.module.quoteContent.join('\n')) - const [saved, setSaved] = useState(false) const [error, setError] = useState() const [commitNonce, setCommitNonce] = useState(0) const previewBody = useMemo(() => JSON.stringify({ contentLines: splitLines(text) }), [text]) @@ -174,7 +173,7 @@ function QuoteContent(props: { module: Module; isOwner: boolean; onSaved: () => const save = () => { setError(undefined) patchModule(props.module.id, { quoteContent: splitLines(text) }) - .then(() => { setSaved(true); toast.ok('Quote content saved'); props.onSaved() }) + .then(() => { toast.ok('Quote content saved'); props.onSaved() }) .catch((e: Error) => setError(e.message)) } @@ -185,10 +184,9 @@ function QuoteContent(props: { module: Module; isOwner: boolean; onSaved: () =>