Files
novarix.uk/components/CookieBanner.tsx
T
Kismet Hasanaj c22d54abb0 .
2026-05-03 01:09:44 +02:00

135 lines
5.5 KiB
TypeScript

"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, useLayoutEffect, 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() {
// Hidden by default on the server, then synchronously shown on first paint
// if no consent choice exists yet.
const [visible, setVisible] = useState(false);
// Read the stored consent on mount and decide whether to show the banner.
useLayoutEffect(() => {
try {
const stored = window.localStorage.getItem(CONSENT_KEY) as Consent | null;
if (stored !== "ack" && stored !== "rst") {
setVisible(true);
}
} catch {
// localStorage unavailable (private mode, blocked, etc.) — show the
// banner so the user is still told.
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 */
}
setVisible(false);
}
if (!visible) return null;
return (
<div
role="dialog"
aria-live="polite"
aria-label="Cookie consent"
className="fixed inset-x-0 bottom-0 z-[2500] flex justify-center p-3 sm:p-5"
>
<div className="pointer-events-auto w-full max-w-3xl rounded-2xl border border-[var(--border-strong)] bg-[var(--surface-strong)] p-4 shadow-[0_24px_60px_-20px_oklch(0_0_0/0.45)] backdrop-blur-xl sm:p-5">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
{/* Message + policy link */}
<p className="text-sm leading-relaxed text-[var(--text)]">
{site.cookies.message}{" "}
<Link
href={site.cookies.policyHref}
className="font-medium text-[var(--accent)] underline-offset-4 hover:underline"
>
{site.cookies.policyLabel}
</Link>
</p>
{/* Buttons */}
<div className="flex flex-shrink-0 gap-2">
<button
type="button"
onClick={() => decide("rst")}
aria-label={site.cookies.reject.ariaLabel}
title={site.cookies.reject.tooltip}
className="group inline-flex h-11 min-w-[5.5rem] flex-col items-center justify-center rounded-xl border border-[var(--border-strong)] bg-transparent px-4 font-mono text-sm font-semibold text-[var(--text)] transition-all hover:-translate-y-0.5 hover:border-[var(--text)]"
>
<span className="leading-none">{site.cookies.reject.label}</span>
<span className="mt-0.5 text-[0.65rem] font-normal tracking-wide text-[var(--text-soft)] uppercase">
{site.cookies.reject.subtitle}
</span>
</button>
<button
type="button"
onClick={() => decide("ack")}
aria-label={site.cookies.accept.ariaLabel}
title={site.cookies.accept.tooltip}
className="group inline-flex h-11 min-w-[5.5rem] flex-col items-center justify-center rounded-xl bg-[var(--button-bg)] px-4 font-mono text-sm font-semibold text-[var(--button-fg)] shadow-sm transition-all hover:-translate-y-0.5 hover:bg-[var(--button-hover)] hover:shadow-md"
>
<span className="leading-none">{site.cookies.accept.label}</span>
<span className="mt-0.5 text-[0.65rem] font-normal tracking-wide opacity-70 uppercase">
{site.cookies.accept.subtitle}
</span>
</button>
</div>
</div>
</div>
</div>
);
}
// Helper used by the footer "Cookie preferences" link to re-open the banner.
export function openCookieBanner() {
window.dispatchEvent(new Event(REOPEN_EVENT));
}