Skip to content

Commit

Permalink
cart handles quantity rules
Browse files Browse the repository at this point in the history
  • Loading branch information
dustinfirman committed Apr 22, 2024
1 parent cdce061 commit 891b7c2
Show file tree
Hide file tree
Showing 3 changed files with 489 additions and 53 deletions.
349 changes: 349 additions & 0 deletions examples/b2b/app/components/Cart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
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 '~/lib/variants';

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;
/***********************************************/
/********** EXAMPLE UPDATE STARTS ************/
const {increment, minimum} = line.merchandise.quantityRule;
const prevQuantity = Number(Math.max(0, quantity - increment).toFixed(0));
const nextQuantity = Number((quantity + increment).toFixed(0));
/********** EXAMPLE UPDATE END ************/
/***********************************************/

return (
<div className="cart-line-quantity">
<small>Quantity: {quantity} &nbsp;&nbsp;</small>
<CartLineUpdateButton lines={[{id: lineId, quantity: prevQuantity}]}>
<button
aria-label="Decrease quantity"
/***********************************************/
/********** EXAMPLE UPDATE STARTS ************/
disabled={quantity <= minimum}
/********** EXAMPLE UPDATE END ************/
/***********************************************/
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>
);
}
Loading

0 comments on commit 891b7c2

Please sign in to comment.