feat: Kaufjourney-Optimierungen fuer Kund:innen (Shop, Produktseite, Warenkorb, Checkout)

Konkrete Reibungspunkte entlang der Kauf-Journey behoben:
- Shop-Suche als sichtbares, editierbares Feld im Filter-Formular statt nur
  in der kleinen Kopfzeilen-Suche — Suche/Kategorie/Sortierung/Verfuegbarkeit
  lassen sich jetzt in beide Richtungen kombinieren, ohne sich gegenseitig
  zurueckzusetzen; plus "Suche zuruecksetzen"-Link.
- Versandkosten- und Lieferzeit-Hinweis direkt auf der Produktdetailseite
  (bisher erst im Warenkorb/Checkout sichtbar).
- Produktgalerie per Touch-Wisch bedienbar, nicht nur ueber die
  Vorschau-Kacheln (mobile Kaufentscheidung).
- Immer sichtbare mobile Kaufleiste auf der Produktseite, damit der
  Warenkorb-Button auf langen Seiten (Bewertungen, verwandte Produkte) nicht
  wieder erst erscrollt werden muss.
- Live-Lagerbestand (Ausverkauft/Nur noch N) jetzt auch im Cart-Drawer und
  auf der Warenkorb-Seite sichtbar, nicht nur auf der Produktkarte — neuer
  Endpunkt /api/shop/cart-stock + useCartStock-Hook.
- Checkout- und Adressformulare: autoComplete/inputMode/type auf allen
  Feldern (given-name, tel, postal-code, ...) fuer Mobil-Autofill und
  passende Bildschirmtastaturen.
- Bestellpositionen in "Meine Bestellungen" und der Bestellverfolgung
  verlinken jetzt zur Produktseite (samt Bewertungs-Anker) — bisher war die
  Bewertungsfunktion nach dem Kauf nur durch manuelles Zurueeknavigieren
  auffindbar.

Neue Tests fuer die Lagerbestand-Hinweise im Warenkorb; tsc/vitest/build
bleiben gruen (142 Tests).
This commit is contained in:
Nick Szaunig committed 2026-09-05 18:02:38 +02:00
1 parent 54919d82ed
commit 0b2196460a
16 files changed
+445 -84

No files matched your search

+31 -3
View File
@@ -1,8 +1,12 @@
"use client";
import { useState } from "react";
import { useRef, useState, type TouchEvent } from "react";
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 } {
@@ -29,13 +33,37 @@ export function ProductGallery({ images, productName }: 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<number | null>(null);
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 (
<div>
<div className="aspect-[4/3] overflow-hidden">
<div
className="aspect-[4/3] overflow-hidden touch-pan-y"
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{isImageSlide(active) ? (
// eslint-disable-next-line @next/next/no-img-element -- externe Bildquelle noch unbekannt, next/image braucht dafuer eine feste remotePatterns-Konfiguration.
<img src={active.src} alt={productName} className="w-full h-full object-cover" />
<img src={active.src} alt={productName} className="w-full h-full object-cover" draggable={false} />
) : (
<PlaceholderImage tone={active.tone} label={active.label} className="w-full h-full" />
)}