From ce9634083344eed01b50885e9cd1e60b08da8969 Mon Sep 17 00:00:00 2001 From: paolosantarsiero Date: Thu, 26 Dec 2024 21:35:53 +0100 Subject: [PATCH] feat: add login and cart provider --- app/[page]/opengraph-image.tsx | 4 +- app/[page]/page.tsx | 23 +- app/api/auth/[...nextauth]/route.ts | 50 ++ app/api/cart/route.ts | 54 +++ app/api/revalidate/route.ts | 6 - app/checkout/page.tsx | 9 + app/collection/[name]/page.tsx | 18 + app/layout.tsx | 29 +- app/page.tsx | 4 +- app/product/[handle]/page.tsx | 149 ------ app/product/[name]/page.tsx | 97 ++++ app/search/[collection]/opengraph-image.tsx | 5 +- app/search/layout.tsx | 2 - app/search/page.tsx | 4 +- app/sitemap.ts | 53 +- components/carousel.tsx | 19 +- components/cart/actions.ts | 109 ----- components/cart/add-to-cart.tsx | 66 +-- components/cart/cart-context.tsx | 179 +------ components/cart/delete-item-button.tsx | 22 +- components/cart/edit-item-quantity-button.tsx | 26 +- components/cart/modal.tsx | 95 ++-- components/cart/open-cart.tsx | 2 +- components/grid/three-items.tsx | 31 +- components/layout/footer.tsx | 17 +- components/layout/navbar/index.tsx | 25 +- components/layout/product-grid-items.tsx | 16 +- components/login/modal.tsx | 133 +++++ components/next-session-provider.tsx | 11 + components/price.tsx | 5 +- components/product/gallery.tsx | 4 +- components/product/product-description.tsx | 14 +- lib/shopify/fragments/cart.ts | 53 -- lib/shopify/fragments/image.ts | 10 - lib/shopify/fragments/product.ts | 64 --- lib/shopify/fragments/seo.ts | 8 - lib/shopify/index.ts | 455 ------------------ lib/shopify/mutations/cart.ts | 45 -- lib/shopify/queries/cart.ts | 10 - lib/shopify/queries/collection.ts | 56 --- lib/shopify/queries/menu.ts | 10 - lib/shopify/queries/page.ts | 41 -- lib/shopify/queries/product.ts | 32 -- lib/shopify/types.ts | 272 ----------- lib/woocomerce/models/base.ts | 49 ++ lib/woocomerce/models/billing.ts | 13 + lib/woocomerce/models/cart.ts | 262 ++++++++++ lib/woocomerce/models/client.ts | 430 +++++++++++++++++ lib/woocomerce/models/clientOptions.ts | 87 ++++ lib/woocomerce/models/coupon.ts | 41 ++ lib/woocomerce/models/customer.ts | 23 + lib/woocomerce/models/fee.ts | 13 + lib/woocomerce/models/item.ts | 19 + lib/woocomerce/models/link.ts | 11 + lib/woocomerce/models/orders.ts | 83 ++++ lib/woocomerce/models/payment.ts | 36 ++ lib/woocomerce/models/product.ts | 221 +++++++++ lib/woocomerce/models/refund.ts | 32 ++ lib/woocomerce/models/shipping.ts | 79 +++ lib/woocomerce/models/taxes.ts | 48 ++ lib/woocomerce/models/webhooks.ts | 32 ++ lib/woocomerce/storeApi.ts | 56 +++ lib/woocomerce/woocommerce.ts | 11 + next.config.ts | 5 +- package.json | 7 +- pnpm-lock.yaml | 237 +++++++++ tsconfig.json | 2 +- types/next-auth.d.ts | 31 ++ 68 files changed, 2421 insertions(+), 1744 deletions(-) create mode 100644 app/api/auth/[...nextauth]/route.ts create mode 100644 app/api/cart/route.ts delete mode 100644 app/api/revalidate/route.ts create mode 100644 app/checkout/page.tsx create mode 100644 app/collection/[name]/page.tsx delete mode 100644 app/product/[handle]/page.tsx create mode 100644 app/product/[name]/page.tsx delete mode 100644 components/cart/actions.ts create mode 100644 components/login/modal.tsx create mode 100644 components/next-session-provider.tsx delete mode 100644 lib/shopify/fragments/cart.ts delete mode 100644 lib/shopify/fragments/image.ts delete mode 100644 lib/shopify/fragments/product.ts delete mode 100644 lib/shopify/fragments/seo.ts delete mode 100644 lib/shopify/index.ts delete mode 100644 lib/shopify/mutations/cart.ts delete mode 100644 lib/shopify/queries/cart.ts delete mode 100644 lib/shopify/queries/collection.ts delete mode 100644 lib/shopify/queries/menu.ts delete mode 100644 lib/shopify/queries/page.ts delete mode 100644 lib/shopify/queries/product.ts delete mode 100644 lib/shopify/types.ts create mode 100644 lib/woocomerce/models/base.ts create mode 100644 lib/woocomerce/models/billing.ts create mode 100644 lib/woocomerce/models/cart.ts create mode 100644 lib/woocomerce/models/client.ts create mode 100644 lib/woocomerce/models/clientOptions.ts create mode 100644 lib/woocomerce/models/coupon.ts create mode 100644 lib/woocomerce/models/customer.ts create mode 100644 lib/woocomerce/models/fee.ts create mode 100644 lib/woocomerce/models/item.ts create mode 100644 lib/woocomerce/models/link.ts create mode 100644 lib/woocomerce/models/orders.ts create mode 100644 lib/woocomerce/models/payment.ts create mode 100644 lib/woocomerce/models/product.ts create mode 100644 lib/woocomerce/models/refund.ts create mode 100644 lib/woocomerce/models/shipping.ts create mode 100644 lib/woocomerce/models/taxes.ts create mode 100644 lib/woocomerce/models/webhooks.ts create mode 100644 lib/woocomerce/storeApi.ts create mode 100644 lib/woocomerce/woocommerce.ts create mode 100644 types/next-auth.d.ts diff --git a/app/[page]/opengraph-image.tsx b/app/[page]/opengraph-image.tsx index 2fd59281e..f08b01d4b 100644 --- a/app/[page]/opengraph-image.tsx +++ b/app/[page]/opengraph-image.tsx @@ -1,11 +1,9 @@ import OpengraphImage from 'components/opengraph-image'; -import { getPage } from 'lib/shopify'; export const runtime = 'edge'; export default async function Image({ params }: { params: { page: string } }) { - const page = await getPage(params.page); - const title = page.seo?.title || page.title; + const title = ''; return await OpengraphImage({ title }); } diff --git a/app/[page]/page.tsx b/app/[page]/page.tsx index aa0c15603..a7e8aac6e 100644 --- a/app/[page]/page.tsx +++ b/app/[page]/page.tsx @@ -1,23 +1,19 @@ import type { Metadata } from 'next'; import Prose from 'components/prose'; -import { getPage } from 'lib/shopify'; -import { notFound } from 'next/navigation'; export async function generateMetadata(props: { params: Promise<{ page: string }>; }): Promise { const params = await props.params; - const page = await getPage(params.page); - if (!page) return notFound(); return { - title: page.seo?.title || page.title, - description: page.seo?.description || page.bodySummary, + title: '', + description: '', openGraph: { - publishedTime: page.createdAt, - modifiedTime: page.updatedAt, + publishedTime: '', + modifiedTime: '', type: 'article' } }; @@ -25,20 +21,17 @@ export async function generateMetadata(props: { export default async function Page(props: { params: Promise<{ page: string }> }) { const params = await props.params; - const page = await getPage(params.page); - - if (!page) return notFound(); - + return ( <> -

{page.title}

- +

{''}

+

{`This document was last updated on ${new Intl.DateTimeFormat(undefined, { year: 'numeric', month: 'long', day: 'numeric' - }).format(new Date(page.updatedAt))}.`} + }).format(new Date())}.`}

); diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 000000000..c67dad098 --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,50 @@ +import { woocommerce } from "lib/woocomerce/woocommerce"; +import { NextAuthOptions, Session, User } from "next-auth"; +import { JWT } from "next-auth/jwt"; +import NextAuth from "next-auth/next"; +import CredentialsProvider from 'next-auth/providers/credentials'; + +export const authOptions = { + secret: process.env.NEXTAUTH_SECRET, + session: { + strategy: "jwt", // Use JWT for session handling + }, + providers: [ + CredentialsProvider({ + name: 'woocommerce', + credentials: { + username: { label: 'Username', type: 'text', placeholder: 'Username' }, + password: { label: 'Password', type: 'password', placeholder: 'Password' }, + }, + async authorize(credentials, req) { + if (!credentials?.username || !credentials?.password) { + return null; + } + const user = await woocommerce.login(credentials.username, credentials.password); + // If no error and we have user data, return it + if (user) { + return user + } + // Return null if user data could not be retrieved + return null + } + }), + ], + callbacks: { + async jwt({ token, user }: { token: JWT, user: User }) { + if (user) { + console.debug('Set token user', user); + token.user = user; + } + return token; + }, + async session({ session, token }: {session: Session, token: JWT}) { + console.debug('Set session token', token.user); + session.user = token.user; + return session; + }, + }, +} satisfies NextAuthOptions; + +const handler = NextAuth(authOptions) +export { handler as GET, handler as POST }; diff --git a/app/api/cart/route.ts b/app/api/cart/route.ts new file mode 100644 index 000000000..81580c68f --- /dev/null +++ b/app/api/cart/route.ts @@ -0,0 +1,54 @@ +import { storeApi } from 'lib/woocomerce/storeApi'; +import { getServerSession } from 'next-auth'; +import { NextRequest, NextResponse } from 'next/server'; +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(''); + } + const cart = await storeApi.getCart(); + return NextResponse.json(cart, { status: 200 }); + } catch (error) { + return NextResponse.json({ error: 'Failed to fetch cart' }, { status: 500 }); + } +} + +export async function POST(req: NextRequest) { + try { + const { id, quantity, variation } = await req.json(); + 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 }); + } +} + +export async function PUT(req: NextRequest) { + try { + const { key, quantity } = await req.json(); + if (quantity > 0) { + const cart = await storeApi.updateItem({ key, quantity }); + return NextResponse.json(cart, { status: 200 }); + } else { + const cart = await storeApi.removeFromCart({ key }); + return NextResponse.json(cart, { status: 200 }); + } + } catch (error) { + return NextResponse.json({ error: 'Failed to update cart item' }, { status: 500 }); + } +} + +export async function DELETE(req: NextRequest) { + try { + const { key } = await req.json(); + 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 }); + } +} \ No newline at end of file diff --git a/app/api/revalidate/route.ts b/app/api/revalidate/route.ts deleted file mode 100644 index 4ecc0b45d..000000000 --- a/app/api/revalidate/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { revalidate } from 'lib/shopify'; -import { NextRequest, NextResponse } from 'next/server'; - -export async function POST(req: NextRequest): Promise { - return revalidate(req); -} diff --git a/app/checkout/page.tsx b/app/checkout/page.tsx new file mode 100644 index 000000000..43fa7df81 --- /dev/null +++ b/app/checkout/page.tsx @@ -0,0 +1,9 @@ +export default async function CheckoutPage(props: { params: Promise<{ id: number }> }) { + const params = await props.params; + + return ( +
+

Checkout

+
+ ); +} \ No newline at end of file diff --git a/app/collection/[name]/page.tsx b/app/collection/[name]/page.tsx new file mode 100644 index 000000000..5f27debc5 --- /dev/null +++ b/app/collection/[name]/page.tsx @@ -0,0 +1,18 @@ + +import { ThreeItemGridItem } from 'components/grid/three-items'; +import { Product } from 'lib/woocomerce/models/product'; +import { woocommerce } from 'lib/woocomerce/woocommerce'; + + +export default async function ProductPage(props: { params: Promise<{ name: string }> }) { + const params = await props.params; + const products: Product[] = (await (woocommerce.get('products', { category: params.name }))); + + return ( +
+ {products.map((product, index) => ( + + ))} +
+ ); +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 348adcecb..469859939 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,10 +1,11 @@ + import { CartProvider } from 'components/cart/cart-context'; import { Navbar } from 'components/layout/navbar'; +import { NextAuthProvider } from 'components/next-session-provider'; import { WelcomeToast } from 'components/welcome-toast'; import { GeistSans } from 'geist/font/sans'; -import { getCart } from 'lib/shopify'; import { ensureStartsWith } from 'lib/utils'; -import { cookies } from 'next/headers'; +import { storeApi } from 'lib/woocomerce/storeApi'; import { ReactNode } from 'react'; import { Toaster } from 'sonner'; import './globals.css'; @@ -37,21 +38,21 @@ export const metadata = { }; export default async function RootLayout({ children }: { children: ReactNode }) { - const cartId = (await cookies()).get('cartId')?.value; - // Don't await the fetch, pass the Promise to the context provider - const cart = getCart(cartId); - + const cart = await storeApi.getCart(); + return ( - - -
- {children} - - -
-
+ + + +
+ {children} + + +
+
+
); diff --git a/app/page.tsx b/app/page.tsx index 7d407ede8..57cec6345 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -9,10 +9,10 @@ export const metadata = { } }; -export default function HomePage() { +export default async function HomePage() { return ( <> - +