mirror of
https://github.com/vercel/commerce.git
synced 2025-06-28 09:21:22 +00:00
feat: handle product variations
This commit is contained in:
parent
5880b80676
commit
87dd5ef8e8
@ -1,3 +1,4 @@
|
|||||||
|
import { storeApi } from 'lib/woocomerce/storeApi';
|
||||||
import { woocommerce } from 'lib/woocomerce/woocommerce';
|
import { woocommerce } from 'lib/woocomerce/woocommerce';
|
||||||
import { NextAuthOptions, Session, User } from 'next-auth';
|
import { NextAuthOptions, Session, User } from 'next-auth';
|
||||||
import { JWT } from 'next-auth/jwt';
|
import { JWT } from 'next-auth/jwt';
|
||||||
@ -42,6 +43,15 @@ export const authOptions = {
|
|||||||
console.debug('Set session token', token.user);
|
console.debug('Set session token', token.user);
|
||||||
session.user = token.user;
|
session.user = token.user;
|
||||||
return session;
|
return session;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
async signIn() {
|
||||||
|
storeApi._seCartToken('');
|
||||||
|
},
|
||||||
|
async signOut() {
|
||||||
|
storeApi._seCartToken('');
|
||||||
|
storeApi._setAuthorizationToken('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} satisfies NextAuthOptions;
|
} satisfies NextAuthOptions;
|
||||||
|
@ -6,11 +6,7 @@ import { authOptions } from '../auth/[...nextauth]/route';
|
|||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (session?.user?.token) {
|
storeApi._setAuthorizationToken(session?.user?.token ?? '');
|
||||||
storeApi._setAuthorizationToken(session.user.token);
|
|
||||||
} else {
|
|
||||||
storeApi._setAuthorizationToken('');
|
|
||||||
}
|
|
||||||
const cart = await storeApi.getCart();
|
const cart = await storeApi.getCart();
|
||||||
return NextResponse.json(cart, { status: 200 });
|
return NextResponse.json(cart, { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -24,7 +20,7 @@ export async function POST(req: NextRequest) {
|
|||||||
const cart = await storeApi.addToCart({ id, quantity, variation });
|
const cart = await storeApi.addToCart({ id, quantity, variation });
|
||||||
return NextResponse.json(cart, { status: 200 });
|
return NextResponse.json(cart, { status: 200 });
|
||||||
} catch (error) {
|
} 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 });
|
return NextResponse.json(cart, { status: 200 });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 });
|
const cart = await storeApi.removeFromCart({ key });
|
||||||
return NextResponse.json(cart, { status: 200 });
|
return NextResponse.json(cart, { status: 200 });
|
||||||
} catch (error) {
|
} 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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
app/api/customer/route.ts
Normal file
12
app/api/customer/route.ts
Normal file
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,6 @@ import { NextAuthProvider } from 'components/next-session-provider';
|
|||||||
import { WelcomeToast } from 'components/welcome-toast';
|
import { WelcomeToast } from 'components/welcome-toast';
|
||||||
import { GeistSans } from 'geist/font/sans';
|
import { GeistSans } from 'geist/font/sans';
|
||||||
import { ensureStartsWith } from 'lib/utils';
|
import { ensureStartsWith } from 'lib/utils';
|
||||||
import { storeApi } from 'lib/woocomerce/storeApi';
|
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
@ -37,13 +36,11 @@ export const metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({ children }: { children: ReactNode }) {
|
export default async function RootLayout({ children }: { children: ReactNode }) {
|
||||||
const cart = await storeApi.getCart();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={GeistSans.variable}>
|
<html lang="en" className={GeistSans.variable}>
|
||||||
<body className="bg-neutral-50 text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white">
|
<body className="bg-neutral-50 text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white">
|
||||||
<NextAuthProvider>
|
<NextAuthProvider>
|
||||||
<CartProvider value={cart}>
|
<CartProvider>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<main>
|
<main>
|
||||||
{children}
|
{children}
|
||||||
|
@ -7,22 +7,24 @@ import { useState } from 'react';
|
|||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const { replace } = useRouter();
|
const [error, setError] = useState('');
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handleLogin = async (event: React.FormEvent) => {
|
const handleLogin = async (event: React.FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
const res = await signIn('credentials', { username, password, redirect: false, });
|
||||||
await signIn('credentials', { username, password, redirect: false });
|
if (res?.ok) {
|
||||||
replace('/');
|
router.replace('/');
|
||||||
} catch (error) {
|
} else {
|
||||||
console.error(error);
|
setError('Invalid username or password');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mx-auto mt-4 grid max-w-screen-2xl justify-center gap-4 px-4 pb-4">
|
<section className="mx-auto mt-4 grid max-w-screen-2xl justify-center gap-4 px-4 pb-4">
|
||||||
<h1 className="text-2xl font-bold">Login</h1>
|
<h1 className="text-2xl font-bold">Login</h1>
|
||||||
<div className="flex h-screen justify-center">
|
<div className="flex flex-col h-screen w-full max-w-md">
|
||||||
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
<form onSubmit={handleLogin}>
|
<form onSubmit={handleLogin}>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<label
|
<label
|
||||||
@ -64,6 +66,13 @@ export default function LoginPage() {
|
|||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span className="block mt-6 text-center text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Don't have an account?{' '}
|
||||||
|
<a href="/signup" className="text-indigo-600 hover:underline">
|
||||||
|
Sign up
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { AddToCart } from 'components/cart/add-to-cart';
|
||||||
import Footer from 'components/layout/footer';
|
import Footer from 'components/layout/footer';
|
||||||
import { Gallery } from 'components/product/gallery';
|
import { Gallery } from 'components/product/gallery';
|
||||||
import { ProductProvider } from 'components/product/product-context';
|
import { ProductProvider } from 'components/product/product-context';
|
||||||
import { ProductDescription } from 'components/product/product-description';
|
import { ProductDescription } from 'components/product/product-description';
|
||||||
|
import { VariantSelector } from 'components/product/variant-selector';
|
||||||
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
||||||
import { Image } from 'lib/woocomerce/models/base';
|
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 { woocommerce } from 'lib/woocomerce/woocommerce';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
@ -42,6 +44,10 @@ export default async function ProductPage(props: { params: Promise<{ name: strin
|
|||||||
const product: Product | undefined = (
|
const product: Product | undefined = (
|
||||||
await woocommerce.get('products', { slug: params.name })
|
await woocommerce.get('products', { slug: params.name })
|
||||||
)?.[0];
|
)?.[0];
|
||||||
|
let variations: ProductVariations[] = [];
|
||||||
|
if (product?.variations?.length) {
|
||||||
|
variations = await woocommerce.get(`products/${product?.id}/variations`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!product) return notFound();
|
if (!product) return notFound();
|
||||||
|
|
||||||
@ -88,9 +94,15 @@ export default async function ProductPage(props: { params: Promise<{ name: strin
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="basis-full lg:basis-2/6">
|
<div className="basis-full lg:basis-2/6">
|
||||||
|
{variations && (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<VariantSelector options={product.attributes} variations={variations} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<ProductDescription product={product} />
|
<ProductDescription product={product} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
<AddToCart product={product} variations={variations}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,6 @@ export default async function OrderPage(props: { params: Promise<{ id: number }>
|
|||||||
const data = await getServerSession(authOptions);
|
const data = await getServerSession(authOptions);
|
||||||
try {
|
try {
|
||||||
const order = await woocommerce.get('orders', { id: params.id });
|
const order = await woocommerce.get('orders', { id: params.id });
|
||||||
console.log(order);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,9 @@ export default async function SearchPage(props: {
|
|||||||
}) {
|
}) {
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
const { sort, q: searchValue } = searchParams as { [key: string]: string };
|
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';
|
const resultsText = products.length > 1 ? 'results' : 'result';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
144
app/signup/page.tsx
Normal file
144
app/signup/page.tsx
Normal file
@ -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<FormData>(initialState);
|
||||||
|
const [error, setError] = useState<FormData>(initialState);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<section className="mx-auto mt-4 grid max-w-screen-2xl justify-center gap-4 px-4 pb-4">
|
||||||
|
<h1 className="text-2xl font-bold">Sign up</h1>
|
||||||
|
<div className="flex h-screen justify-center">
|
||||||
|
<form onSubmit={handleSignup}>
|
||||||
|
<div className="mt-4">
|
||||||
|
<label
|
||||||
|
htmlFor="username"
|
||||||
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
value={formData.username}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 p-3 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-lg"
|
||||||
|
/>
|
||||||
|
{error['username'] && <p className="text-red-500">{error['username']}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 p-3 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-lg"
|
||||||
|
/>
|
||||||
|
{error['email'] && <p className="text-red-500">{error['email']}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 p-3 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-lg"
|
||||||
|
/>
|
||||||
|
{error['password'] && <p className="text-red-500">{error['password']}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<label
|
||||||
|
htmlFor="confirmPassword"
|
||||||
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirmPassword"
|
||||||
|
value={formData.confirmPassword}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 p-3 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-lg"
|
||||||
|
/>
|
||||||
|
{error['confirmPassword'] && <p className="text-red-500">{error['confirmPassword']}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-3 text-lg font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
Sign up
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
@ -1,12 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { signOut } from 'next-auth/react';
|
import { signOut } from 'next-auth/react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export default function LogoutButton() {
|
export default function LogoutButton() {
|
||||||
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full rounded-md bg-indigo-500 p-3 text-white"
|
className="w-full rounded-md bg-indigo-500 p-3 text-white"
|
||||||
onClick={() => signOut({ callbackUrl: '/' })}
|
onClick={() => {
|
||||||
|
signOut({ redirect: false });
|
||||||
|
router.replace('/')
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
|
@ -2,15 +2,17 @@
|
|||||||
|
|
||||||
import { PlusIcon } from '@heroicons/react/24/outline';
|
import { PlusIcon } from '@heroicons/react/24/outline';
|
||||||
import clsx from 'clsx';
|
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';
|
import { useCart } from './cart-context';
|
||||||
|
|
||||||
function SubmitButton({}: {}) {
|
function SubmitButton({disabled = false}: {disabled: boolean}) {
|
||||||
const buttonClasses =
|
const buttonClasses =
|
||||||
'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white';
|
'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button aria-label="Please select an option" className={clsx(buttonClasses)}>
|
<button aria-label="Please select an option" disabled={disabled} className={clsx(buttonClasses)}>
|
||||||
<div className="absolute left-0 ml-4">
|
<div className="absolute left-0 ml-4">
|
||||||
<PlusIcon className="h-5" />
|
<PlusIcon className="h-5" />
|
||||||
</div>
|
</div>
|
||||||
@ -19,8 +21,23 @@ function SubmitButton({}: {}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AddToCart({ product }: { product: Product }) {
|
export function AddToCart({ product, variations }: { product: Product, variations?: ProductVariations[] }) {
|
||||||
const { setNewCart } = useCart();
|
const { setNewCart } = useCart();
|
||||||
|
const {state} = useProduct();
|
||||||
|
|
||||||
|
|
||||||
|
const productVariant = useMemo(() => {
|
||||||
|
const keys = Object.keys(state).filter((key) => key !== 'id' && key !== 'image').map((key) => ({
|
||||||
|
attribute: key.toLowerCase(),
|
||||||
|
value: state[key]
|
||||||
|
}));
|
||||||
|
const productExist = variations?.find((variation) => {
|
||||||
|
const attributes = variation.attributes.map((attr) => ({name: attr.name, option: attr.option})) || [];
|
||||||
|
return attributes.every((attribute) => attribute.option === keys.find((key) => key.attribute === attribute.name)?.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return productExist ? keys : [];
|
||||||
|
}, [state, variations]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
@ -29,7 +46,7 @@ export function AddToCart({ product }: { product: Product }) {
|
|||||||
const cart = await (
|
const cart = await (
|
||||||
await fetch('/api/cart', {
|
await fetch('/api/cart', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ id: product.id, quantity: 1, variation: [] })
|
body: JSON.stringify({ id: product.id, quantity: 1, variation: productVariant })
|
||||||
})
|
})
|
||||||
).json();
|
).json();
|
||||||
setNewCart(cart);
|
setNewCart(cart);
|
||||||
@ -38,7 +55,7 @@ export function AddToCart({ product }: { product: Product }) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SubmitButton />
|
<SubmitButton disabled={variations?.length && !productVariant.length ? true : false}/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,6 @@
|
|||||||
import { Cart } from 'lib/woocomerce/models/cart';
|
import { Cart } from 'lib/woocomerce/models/cart';
|
||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
type UpdateType = 'plus' | 'minus' | 'delete';
|
|
||||||
|
|
||||||
type UpdatePayload = { key: string | number; quantity: number };
|
|
||||||
type AddPayload = {
|
|
||||||
id: string | number;
|
|
||||||
quantity: number;
|
|
||||||
variation: { attribute: string; value: string }[];
|
|
||||||
};
|
|
||||||
type RemovePayload = { key: string | number };
|
|
||||||
|
|
||||||
type CartContextType = {
|
type CartContextType = {
|
||||||
cart: Cart | undefined;
|
cart: Cart | undefined;
|
||||||
setNewCart: (cart: Cart) => void;
|
setNewCart: (cart: Cart) => void;
|
||||||
@ -20,15 +10,24 @@ type CartContextType = {
|
|||||||
|
|
||||||
const CartContext = createContext<CartContextType | undefined>(undefined);
|
const CartContext = createContext<CartContextType | undefined>(undefined);
|
||||||
|
|
||||||
export function CartProvider({ value, children }: { value: Cart; children: React.ReactNode }) {
|
export function CartProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [cart, setCart] = useState<Cart | undefined>(value);
|
const [cart, setCart] = useState<Cart | undefined>(undefined);
|
||||||
const setNewCart = (cart: Cart) => {
|
const setNewCart = (cart: Cart) => {
|
||||||
setCart(cart);
|
setCart(cart);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchCart = async () => {
|
||||||
|
try {
|
||||||
|
const cart = await (await fetch('/api/cart')).json();
|
||||||
|
setNewCart(cart);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCart(value);
|
fetchCart();
|
||||||
}, [value]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CartContext.Provider
|
<CartContext.Provider
|
||||||
|
@ -113,6 +113,11 @@ export default function CartModal() {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-1 flex-col text-base">
|
<div className="flex flex-1 flex-col text-base">
|
||||||
<span className="leading-tight">{item.name}</span>
|
<span className="leading-tight">{item.name}</span>
|
||||||
|
{item.variation.map((variation, i) => (
|
||||||
|
<span key={i} className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
{variation.attribute}: {variation.value}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,7 +48,7 @@ export async function ThreeItemGrid() {
|
|||||||
const [firstProduct, secondProduct, thirdProduct] = products;
|
const [firstProduct, secondProduct, thirdProduct] = products;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2 lg:max-h-[calc(100vh-200px)]">
|
<section className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2">
|
||||||
{products.map((product, index) => (
|
{products.map((product, index) => (
|
||||||
<ThreeItemGridItem key={product.id} size={index === 0 ? 'full' : 'half'} item={product} />
|
<ThreeItemGridItem key={product.id} size={index === 0 ? 'full' : 'half'} item={product} />
|
||||||
))}
|
))}
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
import { UserCircleIcon } from '@heroicons/react/24/outline';
|
import { UserCircleIcon } from '@heroicons/react/24/outline';
|
||||||
import { authOptions } from 'app/api/auth/[...nextauth]/route';
|
|
||||||
import { getServerSession } from 'next-auth';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export default async function UserIcon() {
|
export default async function UserIcon() {
|
||||||
const isAuthenticated = (await getServerSession(authOptions))?.user?.token;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={!isAuthenticated ? '/login' : '/profile'} className="ms-2" aria-label="login">
|
<Link href={'/login'} className="ms-2" aria-label="login">
|
||||||
<div className="relative flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white">
|
<div className="relative flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white">
|
||||||
<UserCircleIcon className="h-4 transition-all ease-in-out hover:scale-110" />
|
<UserCircleIcon className="h-4 transition-all ease-in-out hover:scale-110" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,5 +7,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const NextAuthProvider = ({ children }: Props) => {
|
export const NextAuthProvider = ({ children }: Props) => {
|
||||||
|
|
||||||
return <SessionProvider>{children}</SessionProvider>;
|
return <SessionProvider>{children}</SessionProvider>;
|
||||||
};
|
};
|
||||||
|
@ -52,7 +52,7 @@ export function ProductProvider({ children }: { children: React.ReactNode }) {
|
|||||||
() => ({
|
() => ({
|
||||||
state,
|
state,
|
||||||
updateOption,
|
updateOption,
|
||||||
updateImage
|
updateImage,
|
||||||
}),
|
}),
|
||||||
[state]
|
[state]
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { AddToCart } from 'components/cart/add-to-cart';
|
|
||||||
import Price from 'components/price';
|
import Price from 'components/price';
|
||||||
import Prose from 'components/prose';
|
import Prose from 'components/prose';
|
||||||
import { Product } from 'lib/woocomerce/models/product';
|
import { Product } from 'lib/woocomerce/models/product';
|
||||||
@ -18,7 +17,6 @@ export function ProductDescription({ product }: { product: Product }) {
|
|||||||
html={product.description}
|
html={product.description}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<AddToCart product={product} />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,64 +2,40 @@
|
|||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useProduct, useUpdateURL } from 'components/product/product-context';
|
import { useProduct, useUpdateURL } from 'components/product/product-context';
|
||||||
import { ProductOption, ProductVariant } from 'lib/shopify/types';
|
import { Attribute } from 'lib/woocomerce/models/base';
|
||||||
|
import { ProductVariations } from 'lib/woocomerce/models/product';
|
||||||
|
|
||||||
type Combination = {
|
|
||||||
id: string;
|
type FilterVariation = {
|
||||||
availableForSale: boolean;
|
name: string | undefined;
|
||||||
[key: string]: string | boolean;
|
values: string[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function VariantSelector({
|
export function VariantSelector({
|
||||||
options,
|
options,
|
||||||
variants
|
variations,
|
||||||
}: {
|
}: {
|
||||||
options: ProductOption[];
|
options: Partial<Attribute>[];
|
||||||
variants: ProductVariant[];
|
variations: ProductVariations[];
|
||||||
}) {
|
}) {
|
||||||
const { state, updateOption } = useProduct();
|
const { state, updateOption } = useProduct();
|
||||||
const updateURL = useUpdateURL();
|
const updateURL = useUpdateURL();
|
||||||
const hasNoOptionsOrJustOneOption =
|
|
||||||
!options.length || (options.length === 1 && options[0]?.values.length === 1);
|
|
||||||
|
|
||||||
if (hasNoOptionsOrJustOneOption) {
|
const combinations: FilterVariation[] = options?.map(attribute => ({
|
||||||
return null;
|
name: attribute.name,
|
||||||
}
|
values: attribute?.options?.map(option => option),
|
||||||
|
|
||||||
const combinations: Combination[] = variants.map((variant) => ({
|
|
||||||
id: variant.id,
|
|
||||||
availableForSale: variant.availableForSale,
|
|
||||||
...variant.selectedOptions.reduce(
|
|
||||||
(accumulator, option) => ({ ...accumulator, [option.name.toLowerCase()]: option.value }),
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return options.map((option) => (
|
return combinations.map((option) => (
|
||||||
<form key={option.id}>
|
<form key={option.name}>
|
||||||
<dl className="mb-8">
|
<dl className="mb-8">
|
||||||
<dt className="mb-4 text-sm uppercase tracking-wide">{option.name}</dt>
|
<dt className="mb-4 text-sm uppercase tracking-wide">{option.name}</dt>
|
||||||
<dd className="flex flex-wrap gap-3">
|
<dd className="flex flex-wrap gap-3">
|
||||||
{option.values.map((value) => {
|
{option?.values?.map((value) => {
|
||||||
const optionNameLowerCase = option.name.toLowerCase();
|
const optionNameLowerCase = option?.name?.toLowerCase();
|
||||||
|
|
||||||
// Base option params on current selectedOptions so we can preserve any other param state.
|
|
||||||
const optionParams = { ...state, [optionNameLowerCase]: value };
|
|
||||||
|
|
||||||
// Filter out invalid options and check if the option combination is available for sale.
|
|
||||||
const filtered = Object.entries(optionParams).filter(([key, value]) =>
|
|
||||||
options.find(
|
|
||||||
(option) => option.name.toLowerCase() === key && option.values.includes(value)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const isAvailableForSale = combinations.find((combination) =>
|
|
||||||
filtered.every(
|
|
||||||
([key, value]) => combination[key] === value && combination.availableForSale
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// The option is active if it's in the selected options.
|
// The option is active if it's in the selected options.
|
||||||
const isActive = state[optionNameLowerCase] === value;
|
const isActive = optionNameLowerCase ? state[optionNameLowerCase] === value : false;
|
||||||
|
if (!optionNameLowerCase) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -68,17 +44,15 @@ export function VariantSelector({
|
|||||||
updateURL(newState);
|
updateURL(newState);
|
||||||
}}
|
}}
|
||||||
key={value}
|
key={value}
|
||||||
aria-disabled={!isAvailableForSale}
|
title={`${option.name} ${value}`}
|
||||||
disabled={!isAvailableForSale}
|
|
||||||
title={`${option.name} ${value}${!isAvailableForSale ? ' (Out of Stock)' : ''}`}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex min-w-[48px] items-center justify-center rounded-full border bg-neutral-100 px-2 py-1 text-sm dark:border-neutral-800 dark:bg-neutral-900',
|
'flex min-w-[48px] items-center justify-center rounded-full border bg-neutral-100 px-2 py-1 text-sm dark:border-neutral-800 dark:bg-neutral-900',
|
||||||
{
|
{
|
||||||
'cursor-default ring-2 ring-blue-600': isActive,
|
'cursor-default ring-2 ring-blue-600': isActive,
|
||||||
'ring-1 ring-transparent transition duration-300 ease-in-out hover:ring-blue-600':
|
'ring-1 ring-transparent transition duration-300 ease-in-out hover:ring-blue-600':
|
||||||
!isActive && isAvailableForSale,
|
!isActive,
|
||||||
'relative z-10 cursor-not-allowed overflow-hidden bg-neutral-100 text-neutral-500 ring-1 ring-neutral-300 before:absolute before:inset-x-0 before:-z-10 before:h-px before:-rotate-45 before:bg-neutral-300 before:transition-transform dark:bg-neutral-900 dark:text-neutral-400 dark:ring-neutral-700 before:dark:bg-neutral-700':
|
'relative z-10 cursor-not-allowed overflow-hidden bg-neutral-100 text-neutral-500 ring-1 ring-neutral-300 before:absolute before:inset-x-0 before:-z-10 before:h-px before:-rotate-45 before:bg-neutral-300 before:transition-transform dark:bg-neutral-900 dark:text-neutral-400 dark:ring-neutral-700 before:dark:bg-neutral-700':
|
||||||
!isAvailableForSale
|
''
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
export type SortFilterItem = {
|
export type SortFilterItem = {
|
||||||
title: string;
|
title: string;
|
||||||
slug: string | null;
|
slug: string | null;
|
||||||
sortKey: 'RELEVANCE' | 'BEST_SELLING' | 'CREATED_AT' | 'PRICE';
|
sortKey: 'rating' | 'popularity' | 'date' | 'price';
|
||||||
reverse: boolean;
|
order?: 'asc' | 'desc';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultSort: SortFilterItem = {
|
export const defaultSort: SortFilterItem = {
|
||||||
title: 'Relevance',
|
title: 'Relevance',
|
||||||
slug: null,
|
slug: null,
|
||||||
sortKey: 'RELEVANCE',
|
sortKey: 'popularity',
|
||||||
reverse: false
|
order: 'desc'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sorting: SortFilterItem[] = [
|
export const sorting: SortFilterItem[] = [
|
||||||
defaultSort,
|
defaultSort,
|
||||||
{ title: 'Trending', slug: 'trending-desc', sortKey: 'BEST_SELLING', reverse: false }, // asc
|
{ title: 'Trending', slug: 'trending-desc', sortKey: 'rating', order: 'desc' }, // asc
|
||||||
{ title: 'Latest arrivals', slug: 'latest-desc', sortKey: 'CREATED_AT', reverse: true },
|
{ title: 'Latest arrivals', slug: 'latest-desc', sortKey: 'date', order: 'desc' },
|
||||||
{ title: 'Price: Low to high', slug: 'price-asc', sortKey: 'PRICE', reverse: false }, // asc
|
{ title: 'Price: Low to high', slug: 'price-asc', sortKey: 'price', order: 'asc' }, // asc
|
||||||
{ title: 'Price: High to low', slug: 'price-desc', sortKey: 'PRICE', reverse: true }
|
{ title: 'Price: High to low', slug: 'price-desc', sortKey: 'price', order: 'desc' }
|
||||||
];
|
];
|
||||||
|
|
||||||
export const TAGS = {
|
export const TAGS = {
|
||||||
|
@ -45,5 +45,6 @@ export type Attribute = {
|
|||||||
export type Default_Attribute = {
|
export type Default_Attribute = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
slug: string;
|
||||||
option: string;
|
option: string;
|
||||||
};
|
};
|
||||||
|
@ -158,7 +158,6 @@ export default class WooCommerceRestApi<T extends WooRestApiOptions> {
|
|||||||
}
|
}
|
||||||
const query = new Url(url, true).query; // Parse the query string returned by the url
|
const query = new Url(url, true).query; // Parse the query string returned by the url
|
||||||
|
|
||||||
// console.log("params:", params);
|
|
||||||
const values = [];
|
const values = [];
|
||||||
|
|
||||||
let queryString = '';
|
let queryString = '';
|
||||||
@ -225,11 +224,13 @@ export default class WooCommerceRestApi<T extends WooRestApiOptions> {
|
|||||||
delete params.id;
|
delete params.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add query params to url
|
const queryParams: string[] = [];
|
||||||
if (Object.keys(params).length !== 0) {
|
for (const key in params) {
|
||||||
for (const key in params) {
|
queryParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key] as string | number | boolean)}`);
|
||||||
url = url + '?' + key + '=' + params[key];
|
}
|
||||||
}
|
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
url += '?' + queryParams.join('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -247,6 +248,7 @@ export default class WooCommerceRestApi<T extends WooRestApiOptions> {
|
|||||||
// url = this._normalizeQueryString(url, params);
|
// url = this._normalizeQueryString(url, params);
|
||||||
// return url;
|
// return url;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ export interface ProductVariations {
|
|||||||
shipping_class: string;
|
shipping_class: string;
|
||||||
shipping_class_id: number;
|
shipping_class_id: number;
|
||||||
image: Partial<Image>;
|
image: Partial<Image>;
|
||||||
attributes: Partial<Attribute>[];
|
attributes: Partial<Default_Attribute>[];
|
||||||
menu_order: number;
|
menu_order: number;
|
||||||
meta_data: Partial<Meta_Data>[];
|
meta_data: Partial<Meta_Data>[];
|
||||||
}
|
}
|
||||||
@ -143,6 +143,7 @@ export interface ProductAttributes {
|
|||||||
type: string;
|
type: string;
|
||||||
order_by: string;
|
order_by: string;
|
||||||
has_archives: boolean;
|
has_archives: boolean;
|
||||||
|
options: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductAttributesTerms {
|
export interface ProductAttributesTerms {
|
||||||
|
@ -6,7 +6,7 @@ import { Cart } from './models/cart';
|
|||||||
* To use this in the client-side, you need to create a new route of api endpoint in your Next.js app.
|
* To use this in the client-side, you need to create a new route of api endpoint in your Next.js app.
|
||||||
*/
|
*/
|
||||||
class WooCommerceStoreApiClient {
|
class WooCommerceStoreApiClient {
|
||||||
public client: AxiosInstance;
|
private client: AxiosInstance;
|
||||||
|
|
||||||
constructor(baseURL: string) {
|
constructor(baseURL: string) {
|
||||||
const headers: RawAxiosRequestHeaders = {
|
const headers: RawAxiosRequestHeaders = {
|
||||||
@ -18,23 +18,31 @@ class WooCommerceStoreApiClient {
|
|||||||
baseURL,
|
baseURL,
|
||||||
headers
|
headers
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.interceptors.response.use((response) => {
|
|
||||||
console.log('cart-token', response.headers['cart-token']);
|
|
||||||
this.client.defaults.headers['cart-token'] = response.headers['cart-token'];
|
|
||||||
|
|
||||||
return response;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_setAuthorizationToken(token: string) {
|
_setAuthorizationToken(token: string) {
|
||||||
if (token) {
|
if (token) {
|
||||||
this.client.defaults.headers['Authorization'] = `Bearer ${token}`;
|
this.client.defaults.headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
} else {
|
||||||
|
this._deleteAuthorizationToken();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_deleteAuthorizationToken() {
|
||||||
|
this.client.defaults.headers['Authorization'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
_seCartToken(cartToken: string) {
|
||||||
|
this.client.defaults.headers['cart-token'] = cartToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async getCart(params?: Record<string, string | number>): Promise<Cart> {
|
async getCart(params?: Record<string, string | number>): Promise<Cart> {
|
||||||
return this.client.get<Cart>('/cart', { params }).then((response) => response.data);
|
return this.client.get<Cart>('/cart', { params }).then(async (response) => {
|
||||||
|
this._seCartToken(response.headers['cart-token']);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addToCart(payload: {
|
async addToCart(payload: {
|
||||||
@ -57,6 +65,6 @@ class WooCommerceStoreApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Example usage.
|
// Example usage.
|
||||||
const baseURL = 'http://wordpress.localhost/wp-json/wc/store/v1'; // Replace with your WooCommerce API URL.
|
const baseURL = 'http://wordpress.localhost/wp-json/wc/store/v1';
|
||||||
|
|
||||||
export const storeApi = new WooCommerceStoreApiClient(baseURL);
|
export const storeApi = new WooCommerceStoreApiClient(baseURL);
|
||||||
|
@ -3,9 +3,9 @@ import WooCommerceRestApi, { WooRestApiOptions } from './models/client';
|
|||||||
const option: WooRestApiOptions = {
|
const option: WooRestApiOptions = {
|
||||||
url: process.env.WOOCOMMERCE_URL ?? 'http://wordpress.localhost',
|
url: process.env.WOOCOMMERCE_URL ?? 'http://wordpress.localhost',
|
||||||
consumerKey:
|
consumerKey:
|
||||||
process.env.WOOCOMMERCE_CONSUMER_KEY ?? 'ck_1fb0a3c9b50ae813c31c7effc086a809d8416d90',
|
process.env.WOOCOMMERCE_CONSUMER_KEY ?? 'ck_2307cad3b7ab10eb2c439fd8c50ef69740967768',
|
||||||
consumerSecret:
|
consumerSecret:
|
||||||
process.env.WOOCOMMERCE_CONSUMER_SECRET ?? 'cs_ee4f1c9e061d07a7cb6025b69d414189a9157e20',
|
process.env.WOOCOMMERCE_CONSUMER_SECRET ?? 'cs_2e2e94e6b9507cca5f7080ff8f856ac84c7b72d5',
|
||||||
isHttps: false,
|
isHttps: false,
|
||||||
version: 'wc/v3',
|
version: 'wc/v3',
|
||||||
queryStringAuth: false // Force Basic Authentication as query string true and using under
|
queryStringAuth: false // Force Basic Authentication as query string true and using under
|
||||||
|
27
middleware.ts
Normal file
27
middleware.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { getToken } from 'next-auth/jwt';
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
// Lista delle pagine protette
|
||||||
|
const protectedRoutes = ['/profile'];
|
||||||
|
|
||||||
|
export async function middleware(req: NextRequest) {
|
||||||
|
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
|
||||||
|
console.log('token', token);
|
||||||
|
const isProtectedRoute = protectedRoutes.some((route) => req.nextUrl.pathname.startsWith(route));
|
||||||
|
if (isProtectedRoute && !token) {
|
||||||
|
const loginUrl = new URL('/login', req.url);
|
||||||
|
return NextResponse.redirect(loginUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.nextUrl.pathname.startsWith('/login') && token) {
|
||||||
|
const profileUrl = new URL('/profile', req.url);
|
||||||
|
return NextResponse.redirect(profileUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: ['/login', '/profile', '/profile/:path*']
|
||||||
|
};
|
@ -17,7 +17,8 @@
|
|||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"sonner": "^1.7.0"
|
"sonner": "^1.7.0",
|
||||||
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
|
1396
pnpm-lock.yaml
generated
1396
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user