"use client"; // ============================================================================= // CookieBanner // ============================================================================= // // A small, non-blocking cookie / consent banner shown on first visit. // Two buttons: ACK (accept) / RST (reject) — network-engineer humour. // // Behaviour: // - On first load, if no consent decision is stored, the banner appears. // - "ACK" stores `novarix-cookie-consent = "ack"` in localStorage and the // banner closes. The site then behaves as before (intro animation // remembers it has played using sessionStorage). // - "RST" stores `novarix-cookie-consent = "rst"` in localStorage and the // banner closes. We also clear the intro-seen flag so we don't keep any // non-consent storage around. // - The footer "Cookie preferences" link dispatches a window event that // re-opens this banner so users can change their mind. // // All visible text comes from `/content.ts` -> site.cookies. // ============================================================================= import Link from "next/link"; import { useEffect, useState } from "react"; import { site } from "@/content"; const CONSENT_KEY = "novarix-cookie-consent"; const INTRO_SEEN_KEY = "novarix-intro-seen"; const REOPEN_EVENT = "novarix:open-cookie-banner"; const INTRO_EVENT = "novarix:start-intro"; type Consent = "unknown" | "ack" | "rst"; export default function CookieBanner() { // Start from the pre-hydration decision made in layout.tsx so the banner // can appear immediately on first visit without waiting for a delayed effect. const [visible, setVisible] = useState(() => { if (typeof document === "undefined") return false; return document.documentElement.dataset.showCookieBanner === "1"; }); // Read the stored consent on mount and decide whether to show the banner. useEffect(() => { try { const stored = window.localStorage.getItem(CONSENT_KEY) as Consent | null; if (stored !== "ack" && stored !== "rst") { document.documentElement.dataset.showCookieBanner = "1"; // eslint-disable-next-line react-hooks/set-state-in-effect setVisible(true); } else { document.documentElement.dataset.showCookieBanner = "0"; } } catch { // localStorage unavailable (private mode, blocked, etc.) — show the // banner so the user is still told. Sync-once-on-mount is a legitimate // use of setState in an effect. // eslint-disable-next-line react-hooks/set-state-in-effect document.documentElement.dataset.showCookieBanner = "1"; setVisible(true); } }, []); // Listen for the "re-open" event from the footer "Cookie preferences" link. useEffect(() => { const handler = () => setVisible(true); window.addEventListener(REOPEN_EVENT, handler); return () => window.removeEventListener(REOPEN_EVENT, handler); }, []); function decide(choice: "ack" | "rst") { try { window.localStorage.setItem(CONSENT_KEY, choice); if (choice === "rst") { // Honour the rejection by clearing any non-consent storage. window.sessionStorage.removeItem(INTRO_SEEN_KEY); } else { window.dispatchEvent(new Event(INTRO_EVENT)); } } catch { /* storage unavailable — nothing to do */ } document.documentElement.dataset.showCookieBanner = "0"; setVisible(false); } if (!visible) return null; return (
{site.cookies.message}{" "} {site.cookies.policyLabel} →
{/* Buttons */}