"use client"; import { useCallback, useEffect, useState } from "react"; // Single "analytics" bucket, not one bool per TrackingCodes.consentCategory // — a plain binary accept/reject banner (TTDSG's own bar for "equally // prominent" only requires two options) rather than a granular per-category // settings panel. A tracking-codes row's `marketing` category is gated by // the same flag as `analytics`; `necessary` always renders regardless of // this cookie (see TrackingScripts.tsx). export type Consent = { analytics: boolean }; const COOKIE_NAME = "cookie_consent"; const COOKIE_MAX_AGE_DAYS = 180; function readConsentCookie(): Consent | null { if (typeof document === "undefined") return null; const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE_NAME}=([^;]*)`)); if (!match) return null; try { const parsed = JSON.parse(decodeURIComponent(match[1])); return typeof parsed?.analytics === "boolean" ? { analytics: parsed.analytics } : null; } catch { return null; } } function writeConsentCookie(consent: Consent) { const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60; document.cookie = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(consent))}; path=/; max-age=${maxAge}; SameSite=Lax`; } /** * `consent === null` means "no decision made yet" (CookieBanner.tsx should * show) — distinct from `{ analytics: false }`, an explicit reject. * `loaded` gates on the client-only cookie read completing, so a server- * rendered page never briefly flashes the banner before hydration confirms * there's actually no cookie. */ export function useConsent() { const [consent, setConsentState] = useState(null); const [loaded, setLoaded] = useState(false); useEffect(() => { setConsentState(readConsentCookie()); setLoaded(true); }, []); const setConsent = useCallback((next: Consent) => { writeConsentCookie(next); setConsentState(next); }, []); return { consent, loaded, setConsent }; }