diff --git a/app/api/cart/route.ts b/app/api/cart/route.ts deleted file mode 100644 index 15f5fb09f..000000000 --- a/app/api/cart/route.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { cookies } from 'next/headers'; -import { NextRequest, NextResponse } from 'next/server'; - -import { addToCart, removeFromCart, updateCart } from 'lib/shopify'; -import { isShopifyError } from 'lib/type-guards'; - -function formatErrorMessage(err: Error): string { - return JSON.stringify(err, Object.getOwnPropertyNames(err)); -} - -export async function POST(req: NextRequest): Promise { - const cartId = cookies().get('cartId')?.value; - const { merchandiseId } = await req.json(); - - if (!cartId?.length || !merchandiseId?.length) { - return NextResponse.json({ error: 'Missing cartId or variantId' }, { status: 400 }); - } - try { - await addToCart(cartId, [{ merchandiseId, quantity: 1 }]); - return NextResponse.json({ status: 204 }); - } catch (e) { - if (isShopifyError(e)) { - return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status }); - } - - return NextResponse.json({ status: 500 }); - } -} - -export async function PUT(req: NextRequest): Promise { - const cartId = cookies().get('cartId')?.value; - const { variantId, quantity, lineId } = await req.json(); - - if (!cartId || !variantId || !quantity || !lineId) { - return NextResponse.json( - { error: 'Missing cartId, variantId, lineId, or quantity' }, - { status: 400 } - ); - } - try { - await updateCart(cartId, [ - { - id: lineId, - merchandiseId: variantId, - quantity - } - ]); - return NextResponse.json({ status: 204 }); - } catch (e) { - if (isShopifyError(e)) { - return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status }); - } - - return NextResponse.json({ status: 500 }); - } -} - -export async function DELETE(req: NextRequest): Promise { - const cartId = cookies().get('cartId')?.value; - const { lineId } = await req.json(); - - if (!cartId || !lineId) { - return NextResponse.json({ error: 'Missing cartId or lineId' }, { status: 400 }); - } - try { - await removeFromCart(cartId, [lineId]); - return NextResponse.json({ status: 204 }); - } catch (e) { - if (isShopifyError(e)) { - return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status }); - } - - return NextResponse.json({ status: 500 }); - } -} diff --git a/app/product/[handle]/page.tsx b/app/product/[handle]/page.tsx index 983857bdb..45286cdbe 100644 --- a/app/product/[handle]/page.tsx +++ b/app/product/[handle]/page.tsx @@ -5,7 +5,7 @@ import { Suspense } from 'react'; import Grid from 'components/grid'; import Footer from 'components/layout/footer'; import ProductGridItems from 'components/layout/product-grid-items'; -import { AddToCart } from 'components/product/add-to-cart'; +import { AddToCart } from 'components/cart/add-to-cart'; import { Gallery } from 'components/product/gallery'; import { VariantSelector } from 'components/product/variant-selector'; import Prose from 'components/prose'; diff --git a/components/cart/actions.ts b/components/cart/actions.ts new file mode 100644 index 000000000..5e5f2b2ea --- /dev/null +++ b/components/cart/actions.ts @@ -0,0 +1,57 @@ +'use server'; + +import { addToCart, removeFromCart, updateCart } from 'lib/shopify'; +import { cookies } from 'next/headers'; + +export const addItem = async (variantId: string | undefined): Promise => { + const cartId = cookies().get('cartId')?.value; + + if (!cartId || !variantId) { + return new Error('Missing cartId or variantId'); + } + try { + await addToCart(cartId, [{ merchandiseId: variantId, quantity: 1 }]); + } catch (e) { + return new Error('Error adding item', { cause: e }); + } +}; + +export const removeItem = async (lineId: string): Promise => { + const cartId = cookies().get('cartId')?.value; + + if (!cartId) { + return new Error('Missing cartId'); + } + try { + await removeFromCart(cartId, [lineId]); + } catch (e) { + return new Error('Error removing item', { cause: e }); + } +}; + +export const updateItemQuantity = async ({ + lineId, + variantId, + quantity +}: { + lineId: string; + variantId: string; + quantity: number; +}): Promise => { + const cartId = cookies().get('cartId')?.value; + + if (!cartId) { + return new Error('Missing cartId'); + } + try { + await updateCart(cartId, [ + { + id: lineId, + merchandiseId: variantId, + quantity + } + ]); + } catch (e) { + return new Error('Error updating item quantity', { cause: e }); + } +}; diff --git a/components/product/add-to-cart.tsx b/components/cart/add-to-cart.tsx similarity index 66% rename from components/product/add-to-cart.tsx rename to components/cart/add-to-cart.tsx index 9da11bafa..eecee848e 100644 --- a/components/product/add-to-cart.tsx +++ b/components/cart/add-to-cart.tsx @@ -1,6 +1,7 @@ 'use client'; import clsx from 'clsx'; +import { addItem } from 'components/cart/actions'; import { useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useState, useTransition } from 'react'; @@ -18,7 +19,6 @@ export function AddToCart({ const router = useRouter(); const searchParams = useSearchParams(); const [isPending, startTransition] = useTransition(); - const [adding, setAdding] = useState(false); useEffect(() => { const variant = variants.find((variant: ProductVariant) => @@ -32,49 +32,33 @@ export function AddToCart({ } }, [searchParams, variants, setSelectedVariantId]); - const isMutating = adding || isPending; - - async function handleAdd() { - if (!availableForSale) return; - - setAdding(true); - - const response = await fetch(`/api/cart`, { - method: 'POST', - body: JSON.stringify({ - merchandiseId: selectedVariantId - }) - }); - - const data = await response.json(); - - if (data.error) { - alert(data.error); - return; - } - - setAdding(false); - - startTransition(() => { - router.refresh(); - }); - } - return ( ); } diff --git a/components/cart/delete-item-button.tsx b/components/cart/delete-item-button.tsx index 789a87754..4684f5a92 100644 --- a/components/cart/delete-item-button.tsx +++ b/components/cart/delete-item-button.tsx @@ -1,50 +1,40 @@ import CloseIcon from 'components/icons/close'; import LoadingDots from 'components/loading-dots'; import { useRouter } from 'next/navigation'; -import { startTransition, useState } from 'react'; import clsx from 'clsx'; import type { CartItem } from 'lib/shopify/types'; +import { useTransition } from 'react'; +import { removeItem } from 'components/cart/actions'; export default function DeleteItemButton({ item }: { item: CartItem }) { const router = useRouter(); - const [removing, setRemoving] = useState(false); + const [isPending, startTransition] = useTransition(); - async function handleRemove() { - setRemoving(true); - - const response = await fetch(`/api/cart`, { - method: 'DELETE', - body: JSON.stringify({ - lineId: item.id - }) - }); - const data = await response.json(); - - if (data.error) { - alert(data.error); - return; - } - - setRemoving(false); - - startTransition(() => { - router.refresh(); - }); - } return (