diff --git a/app/components/CookieBanner.tsx b/app/components/CookieBanner.tsx deleted file mode 100644 index f2cf92f..0000000 --- a/app/components/CookieBanner.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { AnimatePresence, motion } from "motion/react"; -import { useConsent } from "../lib/useConsent"; - -/** - * Bottom banner, shown whenever useConsent()'s cookie hasn't recorded a - * decision yet — the hard prerequisite for TrackingScripts.tsx to ever - * render an analytics/marketing script (see that file's own comment). - * Two equally-sized buttons, not a prominent "Akzeptieren" next to a - * de-emphasized reject link — TTDSG requires an equally easy way to - * decline, not just technically present a way to. - */ -export function CookieBanner() { - const { consent, loaded, setConsent } = useConsent(); - const visible = loaded && consent === null; - - return ( - - {visible && ( - -
-

- Wir nutzen Cookies, um diese Seite zu betreiben und ihre Nutzung zu verstehen. Mehr dazu in unserer{" "} - - Datenschutzerklärung - - . -

-
- - -
-
-
- )} -
- ); -} diff --git a/app/components/KlaroConsentManager.tsx b/app/components/KlaroConsentManager.tsx new file mode 100644 index 0000000..d2426a7 --- /dev/null +++ b/app/components/KlaroConsentManager.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { useEffect } from "react"; +import "klaro/dist/klaro.css"; +import type { TrackingCode } from "../lib/payload"; +import { buildKlaroConfig } from "../lib/klaroConfig"; +import { loadTrackingCode } from "../lib/loadTrackingCode"; + +// Replaced a hand-rolled CookieBanner.tsx + useConsent.ts + TrackingScripts.tsx +// with kiprotect/klaro (open source, self-hosted, npm install klaro) — +// the custom banner only ever covered the accept/reject UI itself; Klaro +// additionally brings a real per-service consent list, bundled German UI +// translations, and (via `cookies`, not used here yet) cookie-deletion on +// withdrawal — all things a hand-rolled version would have had to build +// from scratch. See project memory for the fuller reasoning. +// +// Dynamically imported inside an effect (client-only, after mount) rather +// than a static top-level import — Klaro touches `window`/`document` at +// module-eval time in places, which isn't SSR-safe. The CSS import above +// stays static (Next.js requires CSS imports to be static, not inside a +// dynamic import()), paired with the "-no-css" JS build so the stylesheet +// isn't loaded twice. +export function KlaroConsentManager({ codes }: { codes: TrackingCode[] }) { + useEffect(() => { + // Nothing to ask consent for — don't even load/render Klaro. A cookie + // banner with zero services to list would just be visual noise. + if (codes.length === 0) return; + + let cancelled = false; + import("klaro/dist/klaro-no-css").then((Klaro) => { + if (cancelled) return; + const config = buildKlaroConfig(codes, loadTrackingCode); + Klaro.setup(config); + }); + return () => { + cancelled = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps -- `codes` comes + // from a server-fetched, 60s-ISR-cached layout prop; it's stable for + // the lifetime of this component in practice, and Klaro.setup() isn't + // meant to be called more than once per page load anyway. + }, []); + + return null; +} diff --git a/app/components/TrackingScripts.tsx b/app/components/TrackingScripts.tsx deleted file mode 100644 index 40d30d5..0000000 --- a/app/components/TrackingScripts.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client"; - -import Script from "next/script"; -import { useConsent } from "../lib/useConsent"; -import type { TrackingCode } from "../lib/payload"; - -// Renders one next/script per active tracking code, but only once consent -// actually allows it — `necessary` always renders (nothing in that -// category exists yet, but the field exists for future use, e.g. a -// consent-management/CMP script itself); `analytics`/`marketing` both gate -// on the same single useConsent() flag (see that hook's own comment on why -// there's no separate marketing toggle yet). Fetched server-side -// (getTrackingCodes(), passed in as a prop from layout.tsx) since this -// component itself is a client component and can't call that fetcher -// directly without losing the 60s ISR cache. -export function TrackingScripts({ codes }: { codes: TrackingCode[] }) { - const { consent, loaded } = useConsent(); - if (!loaded) return null; - - const allowed = codes.filter((code) => code.consentCategory === "necessary" || consent?.analytics === true); - - return ( - <> - {allowed.map((code) => { - if (code.provider === "google-analytics" && code.measurementId) { - return ( -
- -
- ); - } - if (code.provider === "facebook-pixel" && code.pixelId) { - return ( - - ); - } - if (code.provider === "google-tag-manager" && code.containerId) { - return ( - - ); - } - // 'other' — the only provider whose script content is admin- - // supplied rather than a fixed loader snippet. Trusted deliberately - // (see TrackingCodes.ts's own admin.description) — only reachable - // by someone with backend admin access in the first place. - if (code.provider === "other" && code.customScript) { - return