From 91496dfc987dc6a9389876f48c4e71e3282f25c8 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 18 Sep 2026 00:17:04 +0530 Subject: [PATCH] feat(site-nav): add sticky /sunset announcement bar to the nav header --- surfsense_web/.env.example | 5 ++ .../homepage/top-announcement-bar.tsx | 87 +++++++++++++++++++ surfsense_web/components/site/site-nav.tsx | 2 + surfsense_web/components/site/site-shell.tsx | 3 +- surfsense_web/components/ui/icons.tsx | 2 + surfsense_web/lib/env-config.ts | 6 ++ 6 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 surfsense_web/components/homepage/top-announcement-bar.tsx diff --git a/surfsense_web/.env.example b/surfsense_web/.env.example index a6b72a1da5..6f0719eb89 100644 --- a/surfsense_web/.env.example +++ b/surfsense_web/.env.example @@ -76,6 +76,11 @@ NEXT_PUBLIC_GOOGLE_ADSENSE_SLOT_FREE_HUB_BEFORE_FAQ= NEXT_PUBLIC_GLOBAL_ANNOUNCEMENT_ENABLED=false NEXT_PUBLIC_GLOBAL_ANNOUNCEMENT_MESSAGE= +# ───────────────────────────────────────────────────────────────────────────── +# Sticky top announcement bar linking to /sunset. Set to "true" to show it. +# ───────────────────────────────────────────────────────────────────────────── +NEXT_PUBLIC_TOP_ANNOUNCEMENT_ENABLED=false + # ───────────────────────────────────────────────────────────────────────────── # Stripe Payment Link (Individual licence) # ───────────────────────────────────────────────────────────────────────────── diff --git a/surfsense_web/components/homepage/top-announcement-bar.tsx b/surfsense_web/components/homepage/top-announcement-bar.tsx new file mode 100644 index 0000000000..37fb387a8d --- /dev/null +++ b/surfsense_web/components/homepage/top-announcement-bar.tsx @@ -0,0 +1,87 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { Cancel01Icon, LinkSquare02Icon } from "@/components/ui/icons"; +import { TOP_ANNOUNCEMENT_ENABLED } from "@/lib/env-config"; + +// Versioned so a new announcement resurfaces even for visitors who dismissed +// an older one. Bump the version when the announcement changes. +const DISMISS_KEY = "surfsense:top-announcement-dismissed:v1"; + +/** + * Top banner pointing visitors at the /sunset announcement. + * + * Rendered as the first child of `SiteNav`'s `.ss-home-nav` header rather than + * as its own sticky element: that header is already `position: sticky`, so + * stacking the banner inside it makes the two scroll and stick together as + * one unit instead of each being independently sticky at `top: 0` and + * overlapping. + * + * Uses `--notice`, the blue defined alongside the rest of the palette in + * `app/(home)/home.css`, with a literal fallback so it also reads correctly + * on `(home)` routes that have not been moved onto that palette yet. + * + * Toggled with `NEXT_PUBLIC_TOP_ANNOUNCEMENT_ENABLED` like `GlobalAnnouncement`, + * so it can flip on/off from Vercel without a code change. Dismissal is + * persisted in localStorage so it stays hidden across reloads and navigations. + */ +export function TopAnnouncementBar() { + // Default visible so the banner is there on first paint instead of popping + // in after the localStorage check and shifting the nav down. The tradeoff + // is a visitor who already dismissed it sees a brief flash before this + // effect hides it again, which is preferable to every other visitor seeing + // a layout shift on every load. + const [dismissed, setDismissed] = useState(false); + + useEffect(() => { + try { + if (localStorage.getItem(DISMISS_KEY) === "1") { + setDismissed(true); + } + } catch { + // localStorage can throw in private browsing / when disabled; leave + // the banner visible. + } + }, []); + + const handleDismiss = () => { + setDismissed(true); + try { + localStorage.setItem(DISMISS_KEY, "1"); + } catch { + // Ignore: dismissal just won't persist across reloads. + } + }; + + if (!TOP_ANNOUNCEMENT_ENABLED || dismissed) { + return null; + } + + return ( +