Template
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F22Ms5Cxam5UHyxaCrtZzu
113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { useCart, useCartForm } from "@/lib/cart";
|
|
import { content } from "@/lib/content";
|
|
import { formatPrice } from "@/lib/money";
|
|
|
|
import { CartLineItem } from "./CartLineItem";
|
|
|
|
/**
|
|
* Shared cart content — line items, discount code form, and totals. Used by
|
|
* both the cart drawer and the `/cart` page (`hydrogen-cart-ui` /
|
|
* `hydrogen-cart-drawer`). The drawer wraps this in fixed header/body/footer
|
|
* zones; the page wraps it in a page layout.
|
|
*
|
|
* Layout: the line-item list flexes to fill the available height and scrolls on
|
|
* its own; the discount code + estimated-total block is pinned to the bottom
|
|
* (feedback: bottom-align the footer block). The order note was removed — its
|
|
* "save note" button was non-functional and the entry point is gone.
|
|
*/
|
|
export function CartContent() {
|
|
const cart = useCart((state) => state.data);
|
|
const loading = useCart((state) => state.loading);
|
|
const lines = cart.lines.nodes;
|
|
const totalQuantity = cart.totalQuantity;
|
|
const subtotal = cart.cost.subtotalAmount;
|
|
const discountCodes = cart.discountCodes;
|
|
|
|
const { formProps, register } = useCartForm();
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="divide-border divide-y" aria-busy="true">
|
|
<p className="text-on-surface-secondary py-8 text-center text-sm">Loading cart…</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (totalQuantity === 0 || lines.length === 0) {
|
|
return (
|
|
<div className="flex flex-col items-center gap-2 py-12 text-center">
|
|
<p className="type-body-sm text-on-surface font-medium">{content.cart.empty}</p>
|
|
<p className="text-on-surface-secondary text-sm">{content.cart.emptyDescription}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full flex-col">
|
|
<ul role="list" className="divide-border min-h-0 flex-1 divide-y overflow-y-auto">
|
|
{lines.map((line) => (
|
|
<li key={line.id}>
|
|
<CartLineItem line={line} />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<div className="border-border mt-3 shrink-0 border-t pt-3">
|
|
<div className="flex flex-col gap-3">
|
|
{/* Discount apply form */}
|
|
<form {...formProps()} className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
{...register("discountCode", { defaultValue: "" })}
|
|
placeholder="Discount code"
|
|
aria-label="Discount code"
|
|
className="number-reset rounded-button border-border h-11 flex-1 border px-3 text-sm"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
{...register("discount-apply")}
|
|
className="rounded-button button-secondary inline-flex h-11 items-center justify-center px-4 text-sm font-medium"
|
|
>
|
|
Apply
|
|
</button>
|
|
</form>
|
|
|
|
{/* Applied discount codes — each removal is its own form */}
|
|
{discountCodes.map((code) => (
|
|
<form {...formProps()} key={code.code} className="flex items-center gap-2">
|
|
<input type="hidden" {...register("discountCode", { value: code.code })} />
|
|
<span className="chip-filled rounded-full px-3 py-1 text-sm">{code.code}</span>
|
|
<button
|
|
type="submit"
|
|
{...register("discount-remove")}
|
|
className="button-icon rounded text-sm"
|
|
aria-label={`Remove discount ${code.code}`}
|
|
>
|
|
<img
|
|
src="/icons/icon-x.svg"
|
|
width="16"
|
|
height="16"
|
|
alt=""
|
|
className="size-4"
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
</form>
|
|
))}
|
|
|
|
{/* Estimated total */}
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-on-surface text-sm font-medium">{content.cart.totalLabel}</span>
|
|
<span className="text-on-surface text-base font-medium">{formatPrice(subtotal)}</span>
|
|
</div>
|
|
<p className="text-on-surface-secondary text-xs">
|
|
{content.cart.taxesAndShippingAtCheckout}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|