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:
1 parent
54919d82ed
commit
0b2196460a
16 files changed
+445
-84
No files matched your search
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user