// — minimal cart preview at /cart. // // Reads from useCart(). Renders a table of line items with thumbnail, // label, size, option choices, quantity controls, line total, and a // grand total. The "Proceed to checkout" button shows an inline // banner with "Checkout coming soon" — checkout is downstream. // // Why a stub // ────────── // Per locked decision #14: checkout is intentionally out of scope // for v0.7. The cart is the hand-off point. When a real checkout // lands (Stripe Checkout session, custom flow, whatever), this // component is the place to wire it — replace the banner with a // POST to the order endpoint and a redirect. import { useState } from 'react'; import { Link } from 'react-router-dom'; import { ShopHeader } from './ShopHeader.jsx'; import { useCart } from '../../hooks/useCart.js'; function formatUSD(cents) { return `$${((cents || 0) / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } function describeOptions(optionChoices) { const entries = Object.entries(optionChoices || {}); if (entries.length === 0) return null; return entries .map(([groupSlug, valueSlug]) => `${groupSlug}: ${valueSlug}`) .join(' · '); } export function CartStub() { const { items, count, totalCents, updateItem, removeItem, clear } = useCart(); const [checkoutBanner, setCheckoutBanner] = useState(false); const handleCheckout = () => { setCheckoutBanner(true); // Auto-dismiss after a few seconds so the page doesn't get // stuck in the toast state. Re-clicking the button re-arms it. setTimeout(() => setCheckoutBanner(false), 4000); }; return (

Your cart

{count === 0 ? 'Empty' : count === 1 ? '1 item' : `${count} items`}

{items.length === 0 ? (

Your cart is empty.

Continue shopping →
) : ( <>
    {items.map((line) => (
  • {line.imageUrl ? ( {line.productLabel} ) : ( )}

    {line.productLabel}

    {line.sizeLabel && {line.sizeLabel}} {describeOptions(line.optionChoices) && ( <> {describeOptions(line.optionChoices)} )}

    {formatUSD(line.unitPriceCents)} each

    { const n = parseInt(e.target.value, 10); if (Number.isFinite(n) && n > 0) { updateItem(line.id, { quantity: Math.min(99, n) }); } }} className="shop-qty-input" aria-label="Quantity" />
    {formatUSD(line.unitPriceCents * line.quantity)}
  • ))}
Total {formatUSD(totalCents)}

Shipping and taxes calculated at checkout.

{checkoutBanner && (

🚧 Checkout coming soon! We're still building this part — your cart will be here when it's ready.

)}
← Continue shopping
)}
); }