diff --git a/.changeset/ninety-hornets-allow.md b/.changeset/ninety-hornets-allow.md new file mode 100644 index 000000000..6d8dd8f26 --- /dev/null +++ b/.changeset/ninety-hornets-allow.md @@ -0,0 +1,5 @@ +--- +"@bigcommerce/catalyst-core": patch +--- + +add loading state on item quantity update and remove when quantity equals 0 diff --git a/.gitignore b/.gitignore index 2c00743a2..7fc5e5732 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ test-results/ playwright-report/ playwright/.cache/ graphql-env.d.ts +.DS_Store diff --git a/apps/core/app/[locale]/(default)/cart/_actions/remove-item.ts b/apps/core/app/[locale]/(default)/cart/_actions/remove-item.ts new file mode 100644 index 000000000..0bc382706 --- /dev/null +++ b/apps/core/app/[locale]/(default)/cart/_actions/remove-item.ts @@ -0,0 +1,47 @@ +'use server'; + +import { revalidateTag } from 'next/cache'; +import { cookies } from 'next/headers'; + +import { graphql } from '~/client/graphql'; +import { deleteCartLineItem } from '~/client/mutations/delete-cart-line-item'; + +type DeleteCartLineItemInput = ReturnType>; + +export async function removeItem({ + lineItemEntityId, +}: Omit) { + // export async function removeProduct(formData: FormData) { + try { + const cartId = cookies().get('cartId')?.value; + // const lineItemEntityId = formData.get('lineItemEntityId'); + + if (!cartId) { + return { status: 'error', error: 'No cartId cookie found' }; + } + + if (!lineItemEntityId) { + return { status: 'error', error: 'No lineItemEntityId found' }; + } + + const updatedCart = await deleteCartLineItem(cartId, lineItemEntityId); + + // If we remove the last item in a cart the cart is deleted + // so we need to remove the cartId cookie and clear shipping data + if (!updatedCart) { + cookies().delete('cartId'); + cookies().delete('shippingCosts'); + revalidateTag('cart'); + } + + revalidateTag('cart'); + + return { status: 'success', data: updatedCart }; + } catch (e: unknown) { + if (e instanceof Error) { + return { status: 'error', error: e.message }; + } + + return { status: 'error' }; + } +} diff --git a/apps/core/app/[locale]/(default)/cart/_actions/remove-products.ts b/apps/core/app/[locale]/(default)/cart/_actions/remove-products.ts deleted file mode 100644 index 71d2e5139..000000000 --- a/apps/core/app/[locale]/(default)/cart/_actions/remove-products.ts +++ /dev/null @@ -1,32 +0,0 @@ -'use server'; - -import { revalidateTag } from 'next/cache'; -import { cookies } from 'next/headers'; - -import { deleteCartLineItem } from '~/client/mutations/delete-cart-line-item'; - -export async function removeProduct(formData: FormData) { - const cartId = cookies().get('cartId')?.value; - - if (!cartId) { - throw new Error('No cartId cookie found'); - } - - const lineItemEntityId = formData.get('lineItemEntityId'); - - if (!lineItemEntityId) { - throw new Error('No lineItemEntityId found'); - } - - const updatedCart = await deleteCartLineItem(cartId, lineItemEntityId.toString()); - - // If we remove the last item in a cart the cart is deleted - // so we need to remove the cartId cookie and clear shipping data - if (!updatedCart) { - cookies().delete('cartId'); - cookies().delete('shippingCosts'); - revalidateTag('cart'); - } - - revalidateTag('cart'); -} diff --git a/apps/core/app/[locale]/(default)/cart/_actions/update-item-quantity.ts b/apps/core/app/[locale]/(default)/cart/_actions/update-item-quantity.ts new file mode 100644 index 000000000..fc6871c86 --- /dev/null +++ b/apps/core/app/[locale]/(default)/cart/_actions/update-item-quantity.ts @@ -0,0 +1,66 @@ +'use server'; + +import { revalidatePath } from 'next/cache'; +import { cookies } from 'next/headers'; + +import { graphql } from '~/client/graphql'; +import { updateCartLineItem } from '~/client/mutations/update-cart-line-item'; + +import { removeItem } from './remove-item'; + +type CartLineItemInput = ReturnType>; +type UpdateCartLineItemInput = ReturnType>; + +interface UpdateProductQuantityParams extends CartLineItemInput { + lineItemEntityId: UpdateCartLineItemInput['lineItemEntityId']; +} + +export async function updateItemQuantity({ + lineItemEntityId, + productEntityId, + quantity, + variantEntityId, + selectedOptions, +}: UpdateProductQuantityParams) { + try { + const cartId = cookies().get('cartId')?.value; + + if (!cartId) { + return { status: 'error', error: 'No cartId cookie found' }; + } + + if (!lineItemEntityId) { + return { status: 'error', error: 'No lineItemEntityId found' }; + } + + if (quantity === 0) { + const result = await removeItem({ lineItemEntityId }); + + return result; + } + + const cartLineItemData = Object.assign( + { quantity, productEntityId }, + variantEntityId && { variantEntityId }, + selectedOptions && { selectedOptions }, + ); + + const updatedCart = await updateCartLineItem(cartId, lineItemEntityId, { + lineItem: cartLineItemData, + }); + + if (!updatedCart) { + return { status: 'error', error: 'Failed to change product quantity in Cart' }; + } + + revalidatePath('/cart'); + + return { status: 'success', data: updatedCart }; + } catch (e: unknown) { + if (e instanceof Error) { + return { status: 'error', error: e.message }; + } + + return { status: 'error' }; + } +} diff --git a/apps/core/app/[locale]/(default)/cart/_actions/update-product-quantity.ts b/apps/core/app/[locale]/(default)/cart/_actions/update-product-quantity.ts deleted file mode 100644 index edbdb8ab5..000000000 --- a/apps/core/app/[locale]/(default)/cart/_actions/update-product-quantity.ts +++ /dev/null @@ -1,48 +0,0 @@ -'use server'; - -import { revalidatePath } from 'next/cache'; -import { cookies } from 'next/headers'; - -import { graphql } from '~/client/graphql'; -import { updateCartLineItem } from '~/client/mutations/update-cart-line-item'; - -type CartLineItemInput = ReturnType>; -type UpdateCartLineItemInput = ReturnType>; - -interface UpdateProductQuantityParams extends CartLineItemInput { - lineItemEntityId: UpdateCartLineItemInput['lineItemEntityId']; -} - -export async function updateProductQuantity({ - lineItemEntityId, - productEntityId, - quantity, - variantEntityId, - selectedOptions, -}: UpdateProductQuantityParams) { - const cartId = cookies().get('cartId')?.value; - - if (!cartId) { - throw new Error('No cartId cookie found'); - } - - if (!lineItemEntityId) { - throw new Error('No lineItemEntityId found'); - } - - const cartLineItemData = Object.assign( - { quantity, productEntityId }, - variantEntityId && { variantEntityId }, - selectedOptions && { selectedOptions }, - ); - - const updatedCart = await updateCartLineItem(cartId, lineItemEntityId, { - lineItem: cartLineItemData, - }); - - if (!updatedCart) { - throw new Error('Failed to change product quantity in Cart'); - } - - revalidatePath('/cart'); -} diff --git a/apps/core/app/[locale]/(default)/cart/_components/cart-item.tsx b/apps/core/app/[locale]/(default)/cart/_components/cart-item.tsx index 2255e9305..549bcc03b 100644 --- a/apps/core/app/[locale]/(default)/cart/_components/cart-item.tsx +++ b/apps/core/app/[locale]/(default)/cart/_components/cart-item.tsx @@ -1,13 +1,12 @@ -import { getTranslations } from 'next-intl/server'; +import { NextIntlClientProvider } from 'next-intl'; +import { getMessages } from 'next-intl/server'; import { getCart } from '~/client/queries/get-cart'; import { ExistingResultType } from '~/client/util'; import { BcImage } from '~/components/bc-image'; -import { removeProduct } from '../_actions/remove-products'; - -import { CartItemCounter } from './cart-item-counter'; -import { RemoveFromCartButton } from './remove-from-cart-button'; +import { ItemQuantity } from './item-quantity'; +import { RemoveItem } from './remove-item'; export type Product = | ExistingResultType['lineItems']['physicalItems'][number] @@ -16,11 +15,13 @@ export type Product = export const CartItem = async ({ currencyCode, product, + locale, }: { currencyCode: string; product: Product; + locale: string; }) => { - const t = await getTranslations('Cart'); + const messages = await getMessages({ locale }); const currencyFormatter = new Intl.NumberFormat('en-US', { style: 'currency', @@ -99,7 +100,9 @@ export const CartItem = async ({ )} - + + +

@@ -107,10 +110,9 @@ export const CartItem = async ({

-
- - - + + + ); diff --git a/apps/core/app/[locale]/(default)/cart/_components/cart-item-counter.tsx b/apps/core/app/[locale]/(default)/cart/_components/item-quantity.tsx similarity index 55% rename from apps/core/app/[locale]/(default)/cart/_components/cart-item-counter.tsx rename to apps/core/app/[locale]/(default)/cart/_components/item-quantity.tsx index 2917f1642..696797d5a 100644 --- a/apps/core/app/[locale]/(default)/cart/_components/cart-item-counter.tsx +++ b/apps/core/app/[locale]/(default)/cart/_components/item-quantity.tsx @@ -1,19 +1,19 @@ 'use client'; -import { Counter } from '@bigcommerce/components/counter'; -import { useState } from 'react'; +import { AlertCircle, Minus, Plus, Loader2 as Spinner } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import { ComponentPropsWithoutRef, useEffect, useState } from 'react'; +import { useFormStatus } from 'react-dom'; +import { toast } from 'react-hot-toast'; import { graphql } from '~/client/graphql'; import { getCart } from '~/client/queries/get-cart'; -import { updateProductQuantity } from '../_actions/update-product-quantity'; +import { updateItemQuantity } from '../_actions/update-item-quantity'; import { Product } from './cart-item'; -type CartLineItemInput = ReturnType>; type CartSelectedOptionsInput = ReturnType>; -type UpdateCartLineItemInput = ReturnType>; - type Cart = NonNullable>>; type CartItemData = Pick< @@ -23,10 +23,6 @@ type CartItemData = Pick< lineItemEntityId: string; }; -interface UpdateProductQuantityData extends CartLineItemInput { - lineItemEntityId: UpdateCartLineItemInput['lineItemEntityId']; -} - const parseSelectedOptions = (selectedOptions: CartItemData['selectedOptions']) => { return selectedOptions.reduce((accum, option) => { let multipleChoicesOptionInput; @@ -126,38 +122,90 @@ const parseSelectedOptions = (selectedOptions: CartItemData['selectedOptions']) }, {}); }; -export const CartItemCounter = ({ product }: { product: Product }) => { - const { quantity, entityId, productEntityId, variantEntityId, selectedOptions } = product; +const SubmitButton = ({ children, ...props }: ComponentPropsWithoutRef<'button'>) => { + const { pending } = useFormStatus(); + const t = useTranslations('Cart.SubmitItemQuantity'); - const [counterValue, setCounterValue] = useState<'' | number>(quantity); - const handleCountUpdate = async (value: string | number) => { - if (value === '') { - setCounterValue(value); + return ( + + ); +}; - return; - } +const Quantity = ({ value }: { value: number }) => { + const { pending } = useFormStatus(); + const t = useTranslations('Cart.SubmitItemQuantity'); - setCounterValue(Number(value)); + return ( + + {pending ? ( + <> + + ); +}; - const productData: UpdateProductQuantityData = Object.assign( - { - lineItemEntityId: entityId, - productEntityId, - quantity: Number(value), - selectedOptions: parseSelectedOptions(selectedOptions), - }, - variantEntityId && { variantEntityId }, - ); +export const ItemQuantity = ({ product }: { product: Product }) => { + const t = useTranslations('Cart.SubmitItemQuantity'); - await updateProductQuantity(productData); + const { quantity, entityId, productEntityId, variantEntityId, selectedOptions } = product; + const [productQuantity, setProductQuantity] = useState(quantity); + + useEffect(() => { + console.log(quantity); + setProductQuantity(quantity); + }, [quantity]); + + const onSubmit = async (formData: FormData) => { + const { status } = await updateItemQuantity({ + lineItemEntityId: entityId, + productEntityId, + quantity: Number(formData.get('quantity')), + selectedOptions: parseSelectedOptions(selectedOptions), + variantEntityId, + }); + + if (status === 'error') { + toast.error(t('errorMessage'), { + icon: , + }); + } }; + console.log(productQuantity, productEntityId); + return ( - +
+
+ setProductQuantity(productQuantity - 1)} + > + + + + + + setProductQuantity(productQuantity + 1)} + > + + +
); }; diff --git a/apps/core/app/[locale]/(default)/cart/_components/remove-from-cart-button.tsx b/apps/core/app/[locale]/(default)/cart/_components/remove-from-cart-button.tsx index c0bf530b4..16005e9c9 100644 --- a/apps/core/app/[locale]/(default)/cart/_components/remove-from-cart-button.tsx +++ b/apps/core/app/[locale]/(default)/cart/_components/remove-from-cart-button.tsx @@ -1,20 +1,16 @@ 'use client'; import { Loader2 as Spinner, Trash } from 'lucide-react'; +import { useTranslations } from 'next-intl'; import { useFormStatus } from 'react-dom'; -export const RemoveFromCartButton = ({ - label, - spinnerLabel, -}: { - label: string; - spinnerLabel: string; -}) => { +export const RemoveFromCartButton = () => { const { pending } = useFormStatus(); + const t = useTranslations('Cart.SubmitRemoveItem'); return (