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

+38 -24
View File
@@ -4,6 +4,7 @@ import Link from "next/link";
import { useState, useEffect, useRef, type RefObject } from "react";
import { AnimatePresence, motion } from "motion/react";
import { useCartStore, cartItemCount, cartTotalCents } from "@/store/cart-store";
import { useCartStock } from "@/hooks/use-cart-stock";
import { formatPrice } from "@/lib/format";
import { mainNav } from "@/lib/site-config";
import { Container, LinkButton } from "@/components/ui";
@@ -417,6 +418,10 @@ export function CartDrawer() {
const removeItem = useCartStore((s) => s.removeItem);
const total = cartTotalCents(items);
const asideRef = useRef<HTMLElement>(null);
// Live-Bestand fuer die Positionen im Drawer — ohne das wuerde ein
// zwischenzeitlich ausverkaufter Artikel hier unauffaellig weiter als
// normal bestellbar erscheinen (siehe use-cart-stock.ts).
const stock = useCartStock(items.map((item) => item.productId));
useEscapeKey(isOpen, closeCart);
useFocusTrap(isOpen, asideRef);
@@ -457,31 +462,40 @@ export function CartDrawer() {
</p>
) : (
<ul className="divide-y divide-ink/10">
{items.map((item) => (
<li key={item.productId} className="py-4 flex gap-4">
<CartItemThumbnail image={item.image} className="w-16 h-16 flex-shrink-0 rounded-sm" />
<div className="flex-1">
<p className="text-sm font-medium text-ink">{item.name}</p>
<p className="text-xs text-ink/65">{item.weightGrams} g</p>
<div className="flex items-center gap-3 mt-2">
<QuantityStepper
quantity={item.quantity}
onChange={(quantity) => setQuantity(item.productId, quantity)}
size="sm"
/>
<button
onClick={() => removeItem(item.productId)}
className="text-xs text-ink/65 hover:text-ink underline"
>
{t.cart.remove}
</button>
{items.map((item) => {
const itemStock = stock?.[item.productId];
return (
<li key={item.productId} className="py-4 flex gap-4">
<CartItemThumbnail image={item.image} className="w-16 h-16 flex-shrink-0 rounded-sm" />
<div className="flex-1">
<p className="text-sm font-medium text-ink">{item.name}</p>
<p className="text-xs text-ink/65">{item.weightGrams} g</p>
{itemStock !== undefined && itemStock === 0 ? (
<p className="mt-1 text-xs text-red-700">{t.product.outOfStock}</p>
) : itemStock !== undefined && itemStock <= 5 ? (
<p className="mt-1 text-xs text-honey-dark">{t.product.onlyLeft(itemStock)}</p>
) : null}
<div className="flex items-center gap-3 mt-2">
<QuantityStepper
quantity={item.quantity}
onChange={(quantity) => setQuantity(item.productId, quantity)}
max={itemStock ?? 10}
size="sm"
/>
<button
onClick={() => removeItem(item.productId)}
className="text-xs text-ink/65 hover:text-ink underline"
>
{t.cart.remove}
</button>
</div>
</div>
</div>
<p className="text-sm text-ink">
{formatPrice(item.priceCents * item.quantity)}
</p>
</li>
))}
<p className="text-sm text-ink">
{formatPrice(item.priceCents * item.quantity)}
</p>
</li>
);
})}
</ul>
)}
</div>