Files
vercel/examples/hydrogen-2/app/components/Cart.tsx
Nathan Rajlich 97659c687b [examples] Add "hydrogen-2" template (#10319)
Adds a Hydrogen v2 template which is the output of the `npm create @shopify/hydrogen@latest` command.

Note that a `vercel.json` file is being used to define the environment variables that are required at runtime. This is required for the template to deploy with zero configuration, however the user should update these values (including replacing the session secret) and migrate them to the Project settings in the Vercel dashboard.

[Live example](https://hydrogen-v2-template.vercel.app)
2023-08-10 00:11:33 +00:00

341 lines
7.7 KiB
TypeScript

import {CartForm, Image, Money} from '@shopify/hydrogen';
import type {CartLineUpdateInput} from '@shopify/hydrogen/storefront-api-types';
import {Link} from '@remix-run/react';
import type {CartApiQueryFragment} from 'storefrontapi.generated';
import {useVariantUrl} from '~/utils';
type CartLine = CartApiQueryFragment['lines']['nodes'][0];
type CartMainProps = {
cart: CartApiQueryFragment | null;
layout: 'page' | 'aside';
};
export function CartMain({layout, cart}: CartMainProps) {
const linesCount = Boolean(cart?.lines?.nodes?.length || 0);
const withDiscount =
cart &&
Boolean(cart.discountCodes.filter((code) => code.applicable).length);
const className = `cart-main ${withDiscount ? 'with-discount' : ''}`;
return (
<div className={className}>
<CartEmpty hidden={linesCount} layout={layout} />
<CartDetails cart={cart} layout={layout} />
</div>
);
}
function CartDetails({layout, cart}: CartMainProps) {
const cartHasItems = !!cart && cart.totalQuantity > 0;
return (
<div className="cart-details">
<CartLines lines={cart?.lines} layout={layout} />
{cartHasItems && (
<CartSummary cost={cart.cost} layout={layout}>
<CartDiscounts discountCodes={cart.discountCodes} />
<CartCheckoutActions checkoutUrl={cart.checkoutUrl} />
</CartSummary>
)}
</div>
);
}
function CartLines({
lines,
layout,
}: {
layout: CartMainProps['layout'];
lines: CartApiQueryFragment['lines'] | undefined;
}) {
if (!lines) return null;
return (
<div aria-labelledby="cart-lines">
<ul>
{lines.nodes.map((line) => (
<CartLineItem key={line.id} line={line} layout={layout} />
))}
</ul>
</div>
);
}
function CartLineItem({
layout,
line,
}: {
layout: CartMainProps['layout'];
line: CartLine;
}) {
const {id, merchandise} = line;
const {product, title, image, selectedOptions} = merchandise;
const lineItemUrl = useVariantUrl(product.handle, selectedOptions);
return (
<li key={id} className="cart-line">
{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 the drawer
window.location.href = lineItemUrl;
}
}}
>
<p>
<strong>{product.title}</strong>
</p>
</Link>
<CartLinePrice line={line} as="span" />
<ul>
{selectedOptions.map((option) => (
<li key={option.name}>
<small>
{option.name}: {option.value}
</small>
</li>
))}
</ul>
<CartLineQuantity line={line} />
</div>
</li>
);
}
function CartCheckoutActions({checkoutUrl}: {checkoutUrl: string}) {
if (!checkoutUrl) return null;
return (
<div>
<a href={checkoutUrl} target="_self">
<p>Continue to Checkout &rarr;</p>
</a>
<br />
</div>
);
}
export function CartSummary({
cost,
layout,
children = null,
}: {
children?: React.ReactNode;
cost: CartApiQueryFragment['cost'];
layout: CartMainProps['layout'];
}) {
const className =
layout === 'page' ? 'cart-summary-page' : 'cart-summary-aside';
return (
<div aria-labelledby="cart-summary" className={className}>
<h4>Totals</h4>
<dl className="cart-subtotal">
<dt>Subtotal</dt>
<dd>
{cost?.subtotalAmount?.amount ? (
<Money data={cost?.subtotalAmount} />
) : (
'-'
)}
</dd>
</dl>
{children}
</div>
);
}
function CartLineRemoveButton({lineIds}: {lineIds: string[]}) {
return (
<CartForm
route="/cart"
action={CartForm.ACTIONS.LinesRemove}
inputs={{lineIds}}
>
<button type="submit">Remove</button>
</CartForm>
);
}
function CartLineQuantity({line}: {line: CartLine}) {
if (!line || typeof line?.quantity === 'undefined') return null;
const {id: lineId, quantity} = line;
const prevQuantity = Number(Math.max(0, quantity - 1).toFixed(0));
const nextQuantity = Number((quantity + 1).toFixed(0));
return (
<div className="cart-line-quantiy">
<small>Quantity: {quantity} &nbsp;&nbsp;</small>
<CartLineUpdateButton lines={[{id: lineId, quantity: prevQuantity}]}>
<button
aria-label="Decrease quantity"
disabled={quantity <= 1}
name="decrease-quantity"
value={prevQuantity}
>
<span>&#8722; </span>
</button>
</CartLineUpdateButton>
&nbsp;
<CartLineUpdateButton lines={[{id: lineId, quantity: nextQuantity}]}>
<button
aria-label="Increase quantity"
name="increase-quantity"
value={nextQuantity}
>
<span>&#43;</span>
</button>
</CartLineUpdateButton>
&nbsp;
<CartLineRemoveButton lineIds={[lineId]} />
</div>
);
}
function CartLinePrice({
line,
priceType = 'regular',
...passthroughProps
}: {
line: CartLine;
priceType?: 'regular' | 'compareAt';
[key: string]: any;
}) {
if (!line?.cost?.amountPerQuantity || !line?.cost?.totalAmount) return null;
const moneyV2 =
priceType === 'regular'
? line.cost.totalAmount
: line.cost.compareAtAmountPerQuantity;
if (moneyV2 == null) {
return null;
}
return (
<div>
<Money withoutTrailingZeros {...passthroughProps} data={moneyV2} />
</div>
);
}
export function CartEmpty({
hidden = false,
layout = 'aside',
}: {
hidden: boolean;
layout?: CartMainProps['layout'];
}) {
return (
<div hidden={hidden}>
<br />
<p>
Looks like you haven&rsquo;t added anything yet, let&rsquo;s get you
started!
</p>
<br />
<Link
to="/collections"
onClick={() => {
if (layout === 'aside') {
window.location.href = '/collections';
}
}}
>
Continue shopping
</Link>
</div>
);
}
function CartDiscounts({
discountCodes,
}: {
discountCodes: CartApiQueryFragment['discountCodes'];
}) {
const codes: string[] =
discountCodes
?.filter((discount) => discount.applicable)
?.map(({code}) => code) || [];
return (
<div>
{/* Have existing discount, display it with a remove option */}
<dl hidden={!codes.length}>
<div>
<dt>Discount(s)</dt>
<UpdateDiscountForm>
<div className="cart-discount">
<code>{codes?.join(', ')}</code>
&nbsp;
<button>Remove</button>
</div>
</UpdateDiscountForm>
</div>
</dl>
{/* Show an input to apply a discount */}
<UpdateDiscountForm discountCodes={codes}>
<div>
<input type="text" name="discountCode" placeholder="Discount code" />
&nbsp;
<button type="submit">Apply</button>
</div>
</UpdateDiscountForm>
</div>
);
}
function UpdateDiscountForm({
discountCodes,
children,
}: {
discountCodes?: string[];
children: React.ReactNode;
}) {
return (
<CartForm
route="/cart"
action={CartForm.ACTIONS.DiscountCodesUpdate}
inputs={{
discountCodes: discountCodes || [],
}}
>
{children}
</CartForm>
);
}
function CartLineUpdateButton({
children,
lines,
}: {
children: React.ReactNode;
lines: CartLineUpdateInput[];
}) {
return (
<CartForm
route="/cart"
action={CartForm.ACTIONS.LinesUpdate}
inputs={{lines}}
>
{children}
</CartForm>
);
}