Files
einfach-produktiv/app/components/CookieBanner.tsx
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

61 lines
2.4 KiB
TypeScript

"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 (
<AnimatePresence>
{visible && (
<motion.div
initial={{ y: 80, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 80, opacity: 0 }}
transition={{ duration: 0.25, ease: "easeOut" }}
role="dialog"
aria-label="Cookie-Einstellungen"
className="fixed inset-x-0 bottom-0 z-50 border-t border-border bg-bg-base px-4 py-5 shadow-[0_-4px_24px_rgba(0,0,0,0.06)] sm:px-6"
>
<div className="mx-auto flex max-w-5xl flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<p className="text-body-sm text-text-primary">
Wir nutzen Cookies, um diese Seite zu betreiben und ihre Nutzung zu verstehen. Mehr dazu in unserer{" "}
<Link href="/datenschutz" className="underline hover:text-brand">
Datenschutzerklärung
</Link>
.
</p>
<div className="flex shrink-0 gap-3">
<button
type="button"
onClick={() => setConsent({ analytics: false })}
className="flex-1 rounded-sm border border-border px-5 py-2.5 text-body-sm font-semibold text-text-primary transition-colors hover:border-brand sm:flex-none"
>
Ablehnen
</button>
<button
type="button"
onClick={() => setConsent({ analytics: true })}
className="flex-1 rounded-sm bg-brand px-5 py-2.5 text-body-sm font-bold text-text-primary transition-colors hover:bg-brand-hover sm:flex-none"
>
Akzeptieren
</button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
);
}