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
@@ -7,6 +7,7 @@ import { CartItemThumbnail } from "@/components/cart-item-thumbnail";
|
||||
import { QuantityStepper } from "@/components/quantity-stepper";
|
||||
import { ProductCard } from "@/components/product-card";
|
||||
import { useCartStore, cartTotalCents, cartItemCount } from "@/store/cart-store";
|
||||
import { useCartStock } from "@/hooks/use-cart-stock";
|
||||
import { formatPrice } from "@/lib/format";
|
||||
import { resolveShippingCostCents, getNextShippingTier, type ShippingTierLike } from "@/lib/shipping-calc";
|
||||
import { resolveBulkDiscount, getNextBulkDiscountTier, type BulkDiscountTierLike } from "@/lib/bulk-discount-calc";
|
||||
@@ -28,6 +29,10 @@ export function WarenkorbContent({
|
||||
const items = useCartStore((s) => s.items);
|
||||
const setQuantity = useCartStore((s) => s.setQuantity);
|
||||
const removeItem = useCartStore((s) => s.removeItem);
|
||||
// Live-Bestand je Position — derselbe Hinweis wie auf der Produktkarte/
|
||||
// -detailseite, aber hier fuer den ggf. seit dem Hinzufuegen veraenderten
|
||||
// aktuellen Bestand (siehe use-cart-stock.ts).
|
||||
const stock = useCartStock(items.map((item) => item.productId));
|
||||
const subtotal = cartTotalCents(items);
|
||||
// Gesamtmenge ueber alle Positionen hinweg — mehrere verschiedene Artikel
|
||||
// werden fuer die Versandstaffel zusammengezaehlt, nicht getrennt bewertet.
|
||||
@@ -63,33 +68,42 @@ export function WarenkorbContent({
|
||||
) : (
|
||||
<div className="mt-14 grid lg:grid-cols-3 gap-14">
|
||||
<div className="lg:col-span-2 divide-y divide-ink/10 border-y border-ink/10">
|
||||
{items.map((item) => (
|
||||
<div key={item.productId} className="py-6 flex flex-wrap items-center gap-x-6 gap-y-3">
|
||||
<CartItemThumbnail image={item.image} className="w-20 h-20 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-[160px]">
|
||||
<Link href={`/honig/${item.slug}`} className="text-lg text-forest">
|
||||
{item.name}
|
||||
</Link>
|
||||
<p className="text-sm text-ink/65">{item.weightGrams} g</p>
|
||||
{items.map((item) => {
|
||||
const itemStock = stock?.[item.productId];
|
||||
return (
|
||||
<div key={item.productId} className="py-6 flex flex-wrap items-center gap-x-6 gap-y-3">
|
||||
<CartItemThumbnail image={item.image} className="w-20 h-20 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-[160px]">
|
||||
<Link href={`/honig/${item.slug}`} className="text-lg text-forest">
|
||||
{item.name}
|
||||
</Link>
|
||||
<p className="text-sm 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>
|
||||
<div className="flex items-center gap-4 ml-auto">
|
||||
<QuantityStepper
|
||||
quantity={item.quantity}
|
||||
onChange={(quantity) => setQuantity(item.productId, quantity)}
|
||||
max={itemStock ?? 10}
|
||||
/>
|
||||
<span className="w-20 text-right text-ink">
|
||||
{formatPrice(item.priceCents * item.quantity)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => removeItem(item.productId)}
|
||||
aria-label={t.cart.remove}
|
||||
className="text-ink/65 hover:text-ink"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 ml-auto">
|
||||
<QuantityStepper
|
||||
quantity={item.quantity}
|
||||
onChange={(quantity) => setQuantity(item.productId, quantity)}
|
||||
/>
|
||||
<span className="w-20 text-right text-ink">
|
||||
{formatPrice(item.priceCents * item.quantity)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => removeItem(item.productId)}
|
||||
aria-label={t.cart.remove}
|
||||
className="text-ink/65 hover:text-ink"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="border border-ink/10 p-8 h-fit">
|
||||
|
||||
Reference in new issue
Block a user