Restyle Klaro to actually match the brand, not just its accent colors
CSS-var overrides alone (colors, corner position) still left Klaro's own border/shadow/spacing/typography, which read as an obviously bolted-on library widget next to this site's hand-designed components. New KlaroTheme component overrides Klaro's real DOM classnames directly (confirmed against kiprotect/klaro's own scss source) — kills the default border, adds this site's own soft-shadow card look, restyles every button, uses the site's actual serif/sans font pairing. Also warms up the notice/modal copy. Updates both READMEs with the tracking-codes/Klaro/back-in-stock/ settings-search/DHL-shipment-label work from this session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1796,6 +1796,64 @@ session (low-stock digest, stale-unverified-accounts report, weekly revenue
|
||||
report, expired-discount-code cleanup) needed no monitor changes of their
|
||||
own; see the Payload README's "Jobs Queue" section.
|
||||
|
||||
## Cookie consent, tracking codes & back-in-stock notifications
|
||||
|
||||
**Cookie consent** (added 2026-08-01) uses **Klaro** (`kiprotect/klaro`,
|
||||
npm, self-hosted, open source) — a first hand-rolled `CookieBanner.tsx`/
|
||||
`useConsent.ts`/`TrackingScripts.tsx` trio shipped the same day, then was
|
||||
replaced with Klaro after real per-service consent lists and bundled
|
||||
legally-grade German translations were judged worth the swap over
|
||||
building all of that by hand. `no @types/klaro` package exists —
|
||||
`types/klaro.d.ts` declares only the small surface actually used
|
||||
(`setup`/`show`).
|
||||
|
||||
- `app/lib/klaroConfig.ts` — builds Klaro's config object dynamically
|
||||
from the Payload backend's `tracking-codes` collection (see that
|
||||
repo's README): one Klaro "service" per row, `purposes: [consentCategory]`.
|
||||
Brand colors applied via Klaro's CSS-custom-property `styling`
|
||||
overrides (`green1`/`red1`/`blue1`/`dark1`/`light1` etc. — confusingly
|
||||
named: `dark1`/`light1` are the notice's background/text colors, not a
|
||||
dark-mode switch; `blue1` colors the modal's "Einstellungen" button and
|
||||
is easy to miss since it only shows once the modal itself opens, not
|
||||
just the notice). Theme: compact bottom-left corner notice
|
||||
(`theme: ["light", "bottom", "left"]`, `notice-max-width: 380px`), not
|
||||
Klaro's full-width `wide` default.
|
||||
- `app/lib/loadTrackingCode.ts` — the actual `<script>` injection side
|
||||
effect (GA4/Facebook Pixel/GTM fixed loader snippets, `other` renders
|
||||
the admin-supplied raw script), wired via each Klaro service's
|
||||
`callback(consent)` in `klaroConfig.ts`, not declaratively rendered.
|
||||
- `app/components/KlaroConsentManager.tsx` — mounts Klaro client-side
|
||||
(dynamic `import()`, not a static import — Klaro touches
|
||||
`window`/`document` at module-eval time, not SSR-safe) inside
|
||||
`app/layout.tsx`. Renders nothing itself if there are zero active
|
||||
`tracking-codes` rows (no banner needed with nothing to consent to).
|
||||
Also renders a **persistent bottom-left cookie icon button** (fixed
|
||||
position, calls `Klaro.show()`) so a visitor can reopen the consent
|
||||
manager any time, not just on first visit.
|
||||
- **Testing note**: a hard reload does NOT clear the `klaro-consent`
|
||||
cookie (180-day expiry) — that's correct/expected behavior, not a bug.
|
||||
To re-trigger the first-visit notice while testing, use the reopen
|
||||
button, delete the cookie in DevTools, or an incognito window.
|
||||
|
||||
**Back-in-stock notifications** — `NotifyMeForm.tsx` replaces the
|
||||
disabled Add-to-cart button's slot entirely (not stacked below it) once a
|
||||
product/variant is out of stock, POSTing to `/api/stock-notifications`
|
||||
→ `app/lib/stockNotifications.ts` → the backend's `stock-notifications`
|
||||
collection. Always shows the email input + submit button directly (no
|
||||
collapse-to-reveal step). `AddToCartButton`/`AddToCartInlineButton` both
|
||||
carry a `numericId` prop (`product.numericId`, the real Payload id — NOT
|
||||
their own `id`/`productId` props, which are the commerce slug) threaded
|
||||
through for this, same split `WishlistButton` already used.
|
||||
`ProductGrid.tsx`/`MerklisteGrid.tsx`/`RelatedProducts.tsx` all use
|
||||
`items-start` on their grid (not CSS Grid's stretch default) so an
|
||||
out-of-stock card being taller than its siblings doesn't push their own
|
||||
buttons out of alignment.
|
||||
|
||||
The email itself (`renderBackInStockHtml` in `app/lib/emailTemplates.ts`)
|
||||
has its own dedicated Live Preview renderer, not the shared generic
|
||||
order-status one — there's no order for this email type, so no fake
|
||||
"Bestellnummer" line; CTA is "Zum Produkt", not "Bestellung ansehen".
|
||||
|
||||
## Tests
|
||||
|
||||
`npm run test:unit` (Vitest, `node` environment, no jsdom/Next.js runtime
|
||||
|
||||
@@ -20,6 +20,66 @@ import { loadTrackingCode } from "../lib/loadTrackingCode";
|
||||
// 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.
|
||||
//
|
||||
// `buildKlaroConfig`'s `styling` CSS-var overrides (brand colors, corner
|
||||
// position) only get Klaro so far — its bundled klaro.css still draws its
|
||||
// own border/shadow/spacing/typography, which read as an obviously
|
||||
// bolted-on library widget next to this site's own hand-designed
|
||||
// components (NewsletterModal.tsx, NotifyMeForm.tsx, etc.). The <style>
|
||||
// block below is a real CSS override pass against Klaro's actual DOM
|
||||
// classnames (confirmed against kiprotect/klaro's own src/scss/*.scss,
|
||||
// not guessed) — same "plain unlayered <style> tag wins the cascade"
|
||||
// approach the Payload admin's own AdminUIStyles.tsx uses, `!important`
|
||||
// added only where needed to beat klaro.css's own rules of otherwise-equal
|
||||
// specificity. Kills Klaro's default border entirely (replaced with a
|
||||
// soft shadow, matching this site's own card language) and restyles
|
||||
// every button to the site's actual rounded/weighted look instead of
|
||||
// Klaro's generic flat rectangles.
|
||||
function KlaroTheme() {
|
||||
return (
|
||||
<style>{`
|
||||
.klaro, .klaro * { box-sizing: border-box; }
|
||||
.klaro .cookie-notice, .klaro .cookie-modal {
|
||||
border: none !important;
|
||||
box-shadow: 0 20px 44px -14px rgba(26,26,24,0.22), 0 4px 14px rgba(26,26,24,0.07) !important;
|
||||
border-radius: 18px !important;
|
||||
font-family: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
|
||||
}
|
||||
.klaro .cookie-notice .cn-body { padding: 22px 24px !important; }
|
||||
.klaro .cookie-notice p, .klaro .cookie-modal p {
|
||||
font-size: 14px !important;
|
||||
line-height: 1.6 !important;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
.klaro h1, .klaro h2 {
|
||||
font-family: var(--font-lora), Georgia, serif !important;
|
||||
font-weight: 700 !important;
|
||||
letter-spacing: -0.01em !important;
|
||||
}
|
||||
.klaro .cm-btn {
|
||||
border: none !important;
|
||||
border-radius: 8px !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 13px !important;
|
||||
padding: 10px 18px !important;
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
.klaro .cm-btn:hover { opacity: 0.88; }
|
||||
.klaro .cm-btn:active { transform: scale(0.97); }
|
||||
.klaro .cm-btn:focus-visible { outline: 2px solid #f6a701 !important; outline-offset: 2px; }
|
||||
.klaro a.cm-link, .klaro .cookie-notice a, .klaro .cookie-modal a {
|
||||
color: #a06b00 !important;
|
||||
text-decoration: underline !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
.klaro select, .klaro .cm-list-input + label {
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
`}</style>
|
||||
);
|
||||
}
|
||||
|
||||
export function KlaroConsentManager({ codes }: { codes: TrackingCode[] }) {
|
||||
// Kept in state (not just called once) so the persistent reopen button
|
||||
// below can call Klaro.show() itself, any time after the initial
|
||||
@@ -48,24 +108,29 @@ export function KlaroConsentManager({ codes }: { codes: TrackingCode[] }) {
|
||||
// meant to be called more than once per page load anyway.
|
||||
}, []);
|
||||
|
||||
if (!klaroModule) return null;
|
||||
if (codes.length === 0) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => klaroModule.show()}
|
||||
aria-label="Cookie-Einstellungen öffnen"
|
||||
title="Cookie-Einstellungen"
|
||||
className="fixed bottom-5 left-5 z-40 flex h-11 w-11 items-center justify-center rounded-full border border-border bg-bg-base shadow-md hover:border-brand transition-colors"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="1.6" />
|
||||
<circle cx="9" cy="9.5" r="1.1" fill="currentColor" />
|
||||
<circle cx="14" cy="8.5" r="1" fill="currentColor" />
|
||||
<circle cx="15" cy="13.5" r="1.1" fill="currentColor" />
|
||||
<circle cx="10.5" cy="14.5" r="1" fill="currentColor" />
|
||||
<circle cx="12" cy="11" r="0.9" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<>
|
||||
<KlaroTheme />
|
||||
{klaroModule && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => klaroModule.show()}
|
||||
aria-label="Cookie-Einstellungen öffnen"
|
||||
title="Cookie-Einstellungen"
|
||||
className="fixed bottom-5 left-5 z-40 flex h-11 w-11 items-center justify-center rounded-full border border-border bg-bg-base shadow-md hover:border-brand transition-colors"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="1.6" />
|
||||
<circle cx="9" cy="9.5" r="1.1" fill="currentColor" />
|
||||
<circle cx="14" cy="8.5" r="1" fill="currentColor" />
|
||||
<circle cx="15" cy="13.5" r="1.1" fill="currentColor" />
|
||||
<circle cx="10.5" cy="14.5" r="1" fill="currentColor" />
|
||||
<circle cx="12" cy="11" r="0.9" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+13
-17
@@ -63,11 +63,16 @@ export function buildKlaroConfig(codes: TrackingCode[], onAccept: (code: Trackin
|
||||
// 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 actual brand-color pass: dark1/light1 are (confusingly
|
||||
// named) the notice's background/text colors, not a dark-mode toggle;
|
||||
// green1 colors the accept button, red1 the decline button (set
|
||||
// neutral gray, not alarming red — this site's own secondary buttons
|
||||
// are plain-bordered, never red, see NotifyMeForm.tsx).
|
||||
// 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
|
||||
@@ -78,18 +83,9 @@ export function buildKlaroConfig(codes: TrackingCode[], onAccept: (code: Trackin
|
||||
light3: "#1a1a18",
|
||||
green1: "#f6a701", // --color-brand — accept button
|
||||
green2: "#d99000",
|
||||
red1: "#e5e0d8", // decline button — neutral bordered look, not red
|
||||
red2: "#d1cabf",
|
||||
// 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/red did, easy to miss since it only
|
||||
// shows once the modal (not just the notice) is open.
|
||||
blue1: "#e5e0d8",
|
||||
blue2: "#6b6b69",
|
||||
"button-text-color": "#1a1a18",
|
||||
"border-radius": "10px",
|
||||
"border-width": "1px",
|
||||
"font-family": "inherit",
|
||||
"notice-max-width": "380px",
|
||||
},
|
||||
storageMethod: "cookie",
|
||||
@@ -103,11 +99,11 @@ export function buildKlaroConfig(codes: TrackingCode[], onAccept: (code: Trackin
|
||||
translations: {
|
||||
de: {
|
||||
consentModal: {
|
||||
title: "Cookie-Einstellungen",
|
||||
description: "Hier kannst du einsehen und anpassen, welche Cookies/Skripte diese Seite verwendet.",
|
||||
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 nutzen Cookies, um diese Seite zu betreiben und ihre Nutzung zu verstehen.",
|
||||
description: "Wir verwenden ein paar Cookies, damit diese Seite reibungslos läuft und wir sehen, was bei euch gut ankommt.",
|
||||
learnMore: "Einstellungen",
|
||||
},
|
||||
purposes: {
|
||||
|
||||
Reference in New Issue
Block a user