diff --git a/components/cart/actions.ts b/components/cart/actions.ts index e24b3b02c..ae69a4879 100644 --- a/components/cart/actions.ts +++ b/components/cart/actions.ts @@ -6,22 +6,13 @@ import { revalidateTag } from 'next/cache'; import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; +let cartId: string | undefined; + export async function addItem(prevState: any, selectedVariantId: string | undefined) { - let cartId = cookies().get('cartId')?.value; - let cart; + cartId = cartId || cookies().get('cartId')?.value; - if (cartId) { - cart = await getCart(cartId); - } - - if (!cartId || !cart) { - cart = await createCart(); - cartId = cart.id; - cookies().set('cartId', cartId); - } - - if (!selectedVariantId) { - return 'Missing product variant ID'; + if (!cartId || !selectedVariantId) { + return 'Error adding item to cart'; } try { @@ -32,16 +23,28 @@ export async function addItem(prevState: any, selectedVariantId: string | undefi } } -export async function removeItem(prevState: any, lineId: string) { - const cartId = cookies().get('cartId')?.value; +export async function removeItem(prevState: any, merchandiseId: string) { + cartId = cartId || cookies().get('cartId')?.value; if (!cartId) { return 'Missing cart ID'; } try { - await removeFromCart(cartId, [lineId]); - revalidateTag(TAGS.cart); + const cart = await getCart(cartId); + + if (!cart) { + return 'Error fetching cart'; + } + + const lineItem = cart.lines.find((line) => line.merchandise.id === merchandiseId); + + if (lineItem) { + await removeFromCart(cartId, [lineItem.id]); + revalidateTag(TAGS.cart); + } else { + return 'Item not found in cart'; + } } catch (e) { return 'Error removing item from cart'; } @@ -50,47 +53,69 @@ export async function removeItem(prevState: any, lineId: string) { export async function updateItemQuantity( prevState: any, payload: { - lineId: string; - variantId: string; + merchandiseId: string; quantity: number; } ) { - const cartId = cookies().get('cartId')?.value; + cartId = cartId || cookies().get('cartId')?.value; if (!cartId) { return 'Missing cart ID'; } - const { lineId, variantId, quantity } = payload; + const { merchandiseId, quantity } = payload; try { - if (quantity === 0) { - await removeFromCart(cartId, [lineId]); - revalidateTag(TAGS.cart); - return; + const cart = await getCart(cartId); + + if (!cart) { + return 'Error fetching cart'; } - await updateCart(cartId, [ - { - id: lineId, - merchandiseId: variantId, - quantity + const lineItem = cart.lines.find((line) => line.merchandise.id === merchandiseId); + + if (lineItem) { + if (quantity === 0) { + await removeFromCart(cartId, [lineItem.id]); + } else { + await updateCart(cartId, [ + { + id: lineItem.id, + merchandiseId, + quantity + } + ]); } - ]); + } else if (quantity > 0) { + // If the item doesn't exist in the cart and quantity > 0, add it + await addToCart(cartId, [{ merchandiseId, quantity }]); + } + revalidateTag(TAGS.cart); } catch (e) { - console.log(e); + console.error(e); return 'Error updating item quantity'; } } export async function redirectToCheckout() { - let cartId = cookies().get('cartId')?.value; + cartId = cartId || cookies().get('cartId')?.value; + + if (!cartId) { + return 'Missing cart ID'; + } + let cart = await getCart(cartId); if (!cart) { - return; + return 'Error fetching cart'; } redirect(cart.checkoutUrl); } + +export async function createCartAndSetCookie() { + let cart = await createCart(); + cartId = cart.id; + cookies().set('cartId', cartId); +} diff --git a/components/cart/cart-context.tsx b/components/cart/cart-context.tsx index 09c9fc22d..71679941b 100644 --- a/components/cart/cart-context.tsx +++ b/components/cart/cart-context.tsx @@ -6,12 +6,12 @@ import React, { createContext, use, useContext, useMemo, useOptimistic } from 'r type UpdateType = 'plus' | 'minus' | 'delete'; type CartAction = - | { type: 'UPDATE_ITEM'; payload: { itemId: string; updateType: UpdateType } } + | { type: 'UPDATE_ITEM'; payload: { merchandiseId: string; updateType: UpdateType } } | { type: 'ADD_ITEM'; payload: { variant: ProductVariant; product: Product } }; type CartContextType = { - cart: Cart | undefined; - updateCartItem: (itemId: string, updateType: UpdateType) => void; + cart: Cart | null; + updateCartItem: (merchandiseId: string, updateType: UpdateType) => void; addCartItem: (variant: ProductVariant, product: Product) => void; }; @@ -89,40 +89,59 @@ function updateCartTotals(lines: CartItem[]): Pick (item.id === itemId ? updateCartItem(item, updateType) : item)) + const { merchandiseId, updateType } = action.payload; + const updatedLines = currentCart.lines + .map((item) => + item.merchandise.id === merchandiseId ? updateCartItem(item, updateType) : item + ) .filter(Boolean) as CartItem[]; if (updatedLines.length === 0) { return { - ...state, + ...currentCart, lines: [], totalQuantity: 0, - cost: { ...state.cost, totalAmount: { ...state.cost.totalAmount, amount: '0' } } + cost: { + ...currentCart.cost, + totalAmount: { ...currentCart.cost.totalAmount, amount: '0' } + } }; } - return { ...state, ...updateCartTotals(updatedLines), lines: updatedLines }; + return { ...currentCart, ...updateCartTotals(updatedLines), lines: updatedLines }; } case 'ADD_ITEM': { const { variant, product } = action.payload; - const existingItem = state.lines.find((item) => item.merchandise.id === variant.id); + const existingItem = currentCart.lines.find((item) => item.merchandise.id === variant.id); const updatedItem = createOrUpdateCartItem(existingItem, variant, product); const updatedLines = existingItem - ? state.lines.map((item) => (item.merchandise.id === variant.id ? updatedItem : item)) - : [...state.lines, updatedItem]; + ? currentCart.lines.map((item) => (item.merchandise.id === variant.id ? updatedItem : item)) + : [...currentCart.lines, updatedItem]; - return { ...state, ...updateCartTotals(updatedLines), lines: updatedLines }; + return { ...currentCart, ...updateCartTotals(updatedLines), lines: updatedLines }; } default: - return state; + return currentCart; } } @@ -131,13 +150,13 @@ export function CartProvider({ cartPromise }: { children: React.ReactNode; - cartPromise: Promise; + cartPromise: Promise; }) { const initialCart = use(cartPromise); const [optimisticCart, updateOptimisticCart] = useOptimistic(initialCart, cartReducer); - const updateCartItem = (itemId: string, updateType: UpdateType) => { - updateOptimisticCart({ type: 'UPDATE_ITEM', payload: { itemId, updateType } }); + const updateCartItem = (merchandiseId: string, updateType: UpdateType) => { + updateOptimisticCart({ type: 'UPDATE_ITEM', payload: { merchandiseId, updateType } }); }; const addCartItem = (variant: ProductVariant, product: Product) => { diff --git a/components/cart/delete-item-button.tsx b/components/cart/delete-item-button.tsx index fc8d04844..d3cbc5ec7 100644 --- a/components/cart/delete-item-button.tsx +++ b/components/cart/delete-item-button.tsx @@ -13,13 +13,13 @@ export function DeleteItemButton({ optimisticUpdate: any; }) { const [message, formAction] = useFormState(removeItem, null); - const itemId = item.id; - const actionWithVariant = formAction.bind(null, itemId); + const merchandiseId = item.merchandise.id; + const actionWithVariant = formAction.bind(null, merchandiseId); return (
{ - optimisticUpdate(itemId, 'delete'); + optimisticUpdate(merchandiseId, 'delete'); await actionWithVariant(); }} > diff --git a/components/cart/edit-item-quantity-button.tsx b/components/cart/edit-item-quantity-button.tsx index dfb64f5a5..28ac78ba5 100644 --- a/components/cart/edit-item-quantity-button.tsx +++ b/components/cart/edit-item-quantity-button.tsx @@ -38,8 +38,7 @@ export function EditItemQuantityButton({ }) { const [message, formAction] = useFormState(updateItemQuantity, null); const payload = { - lineId: item.id, - variantId: item.merchandise.id, + merchandiseId: item.merchandise.id, quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1 }; const actionWithVariant = formAction.bind(null, payload); @@ -47,7 +46,7 @@ export function EditItemQuantityButton({ return ( { - optimisticUpdate(payload.lineId, type); + optimisticUpdate(payload.merchandiseId, type); await actionWithVariant(); }} > diff --git a/components/cart/modal.tsx b/components/cart/modal.tsx index 69d47d95c..56bfb8e00 100644 --- a/components/cart/modal.tsx +++ b/components/cart/modal.tsx @@ -10,7 +10,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { Fragment, useEffect, useRef, useState } from 'react'; import { useFormStatus } from 'react-dom'; -import { redirectToCheckout } from './actions'; +import { createCartAndSetCookie, redirectToCheckout } from './actions'; import { useCart } from './cart-context'; import CloseCart from './close-cart'; import { DeleteItemButton } from './delete-item-button'; @@ -29,7 +29,17 @@ export default function CartModal() { const closeCart = () => setIsOpen(false); useEffect(() => { - if (cart?.totalQuantity !== quantityRef.current) { + if (!cart) { + createCartAndSetCookie(); + } + }, [cart]); + + useEffect(() => { + if ( + cart?.totalQuantity && + cart?.totalQuantity !== quantityRef.current && + cart?.totalQuantity > 0 + ) { if (!isOpen) { setIsOpen(true); } diff --git a/components/welcome-toast.tsx b/components/welcome-toast.tsx index 616333770..9475fafd3 100644 --- a/components/welcome-toast.tsx +++ b/components/welcome-toast.tsx @@ -13,7 +13,7 @@ export function WelcomeToast() { id: 'welcome-toast', duration: Infinity, onDismiss: () => { - document.cookie += 'welcome-toast=2;max-age=31536000'; + document.cookie = 'welcome-toast=2; max-age=31536000; path=/'; }, description: ( <> diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index d126099c2..9eca48bc0 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -262,8 +262,7 @@ export async function getCart(cartId: string | undefined): Promise({ query: getCartQuery, variables: { cartId }, - tags: [TAGS.cart], - cache: 'no-store' + tags: [TAGS.cart] }); // Old carts becomes `null` when you checkout.