Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions surfsense_web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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)
# ─────────────────────────────────────────────────────────────────────────────
Expand Down
87 changes: 87 additions & 0 deletions surfsense_web/components/homepage/top-announcement-bar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-full">
{/* Same box as `.ss-home-nav-bar` below it: centered, capped at
`--home-max`, bordered on the sides. Filling *this* box with the
notice color (rather than the full-bleed strip around it) is what
keeps the banner reading as the same width as the nav. */}
<div className="relative mx-auto flex max-w-(--home-max) items-center justify-center gap-2 border-x border-border bg-(--notice,#3f74c8) px-4 py-2.5 text-center text-sm font-medium text-white">
<span>SurfSense is moving to a local app.</span>
<Link
href="/sunset"
className="inline-flex items-center gap-1 underline underline-offset-2 hover:opacity-90"
>
Read announcement
<LinkSquare02Icon className="size-3.5" />
</Link>
<button
type="button"
onClick={handleDismiss}
aria-label="Dismiss announcement"
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-[2px] p-1 hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
<Cancel01Icon className="size-4" />
</button>
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions surfsense_web/components/site/site-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IconChevronDown, IconMenu2, IconX } from "@tabler/icons-react";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { TopAnnouncementBar } from "@/components/homepage/top-announcement-bar";
import { NAV_LINKS, NAV_RESOURCES } from "@/components/site/site-content";
import { SiteStars } from "@/components/site/site-stars";
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
Expand Down Expand Up @@ -90,6 +91,7 @@ export function SiteNav({ starCount, starsHref }: { starCount: number | null; st

return (
<header ref={headerRef} className="ss-home-nav">
<TopAnnouncementBar />
<div className="ss-home-nav-bar">
<Wordmark />

Expand Down
3 changes: 2 additions & 1 deletion surfsense_web/components/site/site-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ export function SiteShell({

{/* Above every branch, so it is one element in one slot for every route.
React keeps it mounted across navigations and only the content below
changes. */}
changes. The announcement bar renders inside SiteNav's sticky header
rather than here, so the two stick together as one unit. */}
<SiteNav starCount={starCount} starsHref={starsHref} />

{usesSiteDesign ? (
Expand Down
2 changes: 2 additions & 0 deletions surfsense_web/components/ui/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ArrowRightIcon as ArrowRightIconData,
ArrowUp02Icon as ArrowUp02IconData,
ArrowUpRight01Icon as ArrowUpRight01IconData,
Cancel01Icon as Cancel01IconData,
Cards01Icon as Cards01IconData,
ChartHistogramIcon as ChartHistogramIconData,
CheckIcon as CheckIconData,
Expand Down Expand Up @@ -73,6 +74,7 @@ export const AiSearchLinesIcon = createIcon(AiSearchLinesIconData);
export const ArrowRightIcon = createIcon(ArrowRightIconData);
export const ArrowUp02Icon = createIcon(ArrowUp02IconData);
export const ArrowUpRight01Icon = createIcon(ArrowUpRight01IconData);
export const Cancel01Icon = createIcon(Cancel01IconData);
export const Cards01Icon = createIcon(Cards01IconData);
export const ChartHistogramIcon = createIcon(ChartHistogramIconData);
export const CheckIcon = createIcon(CheckIconData);
Expand Down
6 changes: 6 additions & 0 deletions surfsense_web/lib/env-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,9 @@ export const GLOBAL_ANNOUNCEMENT_ENABLED =

export const GLOBAL_ANNOUNCEMENT_MESSAGE =
process.env.NEXT_PUBLIC_GLOBAL_ANNOUNCEMENT_MESSAGE ?? "";

// Sticky top banner pointing at the /sunset announcement. Distinct from the
// maintenance banner above: this one sits above the nav and links straight to
// /sunset. Toggle via NEXT_PUBLIC_TOP_ANNOUNCEMENT_ENABLED ("true" to show) —
// the copy and link are fixed in TopAnnouncementBar.
export const TOP_ANNOUNCEMENT_ENABLED = process.env.NEXT_PUBLIC_TOP_ANNOUNCEMENT_ENABLED === "true";
Loading