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

77 lines
3.7 KiB
TypeScript

"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 (
<div key={code.id}>
<Script src={`https://www.googletagmanager.com/gtag/js?id=${code.measurementId}`} strategy="afterInteractive" />
<Script id={`ga-init-${code.id}`} strategy="afterInteractive">
{`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${code.measurementId}');`}
</Script>
</div>
);
}
if (code.provider === "facebook-pixel" && code.pixelId) {
return (
<Script key={code.id} id={`fb-pixel-${code.id}`} strategy="afterInteractive">
{`!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '${code.pixelId}');
fbq('track', 'PageView');`}
</Script>
);
}
if (code.provider === "google-tag-manager" && code.containerId) {
return (
<Script key={code.id} id={`gtm-${code.id}`} strategy="afterInteractive">
{`(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${code.containerId}');`}
</Script>
);
}
// '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 <Script key={code.id} id={`custom-${code.id}`} strategy="afterInteractive" dangerouslySetInnerHTML={{ __html: code.customScript }} />;
}
return null;
})}
</>
);
}