Wire up product variants end-to-end, add tracking-number display
Completes the frontend half of the Payload backend's variant/inventory/
tracking work (see that repo's own commit):
- **Cart**: CartItem gained an optional `variant?: string` field — every
function that used to match a line by `id` alone (addToCart/
removeFromCart/setQuantity) now matches by `(id, variant)` together via
a shared sameLine() helper, so two lines for the same product with
different variants stay separate entries. `variant` undefined on both
sides (the no-variants case) still matches by simple equality, so every
pre-existing call site keeps working unchanged.
- **Selection UI**: AddToCartInlineButton renders a <select> above the
button when given a non-empty `variants` prop (ProductGrid/
RelatedProducts pass product.variants straight through); defaults to
the first variant.
- **Pricing**: cartTotals.ts's new effectivePrice(entry, product) — a
variant's priceOverride wins over the base product price. Every cart/
checkout/order-confirmation total and per-line price display now goes
through this instead of reading product.price directly (fixes both a
wrong-price bug and a duplicate-React-key bug the old `key={product.id}`
pattern would have had the moment two variants of one product were both
in the cart).
- **Checkout**: re-validates the requested variant server-side (same
"never trust the client" reasoning as price re-derivation) — a variant
name that doesn't exist on that product fails the whole checkout.
variantName snapshots onto orders.items, shown as a parenthetical next
to the product name on the confirmation email, both invoice PDF types,
and the order-detail page.
- **Cross-device cart**: Customers.cart[].variantName (synced via
/api/account/cart) carries the selection through a login/logout cycle,
not just the current session.
Also adds tracking-number display: /konto/bestellungen/[orderNumber]
shows a clickable link when orders.trackingNumber is set, built by a new
app/lib/tracking.ts that mirrors the Payload backend's own copy
byte-for-byte close (same carrier set/URL patterns) so what a customer
sees here matches exactly what the order-shipped email already links to.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useCart, clearCart, mergeServerCartIntoLocal } from "../../lib/cart";
|
||||
import { useProducts } from "../../lib/products";
|
||||
import { useDiscount, clearDiscount } from "../../lib/discount";
|
||||
import { computeSubtotal, computeCartTotals } from "../../lib/cartTotals";
|
||||
import { computeSubtotal, computeCartTotals, effectivePrice } from "../../lib/cartTotals";
|
||||
import { formatPrice } from "../../lib/format";
|
||||
import { Reveal } from "../../components/Reveal";
|
||||
import { VersandModal } from "../../components/VersandModal";
|
||||
@@ -581,20 +581,27 @@ export function CheckoutContent({
|
||||
Deine Bestellung
|
||||
</p>
|
||||
|
||||
{items.map(({ entry, product }) => (
|
||||
<div key={product.id} className="flex gap-4 items-center w-full">
|
||||
{items.map(({ entry, product }) => {
|
||||
const unitPrice = effectivePrice(entry, product);
|
||||
const lineKey = entry.variant ? `${product.id}::${entry.variant}` : product.id;
|
||||
return (
|
||||
<div key={lineKey} className="flex gap-4 items-center w-full">
|
||||
<div className="relative size-16 shrink-0 rounded-sm overflow-hidden">
|
||||
<Image src={product.image} alt={product.name} fill sizes="64px" className="object-cover" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-0.5">
|
||||
<p className="text-body-sm text-text-primary">{product.name}</p>
|
||||
<p className="text-body-sm text-text-primary">
|
||||
{product.name}
|
||||
{entry.variant ? ` (${entry.variant})` : ""}
|
||||
</p>
|
||||
<p className="text-label text-text-muted">
|
||||
{entry.qty} × {formatPrice(product.price)} <span>inkl. MwSt.</span>
|
||||
{entry.qty} × {formatPrice(unitPrice)} <span>inkl. MwSt.</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-body-sm text-text-primary whitespace-nowrap">{formatPrice(entry.qty * product.price)}</p>
|
||||
<p className="text-body-sm text-text-primary whitespace-nowrap">{formatPrice(entry.qty * unitPrice)}</p>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="h-px bg-border w-full" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user