-
Notifications
You must be signed in to change notification settings - Fork 280
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
cdce061
commit 891b7c2
Showing
3 changed files
with
489 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 →</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} </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>− </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> | ||
); | ||
} |
Oops, something went wrong.