"use client"; import { useRef, useState, type TouchEvent } from "react"; import Image from "next/image"; import { PlaceholderImage } from "@/components/placeholder-image"; // Mindest-Wischstrecke in Pixeln, ab der ein Touch-Wisch als Bildwechsel // gilt statt als (vertikales) Scrollen oder ein leichtes Zittern der Hand. const SWIPE_THRESHOLD_PX = 40; type Slide = { src: string } | { tone: "forest" | "cream" | "honey"; label: string }; function isImageSlide(slide: Slide): slide is { src: string } { return "src" in slide; } // Solange fuer ein Produkt keine echte Produktfotografie hinterlegt ist // (admin/produkte -> Bilder, siehe AdminImageListField), zeigt die Galerie // diese vier Platzhalter-Kacheln an — sobald echte Bild-URLs gepflegt // werden, ersetzen sie diese Kacheln automatisch (siehe Verzweigung unten). const PLACEHOLDER_SLIDES: Slide[] = [ { tone: "honey", label: "Glas frontal" }, { tone: "cream", label: "Glas seitlich" }, { tone: "forest", label: "Bienenstand" }, { tone: "honey", label: "Landschaft" }, ]; interface ProductGalleryProps { images: string[]; productName: string; /** Kurzbeschreibung (z.B. Geschmacksnoten) fuer aussagekraeftigeren Alt-Text * auf dem Hauptbild — reiner Produktname allein ist fuer Screenreader UND * Bildersuche weniger nuetzlich als Name + kurze inhaltliche Einordnung. */ description?: string; } export function ProductGallery({ images, productName, description }: ProductGalleryProps) { const slides: Slide[] = images.length > 0 ? images.map((src) => ({ src })) : PLACEHOLDER_SLIDES; const [activeIndex, setActiveIndex] = useState(0); const active = slides[activeIndex] ?? slides[0]; const touchStartX = useRef(null); const mainImageAlt = description ? `${productName} – ${description}` : productName; function handleTouchStart(event: TouchEvent) { touchStartX.current = event.touches[0].clientX; } // Wischen auf dem Hauptbild wechselt das Bild — auf dem Handy ist das die // erwartete Bediengeste fuer eine Produktgalerie, nicht nur ein Tap auf die // (auf kleinen Bildschirmen ohnehin winzigen) Vorschau-Kacheln darunter. function handleTouchEnd(event: TouchEvent) { if (touchStartX.current === null || slides.length <= 1) return; const deltaX = event.changedTouches[0].clientX - touchStartX.current; touchStartX.current = null; if (Math.abs(deltaX) < SWIPE_THRESHOLD_PX) return; setActiveIndex((current) => { const direction = deltaX < 0 ? 1 : -1; return (current + direction + slides.length) % slides.length; }); } return (
{isImageSlide(active) ? ( // Hochgeladene Produktbilder liegen immer same-origin unter // /uploads/... (siehe uploads.ts/isValidUploadPath) — next/image // braucht dafuer KEINEN remotePatterns-Eintrag (nur externe // Bildquellen brauchen den), siehe auch content-image.tsx. Als // wahrscheinlichstes LCP-Element der Produktseite sofort und mit // hoher Netzwerk-Prioritaet geladen, statt erst beim Scannen des // Bild-Tags entdeckt zu werden. Bewusst NICHT ueber die Prop // "priority": die ist seit Next.js 16 abgekuendigt und erzeugt nur // noch einen Preload-Link, ohne fetchpriority am zu setzen. {mainImageAlt} ) : ( )}
{slides.length > 1 ? (
{slides.map((slide, index) => ( ))}
) : null}
); }