From 87dd5ef8e874287cd4cfab57d9595ff4e0065094 Mon Sep 17 00:00:00 2001 From: paolosantarsiero Date: Sun, 29 Dec 2024 15:02:58 +0100 Subject: [PATCH] feat: handle product variations --- app/api/auth/[...nextauth]/route.ts | 10 + app/api/cart/route.ts | 12 +- app/api/customer/route.ts | 12 + app/layout.tsx | 5 +- app/login/page.tsx | 23 +- app/product/[name]/page.tsx | 14 +- app/profile/orders/[id]/page.tsx | 1 - app/search/page.tsx | 4 +- app/signup/page.tsx | 144 ++ components/button/logout.tsx | 7 +- components/cart/add-to-cart.tsx | 29 +- components/cart/cart-context.tsx | 27 +- components/cart/modal.tsx | 5 + components/grid/three-items.tsx | 2 +- components/icons/UserIcon.tsx | 6 +- components/next-session-provider.tsx | 1 + components/product/product-context.tsx | 2 +- components/product/product-description.tsx | 2 - components/product/variant-selector.tsx | 68 +- lib/constants.ts | 16 +- lib/woocomerce/models/base.ts | 1 + lib/woocomerce/models/client.ts | 14 +- lib/woocomerce/models/product.ts | 3 +- lib/woocomerce/storeApi.ts | 28 +- lib/woocomerce/woocommerce.ts | 4 +- middleware.ts | 27 + package.json | 3 +- pnpm-lock.yaml | 1396 +++++--------------- 28 files changed, 705 insertions(+), 1161 deletions(-) create mode 100644 app/api/customer/route.ts create mode 100644 app/signup/page.tsx create mode 100644 middleware.ts diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index b01a0e33c..84cc46056 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,3 +1,4 @@ +import { storeApi } from 'lib/woocomerce/storeApi'; import { woocommerce } from 'lib/woocomerce/woocommerce'; import { NextAuthOptions, Session, User } from 'next-auth'; import { JWT } from 'next-auth/jwt'; @@ -42,6 +43,15 @@ export const authOptions = { console.debug('Set session token', token.user); session.user = token.user; return session; + }, + }, + events: { + async signIn() { + storeApi._seCartToken(''); + }, + async signOut() { + storeApi._seCartToken(''); + storeApi._setAuthorizationToken(''); } } } satisfies NextAuthOptions; diff --git a/app/api/cart/route.ts b/app/api/cart/route.ts index ce41d9faa..71374a6db 100644 --- a/app/api/cart/route.ts +++ b/app/api/cart/route.ts @@ -6,11 +6,7 @@ import { authOptions } from '../auth/[...nextauth]/route'; export async function GET(req: NextRequest) { try { const session = await getServerSession(authOptions); - if (session?.user?.token) { - storeApi._setAuthorizationToken(session.user.token); - } else { - storeApi._setAuthorizationToken(''); - } + storeApi._setAuthorizationToken(session?.user?.token ?? ''); const cart = await storeApi.getCart(); return NextResponse.json(cart, { status: 200 }); } catch (error) { @@ -24,7 +20,7 @@ export async function POST(req: NextRequest) { const cart = await storeApi.addToCart({ id, quantity, variation }); return NextResponse.json(cart, { status: 200 }); } catch (error) { - return NextResponse.json({ error: 'Failed to add item to cart' }, { status: 500 }); + return NextResponse.json({ error: 'Failed to add item to cart', message: JSON.stringify(error) }, { status: 500 }); } } @@ -39,7 +35,7 @@ export async function PUT(req: NextRequest) { return NextResponse.json(cart, { status: 200 }); } } catch (error) { - return NextResponse.json({ error: 'Failed to update cart item' }, { status: 500 }); + return NextResponse.json({ error: 'Failed to update cart item', message: JSON.stringify(error) }, { status: 500 }); } } @@ -49,6 +45,6 @@ export async function DELETE(req: NextRequest) { const cart = await storeApi.removeFromCart({ key }); return NextResponse.json(cart, { status: 200 }); } catch (error) { - return NextResponse.json({ error: 'Failed to remove item from cart' }, { status: 500 }); + return NextResponse.json({ error: 'Failed to remove item from cart', message: JSON.stringify(error) }, { status: 500 }); } } diff --git a/app/api/customer/route.ts b/app/api/customer/route.ts new file mode 100644 index 000000000..92dc984ef --- /dev/null +++ b/app/api/customer/route.ts @@ -0,0 +1,12 @@ +import { woocommerce } from "lib/woocomerce/woocommerce"; +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(req: NextRequest) { + try { + const data = await req.json(); + const cart = await woocommerce.post('customers', data); + return NextResponse.json(cart, { status: 200 }); + } catch (error) { + return NextResponse.json({ error: 'Failed to add item to cart' }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index a61c0153e..d312ffad3 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,7 +4,6 @@ import { NextAuthProvider } from 'components/next-session-provider'; import { WelcomeToast } from 'components/welcome-toast'; import { GeistSans } from 'geist/font/sans'; import { ensureStartsWith } from 'lib/utils'; -import { storeApi } from 'lib/woocomerce/storeApi'; import { ReactNode } from 'react'; import { Toaster } from 'sonner'; import './globals.css'; @@ -37,13 +36,11 @@ export const metadata = { }; export default async function RootLayout({ children }: { children: ReactNode }) { - const cart = await storeApi.getCart(); - return ( - +
{children} diff --git a/app/login/page.tsx b/app/login/page.tsx index 4dffb5264..fc83ee272 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -7,22 +7,24 @@ import { useState } from 'react'; export default function LoginPage() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); - const { replace } = useRouter(); + const [error, setError] = useState(''); + const router = useRouter(); const handleLogin = async (event: React.FormEvent) => { event.preventDefault(); - try { - await signIn('credentials', { username, password, redirect: false }); - replace('/'); - } catch (error) { - console.error(error); + const res = await signIn('credentials', { username, password, redirect: false, }); + if (res?.ok) { + router.replace('/'); + } else { + setError('Invalid username or password'); } }; return (

Login

-
+
+ {error &&

{error}

}
+ + + Don't have an account?{' '} + + Sign up + +
diff --git a/app/product/[name]/page.tsx b/app/product/[name]/page.tsx index 265e9f695..192b60225 100644 --- a/app/product/[name]/page.tsx +++ b/app/product/[name]/page.tsx @@ -1,13 +1,15 @@ import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; +import { AddToCart } from 'components/cart/add-to-cart'; import Footer from 'components/layout/footer'; import { Gallery } from 'components/product/gallery'; import { ProductProvider } from 'components/product/product-context'; import { ProductDescription } from 'components/product/product-description'; +import { VariantSelector } from 'components/product/variant-selector'; import { HIDDEN_PRODUCT_TAG } from 'lib/constants'; import { Image } from 'lib/woocomerce/models/base'; -import { Product } from 'lib/woocomerce/models/product'; +import { Product, ProductVariations } from 'lib/woocomerce/models/product'; import { woocommerce } from 'lib/woocomerce/woocommerce'; import { Suspense } from 'react'; @@ -42,6 +44,10 @@ export default async function ProductPage(props: { params: Promise<{ name: strin const product: Product | undefined = ( await woocommerce.get('products', { slug: params.name }) )?.[0]; + let variations: ProductVariations[] = []; + if (product?.variations?.length) { + variations = await woocommerce.get(`products/${product?.id}/variations`); + } if (!product) return notFound(); @@ -88,9 +94,15 @@ export default async function ProductPage(props: { params: Promise<{ name: strin
+ {variations && ( + + + + )} +
diff --git a/app/profile/orders/[id]/page.tsx b/app/profile/orders/[id]/page.tsx index 871fb71fa..d9d3a9502 100644 --- a/app/profile/orders/[id]/page.tsx +++ b/app/profile/orders/[id]/page.tsx @@ -9,7 +9,6 @@ export default async function OrderPage(props: { params: Promise<{ id: number }> const data = await getServerSession(authOptions); try { const order = await woocommerce.get('orders', { id: params.id }); - console.log(order); } catch (error) { console.error(error); } diff --git a/app/search/page.tsx b/app/search/page.tsx index 96ff941a6..3fff2222d 100644 --- a/app/search/page.tsx +++ b/app/search/page.tsx @@ -13,9 +13,9 @@ export default async function SearchPage(props: { }) { const searchParams = await props.searchParams; const { sort, q: searchValue } = searchParams as { [key: string]: string }; - const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort; + const { sortKey, order } = sorting.find((item) => item.slug === sort) || defaultSort; - const products = await woocommerce.get('products', { search: searchValue, orderby: sortKey }); + const products = await woocommerce.get('products', { search: searchValue, orderby: sortKey, order }); const resultsText = products.length > 1 ? 'results' : 'result'; return ( diff --git a/app/signup/page.tsx b/app/signup/page.tsx new file mode 100644 index 000000000..8f9084919 --- /dev/null +++ b/app/signup/page.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { useState } from 'react'; +import { z } from 'zod'; + +type FormData = { + username: string; + email: string; + password: string; + confirmPassword: string; +} + +const customerSchema = z.object({ + username: z.string().min(3), + email: z.string().email({ message: "Invalid email" }), + password: z.string(), + confirmPassword: z.string(), +}).refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], +});; + +export default function SignUpPage() { + const initialState = { username: '', email: '', password: '', confirmPassword: '' }; + const [formData, setFormData] = useState(initialState); + const [error, setError] = useState(initialState); + + const handleChange = (e: React.ChangeEvent) => { + setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value })); + } + + const handleSignup = async (event: React.FormEvent) => { + event.preventDefault(); + try { + customerSchema.parse(formData); + setError(initialState); + await fetch('/api/customer', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: formData.username, + first_name: '', + last_name: '', + email: formData.email, + password: formData.password + }), + }); + } catch (error) { + if (error instanceof z.ZodError) { + const errorObj: FormData = initialState; + error.errors.forEach((err) => { + const key = err.path[0] as keyof FormData; + errorObj[key] = err.message as string; + }); + console.log(errorObj); + setError(errorObj); + } + } + }; + + return ( +
+

Sign up

+
+
+
+ + + {error['username'] &&

{error['username']}

} +
+
+ + + {error['email'] &&

{error['email']}

} +
+
+ + + {error['password'] &&

{error['password']}

} +
+
+ + + {error['confirmPassword'] &&

{error['confirmPassword']}

} +
+
+ +
+
+
+
+ ); +} diff --git a/components/button/logout.tsx b/components/button/logout.tsx index c156d31e0..0c72c66ee 100644 --- a/components/button/logout.tsx +++ b/components/button/logout.tsx @@ -1,12 +1,17 @@ 'use client'; import { signOut } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; export default function LogoutButton() { + const router = useRouter(); return ( diff --git a/components/cart/add-to-cart.tsx b/components/cart/add-to-cart.tsx index 549356271..85cba14cd 100644 --- a/components/cart/add-to-cart.tsx +++ b/components/cart/add-to-cart.tsx @@ -2,15 +2,17 @@ import { PlusIcon } from '@heroicons/react/24/outline'; import clsx from 'clsx'; -import { Product } from 'lib/woocomerce/models/product'; +import { useProduct } from 'components/product/product-context'; +import { Product, ProductVariations } from 'lib/woocomerce/models/product'; +import { useMemo } from 'react'; import { useCart } from './cart-context'; -function SubmitButton({}: {}) { +function SubmitButton({disabled = false}: {disabled: boolean}) { const buttonClasses = 'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white'; return ( -