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>
This commit is contained in:
@@ -1169,3 +1169,38 @@ export async function getSeoSettings(): Promise<SeoSettings> {
|
||||
(typeof doc.seoDefaultOgImage === "object" && doc.seoDefaultOgImage?.url) || SEO_SETTINGS_FALLBACK.defaultOgImage,
|
||||
};
|
||||
}
|
||||
|
||||
export type TrackingCode = {
|
||||
id: number;
|
||||
provider: "google-analytics" | "facebook-pixel" | "google-tag-manager" | "other";
|
||||
consentCategory: "necessary" | "analytics" | "marketing";
|
||||
measurementId: string | null;
|
||||
pixelId: string | null;
|
||||
containerId: string | null;
|
||||
customScript: string | null;
|
||||
};
|
||||
|
||||
// Public read (TrackingCodes.ts's own access.read: () => true), unlike
|
||||
// most of this file's other Company-Settings-adjacent fetchers — no
|
||||
// x-order-service-secret header needed. Only `active: true` rows, since
|
||||
// TrackingScripts.tsx has no reason to even know an inactive one exists.
|
||||
// Consent-gating itself happens in TrackingScripts.tsx, not here — this
|
||||
// fetcher runs server-side (no cookie access), the gating decision is a
|
||||
// client-only concern (useConsent()).
|
||||
export async function getTrackingCodes(): Promise<TrackingCode[]> {
|
||||
const params = new URLSearchParams({
|
||||
"where[tenant.slug][equals]": TENANT_SLUG,
|
||||
"where[active][equals]": "true",
|
||||
depth: "0",
|
||||
limit: "50",
|
||||
});
|
||||
const res = await fetch(`${PAYLOAD_URL}/api/tracking-codes?${params}`, {
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`getTrackingCodes: Payload returned ${res.status} ${res.statusText}`);
|
||||
return [];
|
||||
}
|
||||
const data: { docs?: TrackingCode[] } = await res.json();
|
||||
return Array.isArray(data.docs) ? data.docs : [];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"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 };
|
||||
}
|
||||
Reference in New Issue
Block a user