Skip to content
This repository has been archived by the owner on Apr 9, 2024. It is now read-only.

feat: update hydrogen and add new cart and variant features #92

Merged
merged 3 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 74 additions & 65 deletions app/components/cart/Cart.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import {
type FetcherWithComponents,
useFetcher,
useMatches,
} from '@remix-run/react';
import {useMatches} from '@remix-run/react';
import {CartForm} from '@shopify/hydrogen';
import type {
Cart,
CartCost,
CartLine,
CartLineUpdateInput,
ComponentizableCartLine,
} from '@shopify/hydrogen/storefront-api-types';
import {
flattenConnection,
Expand All @@ -23,7 +21,7 @@ import PlusCircleIcon from '~/components/icons/PlusCircle';
import RemoveIcon from '~/components/icons/Remove';
import SpinnerIcon from '~/components/icons/Spinner';
import {Link} from '~/components/Link';
import {CartAction} from '~/types/shopify';
import {useCartFetchers} from '~/hooks/useCartFetchers';

export function CartLineItems({
linesObj,
Expand All @@ -45,21 +43,48 @@ export function CartLineItems({
);
}

function LineItem({lineItem}: {lineItem: CartLine}) {
function LineItem({lineItem}: {lineItem: CartLine | ComponentizableCartLine}) {
const {merchandise} = lineItem;

const updatingItems = useCartFetchers(CartForm.ACTIONS.LinesUpdate);
const removingItems = useCartFetchers(CartForm.ACTIONS.LinesRemove);

// Check if the line item is being updated, as we want to show the new quantity as optimistic UI
let updatingQty;
const updating =
updatingItems?.find((fetcher) => {
const formData = fetcher?.formData;

if (formData) {
const formInputs = CartForm.getFormInput(formData);
return (
Array.isArray(formInputs?.inputs?.lines) &&
formInputs?.inputs?.lines?.find((line: CartLineUpdateInput) => {
updatingQty = line.quantity;
return line.id === lineItem.id;
})
);
}
}) && updatingQty;

// Check if the line item is being removed, as we want to show the line item as being deleted
const deleting = removingItems.find((fetcher) => {
const formData = fetcher?.formData;
if (formData) {
const formInputs = CartForm.getFormInput(formData);
return (
Array.isArray(formInputs?.inputs?.lineIds) &&
formInputs?.inputs?.lineIds?.find(
(lineId: CartLineUpdateInput['id']) => lineId === lineItem.id,
)
);
}
});

const firstVariant = merchandise.selectedOptions[0];
const hasDefaultVariantOnly =
firstVariant.name === 'Title' && firstVariant.value === 'Default Title';

const updateItem = useFetcher();
const deleteItem = useFetcher();

const updating =
updateItem.state === 'submitting' || updateItem.state === 'loading';
const deleting =
deleteItem.state === 'submitting' || deleteItem.state === 'loading';

return (
<div
role="row"
Expand Down Expand Up @@ -108,7 +133,7 @@ function LineItem({lineItem}: {lineItem: CartLine}) {
</div>

{/* Quantity */}
<CartItemQuantity line={lineItem} fetcher={updateItem} />
<CartItemQuantity line={lineItem} submissionQuantity={updating} />

{/* Price */}
<div className="ml-4 mr-6 flex min-w-[4rem] justify-end text-sm font-bold leading-none">
Expand All @@ -120,63 +145,49 @@ function LineItem({lineItem}: {lineItem: CartLine}) {
</div>

<div role="cell" className="flex flex-col items-end justify-between">
<ItemRemoveButton lineIds={[lineItem.id]} fetcher={deleteItem} />
<ItemRemoveButton lineIds={[lineItem.id]} />
</div>
</div>
);
}

function CartItemQuantity({
line,
fetcher,
submissionQuantity,
}: {
line: CartLine;
fetcher: FetcherWithComponents<any>;
line: CartLine | ComponentizableCartLine;
submissionQuantity: number | undefined;
}) {
if (!line || typeof line?.quantity === 'undefined') return null;
const {id: lineId, quantity} = line;

// The below handles optimistic updates for the quantity
const submissionQuantity = fetcher?.formData?.get('quantity');
const lineQuantity = submissionQuantity
? Number(submissionQuantity)
: quantity;
// // The below handles optimistic updates for the quantity
const lineQuantity = submissionQuantity ? submissionQuantity : quantity;

const prevQuantity = Number(Math.max(0, lineQuantity - 1).toFixed(0));
const nextQuantity = Number((lineQuantity + 1).toFixed(0));

return (
<div className="flex items-center gap-2">
<fetcher.Form action="/cart" method="post">
<UpdateCartButton lines={[{id: lineId, quantity: prevQuantity}]}>
<input type="hidden" name="quantity" value={prevQuantity} />
<button
name="decrease-quantity"
aria-label="Decrease quantity"
value={prevQuantity}
disabled={quantity <= 1}
>
<MinusCircleIcon />
</button>
</UpdateCartButton>
</fetcher.Form>
<UpdateCartButton lines={[{id: lineId, quantity: prevQuantity}]}>
<button
aria-label="Decrease quantity"
value={prevQuantity}
disabled={quantity <= 1}
>
<MinusCircleIcon />
</button>
</UpdateCartButton>

<div className="min-w-[1rem] text-center text-sm font-bold leading-none text-black">
{lineQuantity}
</div>

<fetcher.Form action="/cart" method="post">
<UpdateCartButton lines={[{id: lineId, quantity: nextQuantity}]}>
<input type="hidden" name="quantity" value={nextQuantity} />
<button
name="increase-quantity"
aria-label="Increase quantity"
value={prevQuantity}
>
<PlusCircleIcon />
</button>
</UpdateCartButton>
</fetcher.Form>
<UpdateCartButton lines={[{id: lineId, quantity: nextQuantity}]}>
<button aria-label="Increase quantity" value={prevQuantity}>
<PlusCircleIcon />
</button>
</UpdateCartButton>
</div>
);
}
Expand All @@ -189,32 +200,30 @@ function UpdateCartButton({
lines: CartLineUpdateInput[];
}) {
return (
<>
<input type="hidden" name="cartAction" value={CartAction.UPDATE_CART} />
<input type="hidden" name="lines" value={JSON.stringify(lines)} />
<CartForm
route="/cart"
action={CartForm.ACTIONS.LinesUpdate}
inputs={{lines}}
>
{children}
</>
</CartForm>
);
}

function ItemRemoveButton({
lineIds,
fetcher,
}: {
lineIds: CartLine['id'][];
fetcher: FetcherWithComponents<any>;
}) {
function ItemRemoveButton({lineIds}: {lineIds: CartLine['id'][]}) {
return (
<fetcher.Form action="/cart" method="post">
<input type="hidden" name="cartAction" value="REMOVE_FROM_CART" />
<input type="hidden" name="linesIds" value={JSON.stringify(lineIds)} />
<CartForm
route="/cart"
action={CartForm.ACTIONS.LinesRemove}
inputs={{lineIds}}
>
<button
className="disabled:pointer-events-all disabled:cursor-wait"
type="submit"
>
<RemoveIcon />
</button>
</fetcher.Form>
</CartForm>
);
}

Expand Down
13 changes: 9 additions & 4 deletions app/components/global/CountrySelector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Listbox} from '@headlessui/react';
import {useFetcher, useLocation, useMatches} from '@remix-run/react';
import {CartForm} from '@shopify/hydrogen';
import clsx from 'clsx';
import {useState} from 'react';
import invariant from 'tiny-invariant';
Expand All @@ -8,7 +9,7 @@ import {ChevronDownIcon} from '~/components/icons/ChevronDown';
import RadioIcon from '~/components/icons/Radio';
import {countries} from '~/data/countries';
import {DEFAULT_LOCALE} from '~/lib/utils';
import {CartAction, type Locale} from '~/types/shopify';
import type {Locale} from '~/types/shopify';

type Props = {
align?: 'center' | 'left' | 'right';
Expand Down Expand Up @@ -48,9 +49,13 @@ export function CountrySelector({align = 'center'}: Props) {

fetcher.submit(
{
cartAction: CartAction.UPDATE_BUYER_IDENTITY,
buyerIdentity: JSON.stringify({
countryCode: newLocale.country,
cartFormInput: JSON.stringify({
action: CartForm.ACTIONS.BuyerIdentityUpdate,
inputs: {
buyerIdentity: {
countryCode: newLocale.country,
},
},
}),
redirectTo: countryUrlPath,
},
Expand Down
3 changes: 2 additions & 1 deletion app/components/global/HeaderActions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {useMatches} from '@remix-run/react';
import {CartForm} from '@shopify/hydrogen';
import clsx from 'clsx';
import {useEffect} from 'react';

Expand All @@ -15,7 +16,7 @@ export default function HeaderActions() {
const cart = root.data?.cart;

// Grab all the fetchers that are adding to cart
const addToCartFetchers = useCartFetchers('ADD_TO_CART');
const addToCartFetchers = useCartFetchers(CartForm.ACTIONS.LinesAdd);

// When the fetchers array changes, open the drawer if there is an add to cart action
useEffect(() => {
Expand Down
4 changes: 4 additions & 0 deletions app/components/product/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import type {SanityProductPage} from '~/lib/sanity';
type Props = {
sanityProduct: SanityProductPage;
storefrontProduct: Product;
storefrontVariants: ProductVariant[];
selectedVariant: ProductVariant;
analytics: ShopifyAnalyticsPayload;
};

export default function ProductDetails({
sanityProduct,
storefrontProduct,
storefrontVariants,
selectedVariant,
analytics,
}: Props) {
Expand All @@ -35,6 +37,7 @@ export default function ProductDetails({
<ProductWidget
sanityProduct={sanityProduct}
storefrontProduct={storefrontProduct}
storefrontVariants={storefrontVariants}
selectedVariant={selectedVariant}
analytics={analytics}
/>
Expand All @@ -52,6 +55,7 @@ export default function ProductDetails({
<ProductWidget
sanityProduct={sanityProduct}
storefrontProduct={storefrontProduct}
storefrontVariants={storefrontVariants}
selectedVariant={selectedVariant}
analytics={analytics}
/>
Expand Down
4 changes: 4 additions & 0 deletions app/components/product/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import {hasMultipleProductOptions} from '~/lib/utils';

export default function ProductForm({
product,
variants,
selectedVariant,
analytics,
customProductOptions,
}: {
product: Product;
variants: ProductVariant[];
selectedVariant: ProductVariant;
analytics: ShopifyAnalyticsPayload;
customProductOptions?: SanityCustomProductOption[];
Expand All @@ -44,6 +46,8 @@ export default function ProductForm({
{multipleProductOptions && (
<>
<ProductOptions
product={product}
variants={variants}
options={product.options}
selectedVariant={selectedVariant}
customProductOptions={customProductOptions}
Expand Down
Loading