70ee518e1d
CookieBanner.tsx (equally-weighted Akzeptieren/Ablehnen, TTDSG) +
useConsent() cookie hook + TrackingScripts.tsx render active
tracking-codes rows only once their consentCategory is actually
accepted ('necessary' always renders). Wired into layout.tsx via the
new getTrackingCodes() fetcher. This is what makes the Phase 1
backend collection (tracking-codes) actually usable end to end.
Also reworks NotifyMeForm back to always-visible input+button (better
UX than a collapse-to-reveal step) — the resulting taller CTA is now
reserved on every card via NotifyMeFormReservedSpace, an invisible
twin rendered behind the real button, so an in-stock card's row
height matches an out-of-stock sibling's without the grid's
row-stretch pushing buttons out of alignment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
"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<Consent | null>(null);
|
|
const [loaded, setLoaded] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setConsentState(readConsentCookie());
|
|
setLoaded(true);
|
|
}, []);
|
|
|
|
const setConsent = useCallback((next: Consent) => {
|
|
writeConsentCookie(next);
|
|
setConsentState(next);
|
|
}, []);
|
|
|
|
return { consent, loaded, setConsent };
|
|
}
|