mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-09 21:07:46 +00:00
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)
341 lines
7.7 KiB
TypeScript
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 →</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} </small>
|
|
<CartLineUpdateButton lines={[{id: lineId, quantity: prevQuantity}]}>
|
|
<button
|
|
aria-label="Decrease quantity"
|
|
disabled={quantity <= 1}
|
|
name="decrease-quantity"
|
|
value={prevQuantity}
|
|
>
|
|
<span>− </span>
|
|
</button>
|
|
</CartLineUpdateButton>
|
|
|
|
<CartLineUpdateButton lines={[{id: lineId, quantity: nextQuantity}]}>
|
|
<button
|
|
aria-label="Increase quantity"
|
|
name="increase-quantity"
|
|
value={nextQuantity}
|
|
>
|
|
<span>+</span>
|
|
</button>
|
|
</CartLineUpdateButton>
|
|
|
|
<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’t added anything yet, let’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>
|
|
|
|
<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" />
|
|
|
|
<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>
|
|
);
|
|
}
|