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

Commit

Permalink
feat: update hydrogen and add new cart and variant features (#92)
Browse files Browse the repository at this point in the history
* chore(deps): update hydrogen to 2023.7.0 and hydrogen-sanity to 3.0.0

* feat: upgrade cart to use createCartHandler context

* feat: upgrade product form to use VariantSelector component
  • Loading branch information
thebiggianthead authored Jul 27, 2023
1 parent 8653cee commit 4bfe59a
Show file tree
Hide file tree
Showing 26 changed files with 3,810 additions and 3,096 deletions.
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

0 comments on commit 4bfe59a

Please sign in to comment.