From 8de1d801c42dbd75740473e09c324c4561ace6d6 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 19 Jun 2026 11:18:20 +0200 Subject: [PATCH] refactor(banner): extract reusable Banner base for VersionBanner + DemoBanner Both top-of-app bars shared the same shell (icon + content + border-b bar); factor it into a tone-driven Banner with optional action/dismiss. The version/demo logic stays in the wrappers. Leaves a clean extension point for a future news-ticker variant. --- frontend/src/components/Banner.tsx | 53 +++++++++++++++++++++++ frontend/src/components/DemoBanner.tsx | 12 +++-- frontend/src/components/VersionBanner.tsx | 34 +++++++-------- 3 files changed, 73 insertions(+), 26 deletions(-) create mode 100644 frontend/src/components/Banner.tsx diff --git a/frontend/src/components/Banner.tsx b/frontend/src/components/Banner.tsx new file mode 100644 index 0000000..d789078 --- /dev/null +++ b/frontend/src/components/Banner.tsx @@ -0,0 +1,53 @@ +import type { ReactNode } from "react"; +import type { LucideIcon } from "lucide-react"; +import { X } from "lucide-react"; + +type BannerTone = "accent" | "warning"; + +const TONES: Record = { + accent: { bar: "bg-accent/15 border-accent/30", icon: "text-accent", action: "bg-accent text-accent-fg" }, + warning: { bar: "bg-amber-500/15 border-amber-500/30", icon: "text-amber-500", action: "bg-amber-500 text-black" }, +}; + +// Shared top-of-app notification bar. VersionBanner and DemoBanner are thin wrappers over this; +// a future news-ticker variant can build on the same shell (rotating content as children). +export default function Banner({ + tone, + icon: Icon, + children, + action, + onDismiss, + dismissTitle, +}: { + tone: BannerTone; + icon: LucideIcon; + children: ReactNode; + action?: { label: string; onClick: () => void }; + onDismiss?: () => void; + dismissTitle?: string; +}) { + const c = TONES[tone]; + return ( +
+ + {children} + {action && ( + + )} + {onDismiss && ( + + )} +
+ ); +} diff --git a/frontend/src/components/DemoBanner.tsx b/frontend/src/components/DemoBanner.tsx index 2fe59bd..d9b6cb9 100644 --- a/frontend/src/components/DemoBanner.tsx +++ b/frontend/src/components/DemoBanner.tsx @@ -1,17 +1,15 @@ import { useTranslation } from "react-i18next"; import { AlertTriangle } from "lucide-react"; +import Banner from "./Banner"; // Permanent, non-dismissible bar shown to the shared demo account so it's always clear the // state is communal. (Replaces the old login-time toast, which re-popped on every reload.) export default function DemoBanner() { const { t } = useTranslation(); return ( -
- - - {t("common.demoTitle")} - — {t("common.demoSharedNotice")} - -
+ + {t("common.demoTitle")} + — {t("common.demoSharedNotice")} + ); } diff --git a/frontend/src/components/VersionBanner.tsx b/frontend/src/components/VersionBanner.tsx index 720854e..d486bb6 100644 --- a/frontend/src/components/VersionBanner.tsx +++ b/frontend/src/components/VersionBanner.tsx @@ -1,7 +1,8 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Sparkles, X } from "lucide-react"; +import { Sparkles } from "lucide-react"; import { FRONTEND_VERSION, SEEN_VERSION_KEY } from "../lib/version"; +import Banner from "./Banner"; // Eye-catching, dismissible bar shown once after the running build's version changes // (compares the baked frontend version to the last one the user acknowledged). @@ -19,25 +20,20 @@ export default function VersionBanner({ onOpen }: { onOpen: () => void }) { } return ( -
- - {t("header.banner.updated", { version: FRONTEND_VERSION })} - - -
+ }, + }} + onDismiss={markSeen} + dismissTitle={t("header.banner.dismiss")} + > + {t("header.banner.updated", { version: FRONTEND_VERSION })} + ); }