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

+27 -10
View File
@@ -9,6 +9,7 @@ import { RecordProductView } from "@/components/record-product-view";
import { RecentlyViewedSection } from "@/components/recently-viewed-section";
import { StarRating } from "@/components/star-rating";
import { TrustBadges } from "@/components/trust-badges";
import { StickyAddToCartBar } from "@/components/sticky-add-to-cart-bar";
import { getProductBySlug, getRelatedProducts } from "@/lib/data";
import { formatPrice, formatPricePerKg, formatDate } from "@/lib/format";
import { siteConfig } from "@/lib/site-config";
@@ -16,6 +17,8 @@ import { getCompanyInfo } from "@/lib/company-info";
import { getCurrentCustomer } from "@/lib/auth";
import { getApprovedReviews, getReviewSummary, getCustomerReview, hasCustomerPurchased } from "@/lib/reviews";
import { isProductWishlisted } from "@/lib/wishlist";
import { getShippingTiers } from "@/lib/shipping";
import { resolveShippingCostCents } from "@/lib/shipping-calc";
import { submitReviewAction, toggleWishlistAction, subscribeStockNotificationAction } from "../actions";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
@@ -54,15 +57,22 @@ export default async function ProductDetailPage(
const lowStock = inStock && stock <= 5;
const customer = await getCurrentCustomer();
const [reviews, reviewSummary, relatedProducts, ownReview, canReview, isWishlisted, company] = await Promise.all([
getApprovedReviews(product.id),
getReviewSummary(product.id),
getRelatedProducts(product.id, product.category),
customer ? getCustomerReview(product.id, customer.id) : Promise.resolve(null),
customer ? hasCustomerPurchased(product.id, customer.id) : Promise.resolve(false),
customer ? isProductWishlisted(product.id, customer.id) : Promise.resolve(false),
getCompanyInfo(),
]);
const [reviews, reviewSummary, relatedProducts, ownReview, canReview, isWishlisted, company, shippingTiers] =
await Promise.all([
getApprovedReviews(product.id),
getReviewSummary(product.id),
getRelatedProducts(product.id, product.category),
customer ? getCustomerReview(product.id, customer.id) : Promise.resolve(null),
customer ? hasCustomerPurchased(product.id, customer.id) : Promise.resolve(false),
customer ? isProductWishlisted(product.id, customer.id) : Promise.resolve(false),
getCompanyInfo(),
getShippingTiers(),
]);
// "Ab"-Preis fuer eine einzelne Einheit — die Staffel, die beim Kauf genau
// dieses einen Produkts greifen wuerde. Macht Versandkosten schon auf der
// Produktseite sichtbar statt erst im Warenkorb/Checkout (Kaufentscheidung
// haengt bei einem kleinen Versandhandel oft direkt daran).
const baseShippingCents = resolveShippingCostCents(shippingTiers, 1);
const jsonLd = {
"@context": "https://schema.org",
@@ -110,8 +120,9 @@ export default async function ProductDetailPage(
};
return (
<div className="py-16">
<div className="py-16 pb-28 md:pb-16">
<RecordProductView productId={product.id} />
<StickyAddToCartBar product={product} inStock={inStock} />
<script
type="application/ld+json"
// "<" escapen, damit Produkttexte niemals ein schliessendes
@@ -151,6 +162,12 @@ export default async function ProductDetailPage(
<p className="text-xs text-ink/65">
{formatPricePerKg(product.pricePerKgCents, product.currency)}
</p>
<Link
href="/versand-zahlung"
className="mt-2 inline-block text-xs text-ink/65 underline decoration-ink/30 hover:text-forest hover:decoration-forest"
>
{t.productPage.shippingHint(baseShippingCents === 0 ? t.cartPage.free : formatPrice(baseShippingCents))}
</Link>
{isHoney ? (
<div className="mt-8 space-y-3">