AbiirSkincare/Hydrogen/web/app/components/CartLineItem.tsx

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} &nbsp;&nbsp;</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>&#8722;</span>
</button>
</CartLineUpdateButton>
{layout === 'aside' ?
<span className="cart-line-qty-abiir__value">{quantity}</span>
: (
<>
&nbsp;
</>
)}
<CartLineUpdateButton lines={[{id: lineId, quantity: nextQuantity}]}>
<button
aria-label="Increase quantity"
name="increase-quantity"
value={nextQuantity}
disabled={!!isOptimistic}
type="submit"
>
<span>&#43;</span>
</button>
</CartLineUpdateButton>
{layout !== 'aside' ?
<>
&nbsp;
<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('-');
}