From 0b2196460a62e9399eaba31429b59a199d282d8f Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Sat, 5 Sep 2026 18:02:38 +0200 Subject: [PATCH] feat: Kaufjourney-Optimierungen fuer Kund:innen (Shop, Produktseite, Warenkorb, Checkout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- src/app/api/shop/cart-stock/route.ts | 43 ++++++++++++ src/app/bestellung-verfolgen/page.tsx | 5 +- src/app/checkout/checkout-form.tsx | 63 ++++++++++++++++-- src/app/honig/[slug]/page.tsx | 37 ++++++++--- src/app/konto/adressen/page.tsx | 32 +++++++-- src/app/konto/bestellungen/page.tsx | 5 +- src/app/shop/page.tsx | 28 ++++++-- src/app/warenkorb/warenkorb-content.test.tsx | 35 +++++++++- src/app/warenkorb/warenkorb-content.tsx | 66 +++++++++++-------- src/components/product-gallery.tsx | 34 +++++++++- src/components/site-chrome.tsx | 62 +++++++++++------- src/components/sticky-add-to-cart-bar.tsx | 69 ++++++++++++++++++++ src/hooks/use-cart-stock.ts | 41 ++++++++++++ src/i18n/dictionaries/de.ts | 3 + src/i18n/dictionaries/en.ts | 3 + src/i18n/dictionary-type.ts | 3 + 16 files changed, 445 insertions(+), 84 deletions(-) create mode 100644 src/app/api/shop/cart-stock/route.ts create mode 100644 src/components/sticky-add-to-cart-bar.tsx create mode 100644 src/hooks/use-cart-stock.ts diff --git a/src/app/api/shop/cart-stock/route.ts b/src/app/api/shop/cart-stock/route.ts new file mode 100644 index 0000000..d7ec011 --- /dev/null +++ b/src/app/api/shop/cart-stock/route.ts @@ -0,0 +1,43 @@ +import { NextResponse } from "next/server"; +import { getProductsByIds } from "@/lib/data"; +import { rateLimit, getClientIp } from "@/lib/rate-limit"; + +const MAX_IDS_PER_REQUEST = 50; +const STOCK_ATTEMPTS_PER_WINDOW = 60; +const STOCK_WINDOW_MS = 60 * 1000; + +/** + * Aktueller Lagerbestand fuer eine Liste von Produkt-IDs — der Warenkorb + * (Zustand/localStorage) kennt nur den Stand beim letzten "Hinzufuegen", + * nicht den aktuellen. Cart-Drawer und Warenkorb-Seite fragen das hierueber + * live ab, damit "Ausverkauft"/"Nur noch N"-Hinweise auch dort erscheinen, + * nicht nur auf der Produktkarte/-detailseite (siehe product-card.tsx). + * Rein oeffentliche, bereits im Shop sichtbare Bestandsdaten — keine + * Authentifizierung noetig, aber wie jeder andere oeffentliche Endpunkt + * rate-limitiert. + */ +export async function GET(request: Request) { + const ip = await getClientIp(); + if (!rateLimit(`cart-stock:${ip}`, STOCK_ATTEMPTS_PER_WINDOW, STOCK_WINDOW_MS)) { + return NextResponse.json({ stock: {} }, { status: 429 }); + } + + const idsParam = new URL(request.url).searchParams.get("ids") ?? ""; + const ids = idsParam + .split(",") + .map((id) => id.trim()) + .filter(Boolean) + .slice(0, MAX_IDS_PER_REQUEST); + + if (ids.length === 0) { + return NextResponse.json({ stock: {} }); + } + + const products = await getProductsByIds(ids); + const stock: Record = {}; + for (const product of products) { + stock[product.id] = product.isActive ? (product.inventory?.quantity ?? 0) : 0; + } + + return NextResponse.json({ stock }); +} diff --git a/src/app/bestellung-verfolgen/page.tsx b/src/app/bestellung-verfolgen/page.tsx index b3639f5..d1fc6db 100644 --- a/src/app/bestellung-verfolgen/page.tsx +++ b/src/app/bestellung-verfolgen/page.tsx @@ -104,7 +104,10 @@ export default async function TrackOrderPage(props: PageProps<"/bestellung-verfo diff --git a/src/app/checkout/checkout-form.tsx b/src/app/checkout/checkout-form.tsx index 92b48de..94665d3 100644 --- a/src/app/checkout/checkout-form.tsx +++ b/src/app/checkout/checkout-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, type FormEvent } from "react"; +import { useState, type FormEvent, type HTMLAttributes } from "react"; import { useRouter } from "next/navigation"; import { Container, SectionHeading, Button } from "@/components/ui"; import { TrustBadges } from "@/components/trust-badges"; @@ -115,15 +115,37 @@ export function CheckoutForm({
- - + +
- +
@@ -158,16 +180,35 @@ export function CheckoutForm({ name="street" label={t.checkoutPage.streetLabel} defaultValue={initialValues?.street} + autoComplete="street-address" required />
- - + +
) : null} - +