fix: build

This commit is contained in:
paolosantarsiero 2024-12-31 11:37:43 +01:00
parent 1b2211ddea
commit aac4af90ae
25 changed files with 1891 additions and 627 deletions

View File

@ -0,0 +1,5 @@
import { notFound } from 'next/navigation';
export default function NotFoundCatchAll() {
notFound();
}

View File

@ -1,5 +1,5 @@
import { authOptions } from "lib/auth/config"; import { authOptions } from 'lib/auth/config';
import NextAuth from "next-auth"; import NextAuth from 'next-auth';
const handler = NextAuth(authOptions); const handler = NextAuth(authOptions);
export { handler as GET, handler as POST }; export { handler as GET, handler as POST };

View File

@ -20,7 +20,10 @@ 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', message: JSON.stringify(error) }, { status: 500 }); return NextResponse.json(
{ error: 'Failed to add item to cart', message: JSON.stringify(error) },
{ status: 500 }
);
} }
} }
@ -35,7 +38,10 @@ 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', message: JSON.stringify(error) }, { status: 500 }); return NextResponse.json(
{ error: 'Failed to update cart item', message: JSON.stringify(error) },
{ status: 500 }
);
} }
} }
@ -45,6 +51,9 @@ 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', message: JSON.stringify(error) }, { status: 500 }); return NextResponse.json(
{ error: 'Failed to remove item from cart', message: JSON.stringify(error) },
{ status: 500 }
);
} }
} }

View File

@ -1,5 +1,5 @@
import { woocommerce } from "lib/woocomerce/woocommerce"; import { woocommerce } from 'lib/woocomerce/woocommerce';
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
@ -9,4 +9,4 @@ export async function POST(req: NextRequest) {
} 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' }, { status: 500 });
} }
} }

View File

@ -74,7 +74,7 @@ export default function CheckoutPage() {
</ul> </ul>
<h2 className="mt-2 text-2xl font-bold">Shipping info</h2> <h2 className="mt-2 text-2xl font-bold">Shipping info</h2>
<form className="md:grid-cols-6 md:grid-rows-2 gap-4"> <form className="gap-4 md:grid-cols-6 md:grid-rows-2">
<div className="mt-4"> <div className="mt-4">
<label <label
htmlFor="address_1" htmlFor="address_1"

View File

@ -12,7 +12,7 @@ export default function LoginPage() {
const handleLogin = async (event: React.FormEvent) => { const handleLogin = async (event: React.FormEvent) => {
event.preventDefault(); event.preventDefault();
const res = await signIn('credentials', { username, password, redirect: false, }); const res = await signIn('credentials', { username, password, redirect: false });
if (res?.ok) { if (res?.ok) {
router.replace('/'); router.replace('/');
} else { } else {
@ -23,7 +23,7 @@ export default function LoginPage() {
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 flex-col h-screen w-full max-w-md"> <div className="flex h-screen w-full max-w-md flex-col">
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500">{error}</p>}
<form onSubmit={handleLogin}> <form onSubmit={handleLogin}>
<div className="mt-4"> <div className="mt-4">
@ -67,7 +67,7 @@ export default function LoginPage() {
</button> </button>
</div> </div>
<span className="block mt-6 text-center text-sm text-gray-600 dark:text-gray-300"> <span className="mt-6 block text-center text-sm text-gray-600 dark:text-gray-300">
Don't have an account?{' '} Don't have an account?{' '}
<a href="/signup" className="text-indigo-600 hover:underline"> <a href="/signup" className="text-indigo-600 hover:underline">
Sign up Sign up

View File

@ -101,9 +101,9 @@ export default async function ProductPage(props: { params: Promise<{ name: strin
</Suspense> </Suspense>
)} )}
<Suspense fallback={null}> <Suspense fallback={null}>
<ProductDescription product={product} variations={variations}/> <ProductDescription product={product} variations={variations} />
</Suspense> </Suspense>
<AddToCart product={product} variations={variations}/> <AddToCart product={product} variations={variations} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -16,8 +16,7 @@ export default function SearchLayout({ children }: { children: React.ReactNode }
<ChildrenWrapper>{children}</ChildrenWrapper> <ChildrenWrapper>{children}</ChildrenWrapper>
</Suspense> </Suspense>
</div> </div>
<div className="order-none flex-none md:order-last md:w-[100px]"> <div className="order-none flex-none md:order-last md:w-[100px]"></div>
</div>
</div> </div>
<Footer /> <Footer />
</> </>

View File

@ -15,7 +15,11 @@ export default async function SearchPage(props: {
const { sort, q: searchValue } = searchParams as { [key: string]: string }; const { sort, q: searchValue } = searchParams as { [key: string]: string };
const { sortKey, order } = 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, order }); 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 (

View File

@ -4,21 +4,23 @@ import { useState } from 'react';
import { z } from 'zod'; import { z } from 'zod';
type FormData = { type FormData = {
username: string; username: string;
email: string; email: string;
password: string; password: string;
confirmPassword: string; confirmPassword: string;
} };
const customerSchema = z.object({ const customerSchema = z
.object({
username: z.string().min(3), username: z.string().min(3),
email: z.string().email({ message: "Invalid email" }), email: z.string().email({ message: 'Invalid email' }),
password: z.string(), password: z.string(),
confirmPassword: z.string(), confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, { })
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match", message: "Passwords don't match",
path: ["confirmPassword"], path: ['confirmPassword']
});; });
export default function SignUpPage() { export default function SignUpPage() {
const initialState = { username: '', email: '', password: '', confirmPassword: '' }; const initialState = { username: '', email: '', password: '', confirmPassword: '' };
@ -27,7 +29,7 @@ export default function SignUpPage() {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value })); setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));
} };
const handleSignup = async (event: React.FormEvent) => { const handleSignup = async (event: React.FormEvent) => {
event.preventDefault(); event.preventDefault();
@ -37,22 +39,22 @@ export default function SignUpPage() {
await fetch('/api/customer', { await fetch('/api/customer', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
username: formData.username, username: formData.username,
first_name: '', first_name: '',
last_name: '', last_name: '',
email: formData.email, email: formData.email,
password: formData.password password: formData.password
}), })
}); });
} catch (error) { } catch (error) {
if (error instanceof z.ZodError) { if (error instanceof z.ZodError) {
const errorObj: FormData = initialState; const errorObj: FormData = initialState;
error.errors.forEach((err) => { error.errors.forEach((err) => {
const key = err.path[0] as keyof FormData; const key = err.path[0] as keyof FormData;
errorObj[key] = err.message as string; errorObj[key] = err.message as string;
}); });
console.log(errorObj); console.log(errorObj);
setError(errorObj); setError(errorObj);

View File

@ -10,7 +10,7 @@ export default function LogoutButton() {
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={() => { onClick={() => {
signOut({ redirect: false }); signOut({ redirect: false });
router.replace('/') router.replace('/');
}} }}
> >
Logout Logout

View File

@ -6,12 +6,16 @@ import { useProduct } from 'components/product/product-context';
import { Product, ProductVariations } from 'lib/woocomerce/models/product'; import { Product, ProductVariations } from 'lib/woocomerce/models/product';
import { useCart } from './cart-context'; import { useCart } from './cart-context';
function SubmitButton({disabled = false}: {disabled: boolean}) { 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" disabled={disabled} className={clsx(buttonClasses, disabled ? 'opacity-50 cursor-not-allowed' : '')}> <button
aria-label="Please select an option"
disabled={disabled}
className={clsx(buttonClasses, disabled ? 'cursor-not-allowed opacity-50' : '')}
>
<div className="absolute left-0 ml-4"> <div className="absolute left-0 ml-4">
<PlusIcon className="h-5" /> <PlusIcon className="h-5" />
</div> </div>
@ -20,12 +24,21 @@ function SubmitButton({disabled = false}: {disabled: boolean}) {
); );
} }
export function AddToCart({ product, variations }: { product: Product, variations?: ProductVariations[] }) { export function AddToCart({
product,
variations
}: {
product: Product;
variations?: ProductVariations[];
}) {
const { setNewCart } = useCart(); const { setNewCart } = useCart();
const { state } = useProduct(); const { state } = useProduct();
const productVariant = variations?.find((variation) => variation.id.toString() === state.variation); const productVariant = variations?.find(
const variation = productVariant?.attributes.map((attr) => ({ attribute: attr.name, value: attr.option })) || []; (variation) => variation.id.toString() === state.variation
);
const variation =
productVariant?.attributes.map((attr) => ({ attribute: attr.name, value: attr.option })) || [];
return ( return (
<form <form
action={async () => { action={async () => {
@ -42,7 +55,7 @@ export function AddToCart({ product, variations }: { product: Product, variation
} }
}} }}
> >
<SubmitButton disabled={variations?.length && !state.variation ? true : false}/> <SubmitButton disabled={variations?.length && !state.variation ? true : false} />
</form> </form>
); );
} }

View File

@ -23,7 +23,7 @@ export function CartProvider({ children }: { children: React.ReactNode }) {
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
} };
useEffect(() => { useEffect(() => {
fetchCart(); fetchCart();

View File

@ -10,7 +10,6 @@ type Menu = {
path: string; path: string;
}; };
export function FooterMenuItem({ item }: { item: Menu }) { export function FooterMenuItem({ item }: { item: Menu }) {
const pathname = usePathname(); const pathname = usePathname();
const [active, setActive] = useState(pathname === item.path); const [active, setActive] = useState(pathname === item.path);

View File

@ -24,10 +24,10 @@ export default async function Footer() {
title: 'Shop', title: 'Shop',
path: '/shop' path: '/shop'
}, },
...categories.map((category) => ({ ...categories.map((category) => ({
title: category.name, title: category.name,
path: path.join('/collection', category.id.toString()) path: path.join('/collection', category.id.toString())
})) }))
] as Menu[]; ] as Menu[];
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : ''); const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : '');

View File

@ -14,15 +14,15 @@ function FilterItemList({ list }: { list: ListItem[] }) {
{list.map((item: ListItem, i) => ( {list.map((item: ListItem, i) => (
<FilterItem key={i} item={item} /> <FilterItem key={i} item={item} />
))} ))}
<Slider <Slider
className="max-w-md" className="max-w-md"
defaultValue={[100, 500]} defaultValue={[100, 500]}
formatOptions={{style: "currency", currency: "USD"}} formatOptions={{ style: 'currency', currency: 'USD' }}
label="Price Range" label="Price Range"
maxValue={1000} maxValue={1000}
minValue={0} minValue={0}
step={50} step={50}
/> />
</> </>
); );
} }

View File

@ -7,6 +7,5 @@ type Props = {
}; };
export const NextAuthProvider = ({ children }: Props) => { export const NextAuthProvider = ({ children }: Props) => {
return <SessionProvider>{children}</SessionProvider>; return <SessionProvider>{children}</SessionProvider>;
}; };

View File

@ -9,7 +9,7 @@ type ProductState = {
image?: string; image?: string;
} & { } & {
variation?: string; variation?: string;
} };
type ProductContextType = { type ProductContextType = {
state: ProductState; state: ProductState;
@ -55,7 +55,7 @@ export function ProductProvider({ children }: { children: React.ReactNode }) {
const newState = { variation }; const newState = { variation };
setOptimisticState(newState); setOptimisticState(newState);
return { ...state, ...newState }; return { ...state, ...newState };
} };
const value = useMemo( const value = useMemo(
() => ({ () => ({

View File

@ -4,15 +4,26 @@ import Prose from 'components/prose';
import { Product, ProductVariations } from 'lib/woocomerce/models/product'; import { Product, ProductVariations } from 'lib/woocomerce/models/product';
import { useProduct } from './product-context'; import { useProduct } from './product-context';
export function ProductDescription({ product, variations }: { product: Product, variations?: ProductVariations[] }) { export function ProductDescription({
product,
variations
}: {
product: Product;
variations?: ProductVariations[];
}) {
const { state } = useProduct(); const { state } = useProduct();
const productVariant = variations?.find((variation) => variation.id.toString() === state.variation); const productVariant = variations?.find(
(variation) => variation.id.toString() === state.variation
);
return ( return (
<> <>
<div className="mb-6 flex flex-col border-b pb-6 dark:border-neutral-700"> <div className="mb-6 flex flex-col border-b pb-6 dark:border-neutral-700">
<div className="mr-auto w-auto rounded-full bg-blue-600 p-2 text-sm text-white"> <div className="mr-auto w-auto rounded-full bg-blue-600 p-2 text-sm text-white">
<Price amount={productVariant ? productVariant.price : product.price} currencyCode="EUR" /> <Price
amount={productVariant ? productVariant.price : product.price}
currencyCode="EUR"
/>
</div> </div>
</div> </div>
{product.description ? ( {product.description ? (

View File

@ -5,7 +5,6 @@ import { useProduct, useUpdateURL } from 'components/product/product-context';
import { Attribute } from 'lib/woocomerce/models/base'; import { Attribute } from 'lib/woocomerce/models/base';
import { ProductVariations } from 'lib/woocomerce/models/product'; import { ProductVariations } from 'lib/woocomerce/models/product';
type FilterVariation = { type FilterVariation = {
name: string | undefined; name: string | undefined;
values: string[] | undefined; values: string[] | undefined;
@ -13,7 +12,7 @@ type FilterVariation = {
export function VariantSelector({ export function VariantSelector({
options, options,
variations, variations
}: { }: {
options: Partial<Attribute>[]; options: Partial<Attribute>[];
variations: ProductVariations[]; variations: ProductVariations[];
@ -21,9 +20,9 @@ export function VariantSelector({
const { state, updateOption } = useProduct(); const { state, updateOption } = useProduct();
const updateURL = useUpdateURL(); const updateURL = useUpdateURL();
const combinations: FilterVariation[] = options?.map(attribute => ({ const combinations: FilterVariation[] = options?.map((attribute) => ({
name: attribute.name, name: attribute.name,
values: attribute?.options?.map(option => option), values: attribute?.options?.map((option) => option)
})); }));
return combinations.map((option) => ( return combinations.map((option) => (
@ -41,12 +40,21 @@ export function VariantSelector({
formAction={() => { formAction={() => {
if (!optionNameLowerCase) return; if (!optionNameLowerCase) return;
let newState = updateOption(optionNameLowerCase, value); let newState = updateOption(optionNameLowerCase, value);
const keys = Object.keys(newState).filter((key) => key !== 'id' && key !== 'image' && key !== 'variation'); const keys = Object.keys(newState).filter(
const variant = variations.find((variation) => { (key) => key !== 'id' && key !== 'image' && key !== 'variation'
return variation?.attributes?.every((attr) => attr.name && keys.includes(attr.name) && newState[attr.name] === attr.option); );
})?.id?.toString(); const variant = variations
.find((variation) => {
return variation?.attributes?.every(
(attr) =>
attr.name &&
keys.includes(attr.name) &&
newState[attr.name] === attr.option
);
})
?.id?.toString();
if (variant) { if (variant) {
newState = {...newState, variation: variant}; newState = { ...newState, variation: variant };
} }
updateURL(newState); updateURL(newState);
}} }}
@ -59,7 +67,7 @@ export function VariantSelector({
'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, !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':
'' ''
} }
)} )}
> >

View File

@ -42,7 +42,7 @@ 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: { events: {
async signIn() { async signIn() {
@ -53,4 +53,4 @@ export const authOptions = {
storeApi._setAuthorizationToken(''); storeApi._setAuthorizationToken('');
} }
} }
} satisfies NextAuthOptions; } satisfies NextAuthOptions;

View File

@ -226,7 +226,9 @@ export default class WooCommerceRestApi<T extends WooRestApiOptions> {
const queryParams: string[] = []; const queryParams: string[] = [];
for (const key in params) { for (const key in params) {
queryParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key] as string | number | boolean)}`); queryParams.push(
`${encodeURIComponent(key)}=${encodeURIComponent(params[key] as string | number | boolean)}`
);
} }
if (queryParams.length > 0) { if (queryParams.length > 0) {

View File

@ -15,12 +15,12 @@ export type OrderPayload = {
payment_method: string; payment_method: string;
payment_data?: PaymentMethodData[]; payment_data?: PaymentMethodData[];
customer_note?: string; customer_note?: string;
} };
export type PaymentMethodData = { export type PaymentMethodData = {
key: string; key: string;
value: string; value: string;
} };
class WooCommerceStoreApiClient { class WooCommerceStoreApiClient {
private client: AxiosInstance; private client: AxiosInstance;
@ -53,7 +53,6 @@ class WooCommerceStoreApiClient {
this.client.defaults.headers['cart-token'] = cartToken; 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(async (response) => { return this.client.get<Cart>('/cart', { params }).then(async (response) => {
this._seCartToken(response.headers['cart-token']); this._seCartToken(response.headers['cart-token']);
@ -94,6 +93,7 @@ class WooCommerceStoreApiClient {
} }
// Example usage. // Example usage.
const baseURL = process.env.WOOCOMMERCE_STORE_API_URL ?? 'http://wordpress.localhost/wp-json/wc/store/v1'; const baseURL =
process.env.WOOCOMMERCE_STORE_API_URL ?? 'http://wordpress.localhost/wp-json/wc/store/v1';
export const storeApi = new WooCommerceStoreApiClient(baseURL); export const storeApi = new WooCommerceStoreApiClient(baseURL);

View File

@ -2,10 +2,8 @@ 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 ?? '',
process.env.WOOCOMMERCE_CONSUMER_KEY ?? '', consumerSecret: process.env.WOOCOMMERCE_CONSUMER_SECRET ?? '',
consumerSecret:
process.env.WOOCOMMERCE_CONSUMER_SECRET ?? '',
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

2291
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff