Files
einfach-produktiv/app/lib/useConsent.ts
T
Marco 70ee518e1d Ship tracking-codes Phase 2: cookie consent banner + gated script injection
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>
2026-08-01 20:03:11 +00:00

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 };
}