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

+24 -4
View File
@@ -116,11 +116,25 @@ export default async function ShopPage(props: PageProps<"/shop">) {
<div className="mt-6 flex flex-wrap items-center gap-4">
{/* GET-Formular statt Client-Component: funktioniert ohne JavaScript,
die Auswahl wird per Seiten-Neuladen als Query-Parameter uebernommen. */}
<form action="/shop" method="GET" className="flex items-center gap-2 text-sm text-ink/70">
{query ? <input type="hidden" name="q" value={query} /> : null}
die Auswahl wird per Seiten-Neuladen als Query-Parameter uebernommen.
Die Suche ist bewusst Teil DIESES Formulars (statt nur der kleinen
Kopfzeilen-Suche): so werden Kategorie/Verfuegbarkeit-Filter beim
Aendern der Suche innerhalb der Shop-Seite nicht verworfen — anders
als bei der Kopfzeilen-Suche, die immer neu auf /shop startet. */}
<form action="/shop" method="GET" className="flex flex-wrap items-center gap-2 text-sm text-ink/70">
{category ? <input type="hidden" name="kategorie" value={category} /> : null}
{onlyAvailable ? <input type="hidden" name="verfuegbar" value="1" /> : null}
<label htmlFor="shop-search" className="sr-only">
{t.shop.searchInputLabel}
</label>
<input
id="shop-search"
type="search"
name="q"
defaultValue={query ?? ""}
placeholder={t.nav.searchPlaceholder}
className="border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest"
/>
<label htmlFor="sortierung">{t.shop.sortLabel}</label>
<select
id="sortierung"
@@ -157,7 +171,13 @@ export default async function ShopPage(props: PageProps<"/shop">) {
{query ? (
<p className="mt-6 text-sm text-ink/70">
{t.shop.searchResults(query, products.length)}
{t.shop.searchResults(query, products.length)}{" "}
<Link
href={buildHref({ kategorie: category, sortierung: sort, verfuegbar: onlyAvailable ? "1" : undefined })}
className="underline hover:text-forest"
>
{t.shop.clearSearch}
</Link>
</p>
) : null}
<h2 className="sr-only">{t.shop.productsHeading}</h2>