feat(ui): Warenkorb-Seite mit Fortschritt zum Mengenrabatt
- Weg zum Mengenrabatt als sich fuellender Balken (gleiche Schwelle wie bisher der Texthinweis), erreichter Rabatt als Bestaetigung mit Wabe - Positionen gleiten herein und beim Entfernen hinaus, Entfernen-Knopf als gezeichnetes Symbol statt Textzeichen - Artikelnamen und Preise in Serifenschrift, Gesamtsumme tickt bei jeder Aenderung, Zusammenfassung laeuft beim Scrollen mit Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
467984d027
commit
e75392c75b
1 file changed
+62
-22
@@ -1,6 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { HexCheck } from "@/components/honeycomb";
|
||||||
import { Container, SectionHeading, LinkButton } from "@/components/ui";
|
import { Container, SectionHeading, LinkButton } from "@/components/ui";
|
||||||
import { TrustBadges } from "@/components/trust-badges";
|
import { TrustBadges } from "@/components/trust-badges";
|
||||||
import { CartItemThumbnail } from "@/components/cart-item-thumbnail";
|
import { CartItemThumbnail } from "@/components/cart-item-thumbnail";
|
||||||
@@ -76,13 +78,21 @@ export function WarenkorbContent({
|
|||||||
) : (
|
) : (
|
||||||
<div className="mt-14 grid lg:grid-cols-3 gap-14">
|
<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">
|
<div className="lg:col-span-2 divide-y divide-ink/10 border-y border-ink/10">
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const itemStock = stock?.[item.productId];
|
const itemStock = stock?.[item.productId];
|
||||||
return (
|
return (
|
||||||
<div key={item.productId} className="py-6 flex flex-wrap items-center gap-x-6 gap-y-3">
|
<motion.div
|
||||||
|
key={item.productId}
|
||||||
|
layout
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, x: 40, transition: { duration: 0.2 } }}
|
||||||
|
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 rounded-lg" />
|
<CartItemThumbnail image={item.image} className="w-20 h-20 flex-shrink-0 rounded-lg" />
|
||||||
<div className="flex-1 min-w-[160px]">
|
<div className="flex-1 min-w-[160px]">
|
||||||
<Link href={`/honig/${item.slug}`} className="text-lg text-forest">
|
<Link href={`/honig/${item.slug}`} className="link-draw pb-0.5 font-serif text-2xl text-forest">
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
<p className="text-sm text-ink/65">{item.weightGrams} g</p>
|
<p className="text-sm text-ink/65">{item.weightGrams} g</p>
|
||||||
@@ -103,31 +113,34 @@ export function WarenkorbContent({
|
|||||||
onChange={(quantity) => setQuantity(item.productId, quantity)}
|
onChange={(quantity) => setQuantity(item.productId, quantity)}
|
||||||
max={itemStock ?? 10}
|
max={itemStock ?? 10}
|
||||||
/>
|
/>
|
||||||
<span className="w-20 text-right text-ink">
|
<span className="w-24 text-right font-serif text-xl text-forest tabular-nums">
|
||||||
{formatPrice(item.priceCents * item.quantity, "EUR", locale)}
|
{formatPrice(item.priceCents * item.quantity, "EUR", locale)}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeItem(item.productId)}
|
onClick={() => removeItem(item.productId)}
|
||||||
aria-label={t.cart.remove}
|
aria-label={t.cart.remove}
|
||||||
className="text-ink/65 hover:text-ink"
|
className="flex h-9 w-9 items-center justify-center text-ink/55 transition-[color,transform] duration-300 hover:rotate-90 hover:text-red-700"
|
||||||
>
|
>
|
||||||
✕
|
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true">
|
||||||
|
<path d="M6 6l12 12M18 6L6 18" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border border-ink/10 p-8 h-fit">
|
<div className="border border-ink/10 bg-background p-8 h-fit lg:sticky lg:top-28">
|
||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>{t.cart.subtotal}</span>
|
<span>{t.cart.subtotal}</span>
|
||||||
<span>{formatPrice(subtotal, "EUR", locale)}</span>
|
<span className="tabular-nums">{formatPrice(subtotal, "EUR", locale)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>{t.cartPage.shippingLabel(totalQuantity)}</span>
|
<span>{t.cartPage.shippingLabel(totalQuantity)}</span>
|
||||||
<span>{shipping === 0 ? t.cartPage.free : formatPrice(shipping, "EUR", locale)}</span>
|
<span className="tabular-nums">{shipping === 0 ? t.cartPage.free : formatPrice(shipping, "EUR", locale)}</span>
|
||||||
</div>
|
</div>
|
||||||
{nextTier && nextTier.unitsUntilNext <= 2 ? (
|
{nextTier && nextTier.unitsUntilNext <= 2 ? (
|
||||||
<p className="text-xs text-ink/70">
|
<p className="text-xs text-ink/70">
|
||||||
@@ -135,20 +148,47 @@ export function WarenkorbContent({
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{activeBulkDiscount ? (
|
{activeBulkDiscount ? (
|
||||||
<div className="flex justify-between text-forest">
|
<motion.div
|
||||||
<span>
|
initial={{ opacity: 0, scale: 0.96 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ type: "spring", stiffness: 380, damping: 22 }}
|
||||||
|
className="flex items-center justify-between rounded-sm bg-honey/10 px-3 py-2 text-forest"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<HexCheck className="h-4 w-4 text-honey-dark" />
|
||||||
{t.cartPage.discountLabel} ({activeBulkDiscount.percent}%)
|
{t.cartPage.discountLabel} ({activeBulkDiscount.percent}%)
|
||||||
</span>
|
</span>
|
||||||
<span>-{formatPrice(bulkDiscountCents, "EUR", locale)}</span>
|
<span className="tabular-nums">-{formatPrice(bulkDiscountCents, "EUR", locale)}</span>
|
||||||
</div>
|
</motion.div>
|
||||||
) : nextBulkDiscount && nextBulkDiscount.unitsUntilNext <= 2 ? (
|
) : nextBulkDiscount && nextBulkDiscount.unitsUntilNext <= 2 ? (
|
||||||
<p className="text-xs text-ink/70">
|
// Der Weg zum Mengenrabatt als Glas, das sich mit jedem
|
||||||
{t.cartPage.bulkDiscountProgress(nextBulkDiscount.unitsUntilNext, nextBulkDiscount.percent)}
|
// weiteren Stueck fuellt — nicht nur als Kleingedrucktes.
|
||||||
</p>
|
<div className="rounded-sm bg-honey/[0.07] px-3 py-3">
|
||||||
|
<p className="text-xs text-ink/75">
|
||||||
|
{t.cartPage.bulkDiscountProgress(nextBulkDiscount.unitsUntilNext, nextBulkDiscount.percent)}
|
||||||
|
</p>
|
||||||
|
<div aria-hidden="true" className="mt-2 h-1.5 overflow-hidden rounded-full bg-ink/10">
|
||||||
|
<motion.div
|
||||||
|
className="h-full rounded-full bg-gradient-to-r from-honey-dark to-honey"
|
||||||
|
initial={false}
|
||||||
|
animate={{
|
||||||
|
width: `${(totalQuantity / (totalQuantity + nextBulkDiscount.unitsUntilNext)) * 100}%`,
|
||||||
|
}}
|
||||||
|
transition={{ type: "spring", stiffness: 120, damping: 20 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex justify-between text-base text-forest pt-3 border-t border-ink/10">
|
<div className="flex items-baseline justify-between text-forest pt-4 border-t border-ink/10">
|
||||||
<span>{t.cartPage.total}</span>
|
<span className="text-base">{t.cartPage.total}</span>
|
||||||
<span>{formatPrice(subtotal - bulkDiscountCents + shipping, "EUR", locale)}</span>
|
<motion.span
|
||||||
|
key={subtotal - bulkDiscountCents + shipping}
|
||||||
|
initial={{ opacity: 0.4, y: -4 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="font-serif text-3xl tabular-nums"
|
||||||
|
>
|
||||||
|
{formatPrice(subtotal - bulkDiscountCents + shipping, "EUR", locale)}
|
||||||
|
</motion.span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LinkButton href="/checkout" className="w-full mt-6">
|
<LinkButton href="/checkout" className="w-full mt-6">
|
||||||
@@ -164,10 +204,10 @@ export function WarenkorbContent({
|
|||||||
|
|
||||||
{items.length > 0 && crossSellProducts.length > 0 ? (
|
{items.length > 0 && crossSellProducts.length > 0 ? (
|
||||||
<div className="mt-20">
|
<div className="mt-20">
|
||||||
<h2 className="text-2xl text-forest mb-8">{t.productPage.relatedProductsHeading}</h2>
|
<h2 className="text-3xl md:text-4xl text-forest mb-10">{t.productPage.relatedProductsHeading}</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-14">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-14">
|
||||||
{crossSellProducts.map((product) => (
|
{crossSellProducts.map((product, index) => (
|
||||||
<ProductCard key={product.id} product={product} />
|
<ProductCard key={product.id} product={product} index={index} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user