From 1f47796529f523138a676b24d7f85f239928bfd0 Mon Sep 17 00:00:00 2001 From: Lee Robinson Date: Tue, 10 Oct 2023 21:45:55 -0500 Subject: [PATCH] Improves form submissions and updates dependencies (#1209) --- components/cart/actions.ts | 46 +- components/cart/add-to-cart.tsx | 110 +- components/cart/delete-item-button.tsx | 54 +- components/cart/edit-item-quantity-button.tsx | 69 +- components/cart/modal.tsx | 4 +- components/layout/footer.tsx | 3 +- components/layout/navbar/search.tsx | 1 + components/layout/search/filter/index.tsx | 6 +- lib/constants.ts | 3 +- lib/shopify/index.ts | 1 + package.json | 32 +- pnpm-lock.yaml | 1432 ++++++++--------- prettier.config.js | 5 +- 13 files changed, 851 insertions(+), 915 deletions(-) diff --git a/components/cart/actions.ts b/components/cart/actions.ts index 3fb013194e..fa2c34d37d 100644 --- a/components/cart/actions.ts +++ b/components/cart/actions.ts @@ -1,9 +1,11 @@ 'use server'; +import { TAGS } from 'lib/constants'; import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/shopify'; +import { revalidateTag } from 'next/cache'; import { cookies } from 'next/headers'; -export const addItem = async (variantId: string | undefined): Promise => { +export async function addItem(prevState: any, selectedVariantId: string | undefined) { let cartId = cookies().get('cartId')?.value; let cart; @@ -17,45 +19,56 @@ export const addItem = async (variantId: string | undefined): Promise => { +export async function removeItem(prevState: any, lineId: string) { const cartId = cookies().get('cartId')?.value; if (!cartId) { return 'Missing cart ID'; } + try { await removeFromCart(cartId, [lineId]); + revalidateTag(TAGS.cart); } catch (e) { return 'Error removing item from cart'; } -}; +} -export const updateItemQuantity = async ({ - lineId, - variantId, - quantity -}: { - lineId: string; - variantId: string; - quantity: number; -}): Promise => { +export async function updateItemQuantity( + prevState: any, + payload: { + lineId: string; + variantId: string; + quantity: number; + } +) { const cartId = cookies().get('cartId')?.value; if (!cartId) { return 'Missing cart ID'; } + + const { lineId, variantId, quantity } = payload; + try { + if (quantity === 0) { + await removeFromCart(cartId, [lineId]); + revalidateTag(TAGS.cart); + return; + } + await updateCart(cartId, [ { id: lineId, @@ -63,7 +76,8 @@ export const updateItemQuantity = async ({ quantity } ]); + revalidateTag(TAGS.cart); } catch (e) { return 'Error updating item quantity'; } -}; +} diff --git a/components/cart/add-to-cart.tsx b/components/cart/add-to-cart.tsx index f9d21c0fda..b3b560b58a 100644 --- a/components/cart/add-to-cart.tsx +++ b/components/cart/add-to-cart.tsx @@ -5,8 +5,67 @@ import clsx from 'clsx'; import { addItem } from 'components/cart/actions'; import LoadingDots from 'components/loading-dots'; import { ProductVariant } from 'lib/shopify/types'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { useTransition } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { + // @ts-ignore + experimental_useFormState as useFormState, + experimental_useFormStatus as useFormStatus +} from 'react-dom'; + +function SubmitButton({ + availableForSale, + selectedVariantId +}: { + availableForSale: boolean; + selectedVariantId: string | undefined; +}) { + const { pending } = useFormStatus(); + const buttonClasses = + 'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white'; + const disabledClasses = 'cursor-not-allowed opacity-60 hover:opacity-60'; + + if (!availableForSale) { + return ( + + ); + } + + if (!selectedVariantId) { + return ( + + ); + } + + return ( + + ); +} export function AddToCart({ variants, @@ -15,9 +74,8 @@ export function AddToCart({ variants: ProductVariant[]; availableForSale: boolean; }) { - const router = useRouter(); + const [message, formAction] = useFormState(addItem, null); const searchParams = useSearchParams(); - const [isPending, startTransition] = useTransition(); const defaultVariantId = variants.length === 1 ? variants[0]?.id : undefined; const variant = variants.find((variant: ProductVariant) => variant.selectedOptions.every( @@ -25,44 +83,14 @@ export function AddToCart({ ) ); const selectedVariantId = variant?.id || defaultVariantId; - const title = !availableForSale - ? 'Out of stock' - : !selectedVariantId - ? 'Please select options' - : undefined; + const actionWithVariant = formAction.bind(null, selectedVariantId); return ( - +
+ +

+ {message} +

+ ); } diff --git a/components/cart/delete-item-button.tsx b/components/cart/delete-item-button.tsx index 605adcf512..3f95be0ac9 100644 --- a/components/cart/delete-item-button.tsx +++ b/components/cart/delete-item-button.tsx @@ -1,40 +1,35 @@ -import { XMarkIcon } from '@heroicons/react/24/outline'; -import LoadingDots from 'components/loading-dots'; -import { useRouter } from 'next/navigation'; +'use client'; +import { XMarkIcon } from '@heroicons/react/24/outline'; import clsx from 'clsx'; import { removeItem } from 'components/cart/actions'; +import LoadingDots from 'components/loading-dots'; import type { CartItem } from 'lib/shopify/types'; -import { useTransition } from 'react'; +import { + // @ts-ignore + experimental_useFormState as useFormState, + experimental_useFormStatus as useFormStatus +} from 'react-dom'; -export default function DeleteItemButton({ item }: { item: CartItem }) { - const router = useRouter(); - const [isPending, startTransition] = useTransition(); +function SubmitButton() { + const { pending } = useFormStatus(); return ( ); } + +export function DeleteItemButton({ item }: { item: CartItem }) { + const [message, formAction] = useFormState(removeItem, null); + const itemId = item.id; + const actionWithVariant = formAction.bind(null, itemId); + + return ( +
+ +

+ {message} +

+ + ); +} diff --git a/components/cart/edit-item-quantity-button.tsx b/components/cart/edit-item-quantity-button.tsx index e846196caa..87cade4cfb 100644 --- a/components/cart/edit-item-quantity-button.tsx +++ b/components/cart/edit-item-quantity-button.tsx @@ -1,54 +1,36 @@ -import { useRouter } from 'next/navigation'; -import { useTransition } from 'react'; +'use client'; import { MinusIcon, PlusIcon } from '@heroicons/react/24/outline'; import clsx from 'clsx'; -import { removeItem, updateItemQuantity } from 'components/cart/actions'; +import { updateItemQuantity } from 'components/cart/actions'; import LoadingDots from 'components/loading-dots'; import type { CartItem } from 'lib/shopify/types'; +import { + // @ts-ignore + experimental_useFormState as useFormState, + experimental_useFormStatus as useFormStatus +} from 'react-dom'; -export default function EditItemQuantityButton({ - item, - type -}: { - item: CartItem; - type: 'plus' | 'minus'; -}) { - const router = useRouter(); - const [isPending, startTransition] = useTransition(); +function SubmitButton({ type }: { type: 'plus' | 'minus' }) { + const { pending } = useFormStatus(); return ( ); } + +export function EditItemQuantityButton({ item, type }: { item: CartItem; type: 'plus' | 'minus' }) { + const [message, formAction] = useFormState(updateItemQuantity, null); + const payload = { + lineId: item.id, + variantId: item.merchandise.id, + quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1 + }; + const actionWithVariant = formAction.bind(null, payload); + + return ( +
+ +

+ {message} +

+ + ); +} diff --git a/components/cart/modal.tsx b/components/cart/modal.tsx index 4fbfcc6adf..aee2f7a47a 100644 --- a/components/cart/modal.tsx +++ b/components/cart/modal.tsx @@ -10,8 +10,8 @@ import Image from 'next/image'; import Link from 'next/link'; import { Fragment, useEffect, useRef, useState } from 'react'; import CloseCart from './close-cart'; -import DeleteItemButton from './delete-item-button'; -import EditItemQuantityButton from './edit-item-quantity-button'; +import { DeleteItemButton } from './delete-item-button'; +import { EditItemQuantityButton } from './edit-item-quantity-button'; import OpenCart from './open-cart'; type MerchandiseSearchParams = { diff --git a/components/layout/footer.tsx b/components/layout/footer.tsx index 8627ca28b3..6c60e6959a 100644 --- a/components/layout/footer.tsx +++ b/components/layout/footer.tsx @@ -58,9 +58,8 @@ export default async function Footer() {

Designed in California

- Crafted by{' '} - ▲ Vercel + Crafted by ▲ Vercel

diff --git a/components/layout/navbar/search.tsx b/components/layout/navbar/search.tsx index 57032a0357..03d152ec97 100644 --- a/components/layout/navbar/search.tsx +++ b/components/layout/navbar/search.tsx @@ -27,6 +27,7 @@ export default function Search() { return (