Add product image gallery, harden Klaro border reset, remove attribution link
ProductGallery.tsx (main image + thumbnail strip) wired into TodoKartenHero.tsx, only replacing the static hero photo once a product actually has extra gallery photos — no visual change otherwise. Backend field: Products.gallery (see payload repo). Klaro's hard black border was still showing after the previous color-only pass — the targeted border:none rule wasn't enough, so this blanket-resets border/outline on every .klaro descendant and re-adds only the shadow this theme actually wants. Also disables the "Realisiert mit Klaro!" attribution link (BSD-3-Clause has no on-page-attribution requirement, Klaro's own disablePoweredBy flag covers this cleanly). Updates the frontend README with the gallery + Klaro fixes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,9 +38,14 @@ import { loadTrackingCode } from "../lib/loadTrackingCode";
|
||||
function KlaroTheme() {
|
||||
return (
|
||||
<style>{`
|
||||
.klaro, .klaro * { box-sizing: border-box; }
|
||||
/* Blanket reset first — the previous pass only targeted
|
||||
.cookie-notice/.cookie-modal directly and a hard black border
|
||||
still showed up in production (screenshot-confirmed), so every
|
||||
descendant gets border/outline stripped here regardless of which
|
||||
specific Klaro rule was actually drawing it; the two rules below
|
||||
re-add exactly the borders this theme actually wants. */
|
||||
.klaro, .klaro * { box-sizing: border-box; border: 0 !important; outline: 0 !important; }
|
||||
.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;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
/**
|
||||
* Main image + thumbnail strip, swappable on click. `image` is always
|
||||
* slide zero, `gallery` (Products.gallery, optional/empty for most
|
||||
* products) fills in the rest. Renders as a single plain image with no
|
||||
* thumbnail row at all when `gallery` is empty — the common case, and
|
||||
* exactly today's pre-gallery appearance, no behavior change for any
|
||||
* product that hasn't opted in. Plain divs/buttons, no carousel package —
|
||||
* same "no charting/UI-library dependency for a simple case" reasoning as
|
||||
* OrderQueueWidget.tsx's own OrderSparkline.
|
||||
*/
|
||||
export function ProductGallery({ image, gallery, alt }: { image: string; gallery: string[]; alt: string }) {
|
||||
const slides = [image, ...gallery];
|
||||
const [current, setCurrent] = useState(0);
|
||||
|
||||
if (slides.length <= 1) {
|
||||
return (
|
||||
<div className="relative w-full aspect-[4/3.1] overflow-hidden rounded-md border border-border bg-bg-base">
|
||||
<Image src={image} alt={alt} fill sizes="(min-width: 860px) 55vw, 100vw" className="object-cover" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function next() {
|
||||
setCurrent((c) => (c + 1) % slides.length);
|
||||
}
|
||||
function prev() {
|
||||
setCurrent((c) => (c - 1 + slides.length) % slides.length);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3.5 w-full">
|
||||
<div className="relative w-full aspect-[4/3.1] overflow-hidden rounded-md border border-border bg-bg-base">
|
||||
<Image src={slides[current]} alt={alt} fill sizes="(min-width: 860px) 55vw, 100vw" className="object-cover" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={prev}
|
||||
aria-label="Vorheriges Bild"
|
||||
className="absolute left-3.5 top-1/2 -translate-y-1/2 flex h-9 w-9 items-center justify-center rounded-full bg-bg-base/90 text-text-primary shadow-md hover:bg-bg-base transition-colors"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" aria-hidden="true">
|
||||
<path d="M15 19l-7-7 7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={next}
|
||||
aria-label="Nächstes Bild"
|
||||
className="absolute right-3.5 top-1/2 -translate-y-1/2 flex h-9 w-9 items-center justify-center rounded-full bg-bg-base/90 text-text-primary shadow-md hover:bg-bg-base transition-colors"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" aria-hidden="true">
|
||||
<path d="M9 5l7 7-7 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<span className="absolute bottom-3.5 right-3.5 rounded-full bg-text-primary/60 px-2.5 py-1 text-label font-semibold text-white tabular-nums">
|
||||
{current + 1} / {slides.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2.5">
|
||||
{slides.map((src, i) => (
|
||||
<button
|
||||
key={src + i}
|
||||
type="button"
|
||||
onClick={() => setCurrent(i)}
|
||||
aria-label={`Bild ${i + 1} anzeigen`}
|
||||
aria-current={i === current}
|
||||
className={`relative h-[4.75rem] w-[4.75rem] shrink-0 overflow-hidden rounded-sm border-2 transition-opacity ${
|
||||
i === current ? "border-brand opacity-100" : "border-transparent opacity-70 hover:opacity-100"
|
||||
}`}
|
||||
>
|
||||
<Image src={src} alt="" fill sizes="76px" className="object-cover" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user