299 lines
8.4 KiB
TypeScript
299 lines
8.4 KiB
TypeScript
import type {CartLineUpdateInput} from '@shopify/hydrogen/storefront-api-types';
|
|
import type {CartLayout, LineItemChildrenMap} from '~/components/CartMain';
|
|
import {CartForm, Image, type OptimisticCartLine} from '@shopify/hydrogen';
|
|
import {useVariantUrl} from '~/lib/variants';
|
|
import {Link} from 'react-router';
|
|
import {ProductPrice} from './ProductPrice';
|
|
import {useAside} from './Aside';
|
|
import type {
|
|
CartApiQueryFragment,
|
|
CartLineFragment,
|
|
} from 'storefrontapi.generated';
|
|
|
|
export type CartLine = OptimisticCartLine<CartApiQueryFragment>;
|
|
|
|
function optionSummary(selectedOptions: CartLineFragment['merchandise']['selectedOptions']) {
|
|
if (!selectedOptions?.length) return null;
|
|
const values = selectedOptions
|
|
.map((o) => (o.value ?? '').trim())
|
|
.filter(Boolean)
|
|
.filter((v) => v.toLowerCase() !== 'default title');
|
|
return values.length ? values.join(' · ') : null;
|
|
}
|
|
|
|
/**
|
|
* Trash icon — Figma cart line remove (node 123:13655).
|
|
*/
|
|
function TrashIcon() {
|
|
return (
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden xmlns="http://www.w3.org/2000/svg">
|
|
<path
|
|
d="M9 3h6l1 2h5v2H3V5h5l1-2Zm1 6h2v9h-2V9Zm4 0h2v9h-2V9ZM5 9h2v9H5V9Z"
|
|
fill="currentColor"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function CartLineItem({
|
|
layout,
|
|
line,
|
|
childrenMap,
|
|
}: {
|
|
layout: CartLayout;
|
|
line: CartLine;
|
|
childrenMap: LineItemChildrenMap;
|
|
}) {
|
|
const {id, merchandise} = line;
|
|
const {product, title, image, selectedOptions} = merchandise;
|
|
const lineItemUrl = useVariantUrl(product.handle, selectedOptions);
|
|
const {close} = useAside();
|
|
const lineItemChildren = childrenMap[id];
|
|
const childrenLabelId = `cart-line-children-${id}`;
|
|
const vendorEyebrow = product.vendor?.trim();
|
|
const variantLine = optionSummary(selectedOptions);
|
|
|
|
if (layout === 'aside') {
|
|
return (
|
|
<li key={id} className="cart-line cart-line--abiir-aside">
|
|
<div className="cart-line-abiir">
|
|
{image ?
|
|
<Link
|
|
to={lineItemUrl}
|
|
prefetch="intent"
|
|
onClick={close}
|
|
className="cart-line-abiir__img-wrap"
|
|
>
|
|
<Image
|
|
alt={title}
|
|
aspectRatio="4/5"
|
|
data={image}
|
|
height={100}
|
|
loading="lazy"
|
|
width={80}
|
|
/>
|
|
</Link>
|
|
: null}
|
|
<div className="cart-line-abiir__body">
|
|
<div className="cart-line-abiir__top">
|
|
<div className="cart-line-abiir__text">
|
|
{vendorEyebrow ?
|
|
<p className="cart-line-abiir__vendor">{vendorEyebrow}</p>
|
|
: null}
|
|
<Link
|
|
to={lineItemUrl}
|
|
prefetch="intent"
|
|
onClick={close}
|
|
className="cart-line-abiir__title-link"
|
|
>
|
|
<p className="cart-line-abiir__title">{product.title}</p>
|
|
</Link>
|
|
{variantLine ?
|
|
<p className="cart-line-abiir__meta">{variantLine}</p>
|
|
: null}
|
|
</div>
|
|
<CartLineRemoveButton lineIds={[id]} disabled={!!line.isOptimistic} icon />
|
|
</div>
|
|
<div className="cart-line-abiir__row">
|
|
<CartLineQuantity line={line} layout={layout} />
|
|
<div className="cart-line-abiir__price">
|
|
<ProductPrice price={line?.cost?.totalAmount} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{lineItemChildren ?
|
|
<div>
|
|
<p id={childrenLabelId} className="sr-only">
|
|
Line items with {product.title}
|
|
</p>
|
|
<ul aria-labelledby={childrenLabelId} className="cart-line-children">
|
|
{lineItemChildren.map((childLine) => (
|
|
<CartLineItem
|
|
childrenMap={childrenMap}
|
|
key={childLine.id}
|
|
line={childLine}
|
|
layout={layout}
|
|
/>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
: null}
|
|
</li>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<li key={id} className="cart-line">
|
|
<div className="cart-line-inner">
|
|
{image && (
|
|
<Image
|
|
alt={title}
|
|
aspectRatio="1/1"
|
|
data={image}
|
|
height={100}
|
|
loading="lazy"
|
|
width={100}
|
|
/>
|
|
)}
|
|
|
|
<div>
|
|
<Link
|
|
prefetch="intent"
|
|
to={lineItemUrl}
|
|
onClick={() => {
|
|
if (layout === 'aside') {
|
|
close();
|
|
}
|
|
}}
|
|
>
|
|
<p>
|
|
<strong>{product.title}</strong>
|
|
</p>
|
|
</Link>
|
|
<ProductPrice price={line?.cost?.totalAmount} />
|
|
<ul>
|
|
{selectedOptions
|
|
.filter(
|
|
(option) =>
|
|
(option.value ?? '').trim().toLowerCase() !== 'default title',
|
|
)
|
|
.map((option) => (
|
|
<li key={option.name}>
|
|
<small>
|
|
{option.name}: {option.value}
|
|
</small>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<CartLineQuantity line={line} layout={layout} />
|
|
</div>
|
|
</div>
|
|
|
|
{lineItemChildren ? (
|
|
<div>
|
|
<p id={childrenLabelId} className="sr-only">
|
|
Line items with {product.title}
|
|
</p>
|
|
<ul aria-labelledby={childrenLabelId} className="cart-line-children">
|
|
{lineItemChildren.map((childLine) => (
|
|
<CartLineItem
|
|
childrenMap={childrenMap}
|
|
key={childLine.id}
|
|
line={childLine}
|
|
layout={layout}
|
|
/>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
) : null}
|
|
</li>
|
|
);
|
|
}
|
|
|
|
function CartLineQuantity({line, layout}: {line: CartLine; layout: CartLayout}) {
|
|
if (!line || typeof line?.quantity === 'undefined') return null;
|
|
const {id: lineId, quantity, isOptimistic} = line;
|
|
const prevQuantity = Number(Math.max(0, quantity - 1).toFixed(0));
|
|
const nextQuantity = Number((quantity + 1).toFixed(0));
|
|
|
|
const wrapClass =
|
|
layout === 'aside' ? 'cart-line-qty-abiir' : 'cart-line-quantity';
|
|
|
|
return (
|
|
<div className={wrapClass}>
|
|
{layout !== 'aside' ?
|
|
<small>Quantity: {quantity} </small>
|
|
: null}
|
|
<CartLineUpdateButton lines={[{id: lineId, quantity: prevQuantity}]}>
|
|
<button
|
|
aria-label="Decrease quantity"
|
|
disabled={quantity <= 1 || !!isOptimistic}
|
|
name="decrease-quantity"
|
|
value={prevQuantity}
|
|
type="submit"
|
|
>
|
|
<span>−</span>
|
|
</button>
|
|
</CartLineUpdateButton>
|
|
{layout === 'aside' ?
|
|
<span className="cart-line-qty-abiir__value">{quantity}</span>
|
|
: (
|
|
<>
|
|
|
|
</>
|
|
)}
|
|
<CartLineUpdateButton lines={[{id: lineId, quantity: nextQuantity}]}>
|
|
<button
|
|
aria-label="Increase quantity"
|
|
name="increase-quantity"
|
|
value={nextQuantity}
|
|
disabled={!!isOptimistic}
|
|
type="submit"
|
|
>
|
|
<span>+</span>
|
|
</button>
|
|
</CartLineUpdateButton>
|
|
{layout !== 'aside' ?
|
|
<>
|
|
|
|
<CartLineRemoveButton lineIds={[lineId]} disabled={!!isOptimistic} />
|
|
</>
|
|
: null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CartLineRemoveButton({
|
|
lineIds,
|
|
disabled,
|
|
icon = false,
|
|
}: {
|
|
lineIds: string[];
|
|
disabled: boolean;
|
|
icon?: boolean;
|
|
}) {
|
|
return (
|
|
<CartForm
|
|
fetcherKey={[CartForm.ACTIONS.LinesRemove, ...lineIds].join('-')}
|
|
route="/cart"
|
|
action={CartForm.ACTIONS.LinesRemove}
|
|
inputs={{lineIds}}
|
|
>
|
|
<button
|
|
disabled={disabled}
|
|
type="submit"
|
|
className={icon ? 'cart-line-abiir__remove' : undefined}
|
|
aria-label="Remove line item"
|
|
>
|
|
{icon ? <TrashIcon /> : 'Remove'}
|
|
</button>
|
|
</CartForm>
|
|
);
|
|
}
|
|
|
|
function CartLineUpdateButton({
|
|
children,
|
|
lines,
|
|
}: {
|
|
children: React.ReactNode;
|
|
lines: CartLineUpdateInput[];
|
|
}) {
|
|
const lineIds = lines.map((line) => line.id);
|
|
|
|
return (
|
|
<CartForm
|
|
fetcherKey={getUpdateKey(lineIds)}
|
|
route="/cart"
|
|
action={CartForm.ACTIONS.LinesUpdate}
|
|
inputs={{lines}}
|
|
>
|
|
{children}
|
|
</CartForm>
|
|
);
|
|
}
|
|
|
|
function getUpdateKey(lineIds: string[]) {
|
|
return [CartForm.ACTIONS.LinesUpdate, ...lineIds].join('-');
|
|
}
|