Files
einfach-produktiv/app/lib/klaroConfig.ts
T
Marco ac6c3d6b71 Add GoogleMapsEmbed.tsx — consent-gated Maps embed, prepared but unused
Uses Klaro's own built-in contextual-consent mechanism (not
loadTrackingCode.ts's script-injection path) — the iframe renders
with data-name="google-maps" and its real src; Klaro's DOM scan finds
it after mount and blanks/restores src based on that service's
consent state, showing its own placeholder (styled via
KlaroConsentManager.tsx's KlaroTheme) in the meantime.

No map embedded anywhere in the app yet — this is prep work only, per
explicit request ahead of an actual page needing one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-01 21:58:19 +00:00

162 lines
6.7 KiB
TypeScript

import type { TrackingCode } from "./payload";
// Klaro's own config shape (kiprotect/klaro, see node_modules/klaro's
// dist/config.js for the fully-annotated reference) — only the fields we
// actually set are typed here, not Klaro's full surface.
export type KlaroConfig = {
version: number;
elementID: string;
styling: Record<string, string | string[]>;
storageMethod: "cookie";
cookieName: string;
cookieExpiresAfterDays: number;
default: boolean;
mustConsent: boolean;
acceptAll: boolean;
hideDeclineAll: boolean;
noticeAsModal: boolean;
disablePoweredBy: boolean;
translations: Record<string, Record<string, unknown>>;
services: {
name: string;
title: string;
purposes: string[];
required: boolean;
default: boolean;
onlyOnce: boolean;
callback: (consent: boolean) => void;
}[];
};
// "google-maps" is a fixed name, not id-based like every other provider —
// GoogleMapsEmbed.tsx has to hardcode a `data-name` matching this exact
// string for Klaro's own contextual-consent DOM scan
// (renderContextualConsentNotices in node_modules/klaro) to find it, and
// there's no per-row distinguishing ID the way GA/FB/GTM each have one
// (a Maps embed URL is per-page, not stored on this row at all — see
// TrackingCodes.ts's own field comment). Only ever create ONE
// google-maps row in the admin; a second one would collide on this name.
export function serviceName(code: TrackingCode): string {
if (code.provider === "google-maps") return "google-maps";
return `tracking-code-${code.id}`;
}
function providerTitle(code: TrackingCode): string {
switch (code.provider) {
case "google-analytics":
return "Google Analytics";
case "facebook-pixel":
return "Facebook Pixel";
case "google-tag-manager":
return "Google Tag Manager";
case "google-maps":
return "Google Maps";
default:
return "Sonstiges Tracking-Skript";
}
}
// `onAccept` is threaded in rather than importing loadTrackingCode.ts
// directly here — this module only builds a plain config object (no DOM
// access), kept separate from the actual script-injection side effect so
// it stays trivially testable/reusable if the loading mechanism ever
// changes.
export function buildKlaroConfig(codes: TrackingCode[], onAccept: (code: TrackingCode) => void): KlaroConfig {
const serviceTranslations: Record<string, { title: string }> = {};
for (const code of codes) serviceTranslations[serviceName(code)] = { title: providerTitle(code) };
return {
version: 1,
elementID: "klaro",
// 'light' + 'bottom' + 'left' — a compact corner notice (Klaro's own
// default max-width, ~400px), not a full-width bar. 'wide' (tried
// first) stretched it across nearly the whole viewport, which read as
// too dominant compared to a typical bottom-left corner CMP.
// Extra keys beyond `theme` override individual CSS custom properties
// (Klaro's injectStyles() applies any non-'theme' `styling` key as a
// `--<key>` var, see node_modules/klaro's own utils/styling.js) — this
// is the color pass; the border/shadow/typography pass lives in
// KlaroConsentManager.tsx's <KlaroTheme> instead (a real CSS override,
// needed because Klaro's own default border/spacing/font still read as
// an obviously-bolted-on library widget even with the right colors).
// dark1/light1 are (confusingly named) the notice's background/text
// colors, not a dark-mode toggle. green1 colors the accept button.
// blue1/blue2 color the "Einstellungen" (.cm-btn-info) button and
// in-modal links — Klaro's default blue clashed with the brand just as
// much as its default green did, easy to miss since it only shows
// once the modal (not just the notice) is open.
styling: {
theme: ["light", "bottom", "left"],
dark1: "#fffdf8", // --color-bg-paper — notice/modal background
dark2: "#e5e0d8", // --color-border
dark3: "#f3efe9",
light1: "#1a1a18", // --color-text-primary — notice/modal text
light2: "#6b6b69", // --color-text-muted
light3: "#1a1a18",
green1: "#f6a701", // --color-brand — accept button
green2: "#d99000",
blue1: "#e5e0d8",
blue2: "#6b6b69",
"button-text-color": "#1a1a18",
"notice-max-width": "380px",
},
storageMethod: "cookie",
cookieName: "klaro-consent",
cookieExpiresAfterDays: 180,
default: false,
mustConsent: false,
acceptAll: true,
hideDeclineAll: false,
noticeAsModal: false,
// Removes the "Realisiert mit Klaro!" attribution link in the modal —
// BSD-3-Clause has no on-page-attribution requirement (only source/
// binary copyright-notice retention), this is just the maintainers'
// own polite ask via a dedicated config flag, not a license term.
disablePoweredBy: true,
translations: {
de: {
consentModal: {
title: "Deine Cookie-Einstellungen",
description: "Hier siehst du genau, was wir einsetzen — und entscheidest frei, was du erlaubst. Nichts davon läuft, bevor du zustimmst.",
},
consentNotice: {
description: "Wir verwenden ein paar Cookies, damit diese Seite reibungslos läuft und wir sehen, was bei euch gut ankommt.",
learnMore: "Einstellungen",
},
purposes: {
necessary: "Notwendig",
analytics: "Analyse",
marketing: "Marketing",
content: "Inhalte",
},
...serviceTranslations,
},
},
services: codes.map((code) => ({
name: serviceName(code),
title: providerTitle(code),
purposes: [code.consentCategory],
required: code.consentCategory === "necessary",
default: code.consentCategory === "necessary",
// For "google-maps", `onAccept` below is a no-op (see
// loadTrackingCode.ts) — an embed's src isn't a script this app
// injects, Klaro's OWN contextual-consent DOM scan
// (renderContextualConsentNotices, runs as part of Klaro.render())
// finds any `data-name="google-maps"` iframe on the page and
// blanks/restores its `src` itself, purely by service name +
// consent state. This service entry's only job is telling Klaro
// that name exists and which purpose gates it.
//
// A restock/page-reload shouldn't re-fire the loader for a service
// the visitor already accepted in an earlier session within this
// same page load — Klaro still calls `callback` once per accepted
// service per page load either way, `onlyOnce` just governs repeat
// toggling within one session.
onlyOnce: true,
callback: (consent: boolean) => {
if (consent) onAccept(code);
},
})),
};
}