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

352 lines
9.5 KiB
TypeScript

import type {CartApiQueryFragment} from 'storefrontapi.generated';
import type {CartLayout} from '~/components/CartMain';
import {CartForm, Money, type OptimisticCart} from '@shopify/hydrogen';
import {useEffect, useId, useRef, useState} from 'react';
import {Link} from 'react-router';
import {useFetcher} from 'react-router';
import {useAside} from '~/components/Aside';
type CartSummaryProps = {
cart: OptimisticCart<CartApiQueryFragment | null>;
layout: CartLayout;
};
export function CartSummary({cart, layout}: CartSummaryProps) {
const className =
layout === 'page' ? 'cart-summary-page' : 'cart-summary-aside';
const summaryId = useId();
const discountsHeadingId = useId();
const discountCodeInputId = useId();
const giftCardHeadingId = useId();
const giftCardInputId = useId();
if (layout === 'aside') {
return <CartSummaryAside cart={cart} />;
}
return (
<div aria-labelledby={summaryId} className={className}>
<h4 id={summaryId}>Totals</h4>
<dl role="group" className="cart-subtotal">
<dt>Subtotal</dt>
<dd>
{cart?.cost?.subtotalAmount?.amount ? (
<Money data={cart?.cost?.subtotalAmount} />
) : (
'-'
)}
</dd>
</dl>
<CartDiscounts
discountCodes={cart?.discountCodes}
discountsHeadingId={discountsHeadingId}
discountCodeInputId={discountCodeInputId}
/>
<CartGiftCard
giftCardCodes={cart?.appliedGiftCards}
giftCardHeadingId={giftCardHeadingId}
giftCardInputId={giftCardInputId}
/>
<CartCheckoutActions checkoutUrl={cart?.checkoutUrl} />
</div>
);
}
/** Figma cart drawer footer — node 123:13655 */
function CartSummaryAside({cart}: {cart: OptimisticCart<CartApiQueryFragment | null>}) {
const {close} = useAside();
const checkoutUrl = cart?.checkoutUrl;
const subtotal = cart?.cost?.subtotalAmount;
return (
<div className="cart-summary-aside cart-summary-aside--abiir">
<div className="cart-summary-aside__rows">
<div className="cart-summary-aside__row">
<span className="cart-summary-aside__label">Subtotal</span>
<span className="cart-summary-aside__value">
{subtotal?.amount ?
<Money data={subtotal} />
: '—'}
</span>
</div>
<div className="cart-summary-aside__row">
<span className="cart-summary-aside__label">Shipping</span>
<span className="cart-summary-aside__value cart-summary-aside__value--muted">
Calculated at checkout
</span>
</div>
</div>
{checkoutUrl ?
<a href={checkoutUrl} target="_self" className="cart-summary-aside__checkout">
<span className="cart-summary-aside__checkout-label">Checkout</span>
<span className="cart-summary-aside__checkout-arrow" aria-hidden>
→
</span>
</a>
: null}
<Link to="/collections/all" prefetch="viewport" onClick={close} className="cart-summary-aside__continue">
Continue shopping
</Link>
<div className="cart-summary-aside__trust" aria-label="Checkout assurances">
<span>Secure checkout</span>
<span>Free returns</span>
<span>Encrypted</span>
</div>
</div>
);
}
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>
);
}
function CartDiscounts({
discountCodes,
discountsHeadingId,
discountCodeInputId,
}: {
discountCodes?: CartApiQueryFragment['discountCodes'];
discountsHeadingId: string;
discountCodeInputId: string;
}) {
const codes: string[] =
discountCodes
?.filter((discount) => discount.applicable)
?.map(({code}) => code) || [];
return (
<section aria-label="Discounts">
<dl hidden={!codes.length}>
<div>
<dt id={discountsHeadingId}>Discounts</dt>
<UpdateDiscountForm>
<div
className="cart-discount"
role="group"
aria-labelledby={discountsHeadingId}
>
<code>{codes?.join(', ')}</code>
&nbsp;
<button type="submit" aria-label="Remove discount">
Remove
</button>
</div>
</UpdateDiscountForm>
</div>
</dl>
<UpdateDiscountForm discountCodes={codes}>
<div>
<label htmlFor={discountCodeInputId} className="sr-only">
Discount code
</label>
<input
id={discountCodeInputId}
type="text"
name="discountCode"
placeholder="Discount code"
/>
&nbsp;
<button type="submit" aria-label="Apply discount code">
Apply
</button>
</div>
</UpdateDiscountForm>
</section>
);
}
function UpdateDiscountForm({
discountCodes,
children,
}: {
discountCodes?: string[];
children: React.ReactNode;
}) {
return (
<CartForm
route="/cart"
action={CartForm.ACTIONS.DiscountCodesUpdate}
inputs={{
discountCodes: discountCodes || [],
}}
>
{children}
</CartForm>
);
}
function CartGiftCard({
giftCardCodes,
giftCardHeadingId,
giftCardInputId,
}: {
giftCardCodes: CartApiQueryFragment['appliedGiftCards'] | undefined;
giftCardHeadingId: string;
giftCardInputId: string;
}) {
const giftCardCodeInput = useRef<HTMLInputElement>(null);
const removeButtonRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
const previousCardIdsRef = useRef<string[]>([]);
const giftCardAddFetcher = useFetcher({key: 'gift-card-add'});
const [removedCardIndex, setRemovedCardIndex] = useState<number | null>(null);
useEffect(() => {
if (giftCardAddFetcher.data) {
if (giftCardCodeInput.current !== null) {
giftCardCodeInput.current.value = '';
}
}
}, [giftCardAddFetcher.data]);
useEffect(() => {
const currentCardIds = giftCardCodes?.map((card) => card.id) || [];
if (removedCardIndex !== null && giftCardCodes) {
const focusTargetIndex = Math.min(
removedCardIndex,
giftCardCodes.length - 1,
);
const focusTargetCard = giftCardCodes[focusTargetIndex];
const focusButton = focusTargetCard
? removeButtonRefs.current.get(focusTargetCard.id)
: null;
if (focusButton) {
focusButton.focus();
} else if (giftCardCodeInput.current) {
giftCardCodeInput.current.focus();
}
setRemovedCardIndex(null);
}
previousCardIdsRef.current = currentCardIds;
}, [giftCardCodes, removedCardIndex]);
const handleRemoveClick = (cardId: string) => {
const index = previousCardIdsRef.current.indexOf(cardId);
if (index !== -1) {
setRemovedCardIndex(index);
}
};
return (
<section aria-label="Gift cards">
{giftCardCodes && giftCardCodes.length > 0 && (
<dl>
<dt id={giftCardHeadingId}>Applied Gift Card(s)</dt>
{giftCardCodes.map((giftCard) => (
<dd key={giftCard.id} className="cart-discount">
<RemoveGiftCardForm
giftCardId={giftCard.id}
lastCharacters={giftCard.lastCharacters}
onRemoveClick={() => handleRemoveClick(giftCard.id)}
buttonRef={(el: HTMLButtonElement | null) => {
if (el) {
removeButtonRefs.current.set(giftCard.id, el);
} else {
removeButtonRefs.current.delete(giftCard.id);
}
}}
>
<code>***{giftCard.lastCharacters}</code>
&nbsp;
<Money data={giftCard.amountUsed} />
</RemoveGiftCardForm>
</dd>
))}
</dl>
)}
<AddGiftCardForm fetcherKey="gift-card-add">
<div>
<label htmlFor={giftCardInputId} className="sr-only">
Gift card code
</label>
<input
id={giftCardInputId}
type="text"
name="giftCardCode"
placeholder="Gift card code"
ref={giftCardCodeInput}
/>
&nbsp;
<button
type="submit"
disabled={giftCardAddFetcher.state !== 'idle'}
aria-label="Apply gift card code"
>
Apply
</button>
</div>
</AddGiftCardForm>
</section>
);
}
function AddGiftCardForm({
fetcherKey,
children,
}: {
fetcherKey?: string;
children: React.ReactNode;
}) {
return (
<CartForm
fetcherKey={fetcherKey}
route="/cart"
action={CartForm.ACTIONS.GiftCardCodesAdd}
>
{children}
</CartForm>
);
}
function RemoveGiftCardForm({
giftCardId,
lastCharacters,
children,
onRemoveClick,
buttonRef,
}: {
giftCardId: string;
lastCharacters: string;
children: React.ReactNode;
onRemoveClick?: () => void;
buttonRef?: (el: HTMLButtonElement | null) => void;
}) {
return (
<CartForm
route="/cart"
action={CartForm.ACTIONS.GiftCardCodesRemove}
inputs={{
giftCardCodes: [giftCardId],
}}
>
{children}
&nbsp;
<button
type="submit"
aria-label={`Remove gift card ending in ${lastCharacters}`}
onClick={onRemoveClick}
ref={buttonRef}
>
Remove
</button>
</CartForm>
);
}