refactor: all components

This commit is contained in:
paolosantarsiero 2025-01-15 12:57:24 +01:00
parent 23f9f55fab
commit a16571aca0
23 changed files with 498 additions and 322 deletions

View File

@ -10,7 +10,7 @@ export async function GET(req: NextRequest) {
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) {
return NextResponse.json({ error: 'Failed to fetch cart' }, { status: 500 }); return NextResponse.json({ error: 'Failed to fetch cart', message: error }, { status: 500 });
} }
} }

14
app/api/payments/route.ts Normal file
View File

@ -0,0 +1,14 @@
import { PaymentGateways } from 'lib/woocomerce/models/payment';
import { woocommerce } from 'lib/woocomerce/woocommerce';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
try {
const payments = await woocommerce
.get('payment_gateways')
.then((gateways) => gateways.filter((gateway: PaymentGateways) => gateway.enabled));
return NextResponse.json(payments, { status: 200 });
} catch (error) {
return NextResponse.json({ error: JSON.stringify(error) }, { status: 500 });
}
}

20
app/article/[id]/page.tsx Normal file
View File

@ -0,0 +1,20 @@
import Prose from 'components/prose';
import { wordpress } from 'lib/wordpress/wordpress';
import { notFound } from 'next/navigation';
export default async function ArticlePage(props: { params: Promise<{ id: string }> }) {
const { id } = await props.params;
const article = await wordpress.get(`posts/${id}`);
if (!article) return notFound();
return (
<section className="mt-4 grid max-w-screen-2xl gap-4 px-4 pb-4">
<div className="flex flex-col rounded-lg border border-neutral-200 bg-white p-8 dark:border-neutral-800 dark:bg-black md:p-12 lg:flex-row lg:gap-8">
<div className="flex flex-col">
<h1 className="font-bold">{article.title.rendered}</h1>
<Prose html={article.content.rendered} />
</div>
</div>
</section>
);
}

View File

@ -1,13 +1,14 @@
'use client'; 'use client';
import { Accordion, AccordionItem, Checkbox } from '@nextui-org/react'; import { Accordion, AccordionItem, Checkbox, Radio, RadioGroup } from '@nextui-org/react';
import { useCart } from 'components/cart/cart-context'; import { useCart } from 'components/cart/cart-context';
import CartItemView from 'components/cart/cart-item'; import CartItemView from 'components/cart/cart-item';
import Price from 'components/price'; import Price from 'components/price';
import ShippingForm from 'components/shipping/form'; import ShippingForm from 'components/shipping/form';
import { PaymentGateways } from 'lib/woocomerce/models/payment';
import { OrderPayload } from 'lib/woocomerce/storeApi'; import { OrderPayload } from 'lib/woocomerce/storeApi';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { z } from 'zod'; import { z } from 'zod';
const shippingSchema = z.object({ const shippingSchema = z.object({
@ -18,7 +19,8 @@ const shippingSchema = z.object({
city: z.string().min(3), city: z.string().min(3),
state: z.string().min(3), state: z.string().min(3),
postcode: z.string().min(3), postcode: z.string().min(3),
country: z.string().min(3) country: z.string().min(3),
company: z.string().optional()
}); });
export default function CheckoutPage() { export default function CheckoutPage() {
@ -55,21 +57,43 @@ export default function CheckoutPage() {
}; };
const [formData, setFormData] = useState(initialState); const [formData, setFormData] = useState(initialState);
const [sameBilling, setSameBilling] = useState(true); const [sameBilling, setSameBilling] = useState(true);
const [paymentGateways, setPaymentGateways] = useState<PaymentGateways[]>([]);
useEffect(() => {
const fetchPaymentGateways = async () => {
const paymentGateways = await (await fetch('/api/payments')).json();
setPaymentGateways(paymentGateways);
};
fetchPaymentGateways();
}, []);
const handleChangeShipping = (e: any) => { const handleChangeShipping = (e: any) => {
setFormData(e); setFormData({ ...formData, shipping_address: e });
if (sameBilling) {
setFormData({
...formData,
billing_address: { ...formData.billing_address, ...e }
});
}
}; };
const handleChangeBilling = (e: any) => { const handleChangeBilling = (e: any) => {
setFormData(e); setFormData({ ...formData, billing_address: e });
}; };
return ( return (
<section className="mx-auto grid h-full gap-4 px-4 pb-4"> <section className="mx-auto grid h-full gap-4 px-4 pb-4">
<p>Checkout</p> <p>Checkout</p>
<form <form
action={() => { onSubmit={(e) => {
e.preventDefault();
try { try {
console.log(formData); if (sameBilling) {
setFormData({
...formData,
billing_address: { ...formData.billing_address, ...formData.shipping_address }
});
}
shippingSchema.parse(formData.shipping_address); shippingSchema.parse(formData.shipping_address);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@ -77,26 +101,10 @@ export default function CheckoutPage() {
}} }}
className="rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-black" className="rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-black"
> >
<div className="flew-row col-span-4 row-span-2 flex"> <div className="flex flex-col gap-4 md:flex-row">
<Accordion defaultExpandedKeys={['1']} className="text-white md:w-2/3"> <div className="order-1 md:order-2 md:w-1/3">
<AccordionItem key="1" title="Shipping Info" className="text-white">
<ShippingForm handleChangeAction={handleChangeShipping} />
<Checkbox onValueChange={(v) => setSameBilling(v)} className="mt-2">
Use same address for billing?
</Checkbox>
</AccordionItem>
<AccordionItem key="2" title="Billing Info" className="text-white">
<ShippingForm handleChangeAction={handleChangeBilling} />
</AccordionItem>
<AccordionItem key="3" title="Payment" className="text-white">
<div className="flex flex-col justify-between overflow-hidden p-1">
<h2 className="mt-2 text-2xl font-bold">Payment</h2>
</div>
</AccordionItem>
</Accordion>
{cart && ( {cart && (
<div className="ms-4 flex flex-col justify-between overflow-hidden md:w-1/3"> <div className="ms-4 flex flex-col justify-between overflow-hidden">
<ul className="flex-grow overflow-auto"> <ul className="flex-grow overflow-auto">
{cart.items?.length && {cart.items?.length &&
cart.items cart.items
@ -124,6 +132,51 @@ export default function CheckoutPage() {
</div> </div>
)} )}
</div> </div>
<div className="order-2 md:order-1 md:w-2/3">
<Accordion
defaultExpandedKeys={['1']}
disabledKeys={sameBilling ? ['2'] : []}
selectionMode="multiple"
className="text-white sm:w-full md:w-2/3"
>
<AccordionItem key="1" title="Shipping Info" className="text-white">
<ShippingForm handleChangeAction={handleChangeShipping} />
<Checkbox defaultSelected onValueChange={(v) => setSameBilling(v)} className="mt-2">
Use same address for billing?
</Checkbox>
</AccordionItem>
<AccordionItem key="2" title="Billing Info" className="text-white">
<ShippingForm handleChangeAction={handleChangeBilling} />
</AccordionItem>
<AccordionItem key="3" title="Payment" className="text-white">
<div className="flex flex-col justify-between overflow-hidden">
<div className="flex flex-col gap-4">
<RadioGroup
defaultValue={paymentGateways?.[0]?.id}
className="flex flex-col gap-4"
>
{paymentGateways.map((gateway: any) => (
<Radio
key={gateway.id}
value={gateway.id}
onChange={(e) => {
setFormData((prev) => ({
...prev,
payment_method: e.target.value,
payment_title: gateway.title
}));
}}
>
{gateway.title}
</Radio>
))}
</RadioGroup>
</div>
</div>
</AccordionItem>
</Accordion>
</div>
</div>
<div className="flex justify-center gap-4"> <div className="flex justify-center gap-4">
<button className="rounded-md bg-indigo-500 p-2 text-white" onClick={() => router.back()}> <button className="rounded-md bg-indigo-500 p-2 text-white" onClick={() => router.back()}>
Back Back

View File

@ -13,9 +13,3 @@
clip-path: inset(0.6px); clip-path: inset(0.6px);
} }
} }
a,
input,
button {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50 dark:focus-visible:ring-neutral-600 dark:focus-visible:ring-offset-neutral-900;
}

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import { Button, Input } from '@nextui-org/react';
import { signIn } from 'next-auth/react'; import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@ -25,46 +26,40 @@ export default function LoginPage() {
<h1 className="text-2xl font-bold">Login</h1> <h1 className="text-2xl font-bold">Login</h1>
<div className="flex w-full max-w-md flex-col"> <div className="flex 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} className="flex flex-col">
<div className="mt-4"> <div className="mt-4">
<label <Input
htmlFor="username"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Username
</label>
<input
type="text" type="text"
id="username" id="username"
label="Username"
labelPlacement="outside"
placeholder="Insert username"
size="lg"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
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"
required
/>
</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"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
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"
required required
/> />
</div> </div>
<div className="mt-6"> <div className="mt-6">
<button <Input
type="password"
id="password"
label="Password"
labelPlacement="outside"
placeholder="Insert password"
size="lg"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<div className="mt-6">
<Button
type="submit" 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" 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"
> >
Login Login
</button> </Button>
</div> </div>
<span className="mt-6 block 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">

View File

@ -6,43 +6,46 @@ export const metadata = {
}; };
import { Carousel } from 'components/carousel'; import { Carousel } from 'components/carousel';
import { ThreeItemGrid } from 'components/grid/three-items'; import { ThreeItemGrid } from 'components/grid/three-items';
import ProductSuspense from 'components/product/product-suspense';
import { Category } from 'lib/woocomerce/models/base'; import { Category } from 'lib/woocomerce/models/base';
import { Product } from 'lib/woocomerce/models/product'; import { Product } from 'lib/woocomerce/models/product';
import { woocommerce } from 'lib/woocomerce/woocommerce'; import { woocommerce } from 'lib/woocomerce/woocommerce';
import { wordpress } from 'lib/wordpress/wordpress'; import { wordpress } from 'lib/wordpress/wordpress';
import React from 'react'; import Link from 'next/link';
import { Suspense } from 'react';
export default async function HomePage() { async function Products({ category }: { category: Category }) {
const products: Product[] = await woocommerce.get('products', {
category: category.id.toString()
});
return <ThreeItemGrid products={products} />;
}
async function ProductsByCategory() {
const categories: Category[] = await woocommerce.get('products/categories'); const categories: Category[] = await woocommerce.get('products/categories');
const productsByCategory: Record<string, Product[]> = {};
await Promise.all(
categories.map((category) =>
woocommerce.get('products', { category: category.id.toString() }).then((products) => {
productsByCategory[category.name] = products;
})
)
);
const posts = await wordpress.get('posts');
return ( return (
<section> <>
{categories.map((category, index) => ( {categories.map((category, index) => (
<div key={category.id} className={index % 2 === 0 ? 'bg-blue-600 py-4' : 'bg-white py-4'}> <div key={category.id}>
<div className="mb-2 mt-6 flex items-center justify-between px-4"> <div className="mb-2 mt-6 flex items-center justify-between px-4">
<span className={`${index % 2 === 0 ? 'text-white' : 'text-black'} text-2xl font-bold`}> <span className="text-2xl font-bold">{category.name}</span>
{category.name}
</span>
</div> </div>
<div className="mb-6 px-4"> <div className="mb-6 px-4">
<span className={`${index % 2 === 0 ? 'text-white' : 'text-black'}`}> <span>{category.description}</span>
{category.description}
</span>
</div> </div>
<React.Fragment key={category.id}> <Suspense
{productsByCategory[category.name] && ( fallback={
<ThreeItemGrid products={productsByCategory[category.name] ?? []} /> <div className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2">
)} {[...Array(3)].map((_, i) => (
</React.Fragment> <ProductSuspense key={i} />
))}
</div>
}
>
<Products category={category} />
</Suspense>
{index === 1 && ( {index === 1 && (
<div className="my-6 flex flex-col px-4"> <div className="my-6 flex flex-col px-4">
<span className="mb-2 text-2xl font-bold">Top products</span> <span className="mb-2 text-2xl font-bold">Top products</span>
@ -51,27 +54,63 @@ export default async function HomePage() {
)} )}
</div> </div>
))} ))}
</>
);
}
async function LatestPosts() {
const posts = await wordpress.get('posts?_embed');
return (
<div className="my-6 flex flex-col px-4"> <div className="my-6 flex flex-col px-4">
<span className="mb-2 text-2xl font-bold">Latest posts</span> <span className="mb-2 text-2xl font-bold">Latest posts</span>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{posts.map((post: any) => ( {posts.map((post: any) => (
<div <div
key={post.id + '-post'} key={post.id + '-post'}
className="rounded-lg border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-black" className="flex flex-col rounded-lg border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-black"
> >
<img <img
src={post.featured_image} src={post._embedded?.['wp:featuredmedia']?.[0]?.source_url}
alt={post.title.rendered} alt={post.title.rendered}
className="h-48 w-full object-cover" className="h-48 w-full object-cover"
/> />
<div className="p-4"> <Link href={`/article/${post.id}`} className="p-4">
<h2 className="text-xl font-bold">{post.title.rendered}</h2> <h2 className="text-xl font-bold">{post.title.rendered}</h2>
<div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} /> <div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
</div> </Link>
</div> </div>
))} ))}
</div> </div>
</div> </div>
);
}
export default async function HomePage() {
return (
<section>
<Suspense
fallback={
<div className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2">
{[...Array(3)].map((_, i) => (
<ProductSuspense key={i} />
))}
</div>
}
>
<ProductsByCategory />
</Suspense>
<Suspense
fallback={
<div className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2">
{[...Array(3)].map((_, i) => (
<ProductSuspense key={i} />
))}
</div>
}
>
<LatestPosts />
</Suspense>
</section> </section>
); );
} }

View File

@ -40,6 +40,43 @@ export async function generateMetadata(props: {
}; };
} }
async function RelatedProducts({ product }: { product: Product }) {
const relatedProducts = await Promise.all(
product.related_ids?.map(async (id) => woocommerce.get(`products/${id}`)) || []
);
return (
<>
{relatedProducts.length > 0 && (
<div className="mt-8 py-4">
<h3 className="text-2xl font-bold">Related Products</h3>
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{relatedProducts.map((relatedProduct) => {
return (
<Link
key={relatedProduct.id}
className="rounded-lg border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-black"
href={`/product/${relatedProduct.slug}`}
>
<img
src={relatedProduct.images?.[0].src}
alt={relatedProduct.name}
className="h-48 w-full object-cover"
/>
<div className="p-4">
<h2 className="text-xl font-bold">{relatedProduct.name}</h2>
<div dangerouslySetInnerHTML={{ __html: relatedProduct.short_description }} />
</div>
</Link>
);
})}
</div>
</div>
)}
</>
);
}
export default async function ProductPage(props: { params: Promise<{ name: string }> }) { export default async function ProductPage(props: { params: Promise<{ name: string }> }) {
const params = await props.params; const params = await props.params;
const product: Product | undefined = ( const product: Product | undefined = (
@ -119,31 +156,9 @@ export default async function ProductPage(props: { params: Promise<{ name: strin
<AddToCart product={product} variations={variations} /> <AddToCart product={product} variations={variations} />
</div> </div>
</div> </div>
<div className="mt-8 py-4"> <Suspense fallback={null}>
<h3 className="text-2xl font-bold">Related Products</h3> <RelatedProducts product={product} />
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> </Suspense>
{relatedProducts.map((relatedProduct) => {
return (
<div
key={relatedProduct.id}
className="rounded-lg border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-black"
>
<img
src={relatedProduct.images?.[0].src}
alt={relatedProduct.name}
className="h-48 w-full object-cover"
/>
<div className="p-4">
<Link href={`/product/${relatedProduct.slug}`}>
<h2 className="text-xl font-bold">{relatedProduct.name}</h2>
</Link>
<div dangerouslySetInnerHTML={{ __html: relatedProduct.short_description }} />
</div>
</div>
);
})}
</div>
</div>
</div> </div>
</ProductProvider> </ProductProvider>
); );

View File

@ -15,7 +15,7 @@ export default async function OrderPage(props: { params: Promise<{ id: number }>
const order = await woocommerce.get('orders', { id: params.id }); const order = await woocommerce.get('orders', { id: params.id });
return ( return (
<section className="mx-auto mt-4 grid max-w-screen-2xl justify-center gap-4 px-4 pb-4"> <section className="mt-4 grid max-w-screen-2xl gap-4 px-4 pb-4">
<h1 className="text-2xl font-bold">Order</h1> <h1 className="text-2xl font-bold">Order</h1>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="mt-4"> <div className="mt-4">

View File

@ -8,7 +8,7 @@ import { Shipping } from 'lib/woocomerce/models/shipping';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
export default function ProfileLayout({ children }: { children: React.ReactNode }) { export default function ProfileLayout({ user }: { user: React.ReactNode }) {
const [customer, setCustomer] = useState<Customer | undefined>(undefined); const [customer, setCustomer] = useState<Customer | undefined>(undefined);
const [shippingAddress, setShippingAddress] = useState<Shipping | undefined>(undefined); const [shippingAddress, setShippingAddress] = useState<Shipping | undefined>(undefined);
@ -31,38 +31,37 @@ export default function ProfileLayout({ children }: { children: React.ReactNode
return ( return (
<section className="mx-auto mt-4 flex max-w-screen-2xl flex-row gap-4 px-4 pb-4"> <section className="mx-auto mt-4 flex max-w-screen-2xl flex-row gap-4 px-4 pb-4">
<div className="flex flex-col rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-black md:w-1/3"> <div className="flex flex-col rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-black md:w-1/3">
<h2 className="mb-2 text-2xl font-bold">Profile</h2>
{customer && ( {customer && (
<div> <div>
<Avatar src={customer.avatar_url} alt="avatar" className="h-11 w-11" /> <Avatar src={customer.avatar_url} alt="avatar" className="h-24 w-24" />
<div> <div className="mt-2">
<span>Ciao </span>
<span className="text-lg font-bold">{customer.first_name}</span> <span className="text-lg font-bold">{customer.first_name}</span>
<span className="text-lg font-bold">{customer.last_name}</span>
</div> </div>
<div className="flex-start mt-3 flex"> <div className="flex-start mt-2 flex">
<Link href={`/profile`} className="hover:text-indigo-500"> <Link href={`/profile`} className="hover:text-indigo-500">
<button type="button" className="flex flex-row items-center rounded-md py-3"> <button type="button" className="flex flex-row items-center rounded-md py-1">
<UserCircleIcon className="me-2 h-4" /> <UserCircleIcon className="me-2 h-4" />
Personal area Personal area
</button> </button>
</Link> </Link>
</div> </div>
<div className="flex-start mt-3 flex"> <div className="flex-start mt-2 flex">
<Link href={`/profile/orders`} className="hover:text-indigo-500"> <Link href={`/profile/orders`} className="hover:text-indigo-500">
<button type="button" className="flex flex-row items-center rounded-md py-3"> <button type="button" className="flex flex-row items-center rounded-md py-1">
<CubeIcon className="me-2 h-4" /> <CubeIcon className="me-2 h-4" />
Orders Orders
</button> </button>
</Link> </Link>
</div> </div>
<div className="mt-3"> <div className="mt-2">
<LogoutButton /> <LogoutButton />
</div> </div>
</div> </div>
)} )}
</div> </div>
<div className="flex rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-black md:w-2/3"> <div className="flex rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-black md:w-2/3">
{children} {user}
</div> </div>
</section> </section>
); );

View File

@ -12,13 +12,15 @@ export default async function SearchPage(props: {
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>; searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const { sort, q: searchValue } = searchParams as { [key: string]: string }; const { sort, q: searchValue, minPrice, maxPrice } = 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', { const products = await woocommerce.get('products', {
search: searchValue, search: searchValue,
orderby: sortKey, orderby: sortKey,
order order,
min_price: minPrice ?? '0',
max_price: maxPrice ?? '1000'
}); });
const resultsText = products.length > 1 ? 'results' : 'result'; const resultsText = products.length > 1 ? 'results' : 'result';

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import { Button, Input } from '@nextui-org/react';
import { useState } from 'react'; import { useState } from 'react';
import { z } from 'zod'; import { z } from 'zod';
@ -66,78 +67,71 @@ export default function SignUpPage() {
<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">Sign up</h1> <h1 className="text-2xl font-bold">Sign up</h1>
<div className="flex justify-center"> <div className="flex justify-center">
<form onSubmit={handleSignup}> <form onSubmit={handleSignup} className="flex w-full max-w-md flex-col">
<div className="mt-4"> <div className="mt-4">
<label <Input
htmlFor="username"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Username
</label>
<input
type="text" type="text"
name="username" name="username"
label="Username"
labelPlacement="outside"
placeholder="Insert username"
size="lg"
value={formData.username} value={formData.username}
onChange={handleChange} 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" isInvalid={!!error['username']}
errorMessage={error['username']}
/> />
{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>
<div className="mt-6"> <div className="mt-6">
<button <Input
type="email"
name="email"
label="Email"
labelPlacement="outside"
placeholder="Insert email"
size="lg"
value={formData.email}
onChange={handleChange}
isInvalid={!!error['email']}
errorMessage={error['email']}
className="mt-1 block w-full"
/>
</div>
<div className="mt-6">
<Input
type="password"
name="password"
label="Password"
labelPlacement="outside"
placeholder="Insert password"
size="lg"
value={formData.password}
onChange={handleChange}
className="mt-1 block w-full"
/>
</div>
<div className="mt-6">
<Input
type="password"
name="confirmPassword"
label="Confirm password"
labelPlacement="outside"
placeholder="Insert confirm password"
size="lg"
value={formData.confirmPassword}
onChange={handleChange}
isInvalid={!!error['confirmPassword']}
errorMessage={error['confirmPassword']}
className="mt-1 block w-full"
/>
</div>
<div className="mt-6">
<Button
type="submit" 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" 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 Sign up
</button> </Button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -8,7 +8,7 @@ export default function LogoutButton() {
return ( return (
<button <button
type="button" type="button"
className="flex flex-row items-center rounded-md py-3 hover:text-indigo-500" className="flex flex-row items-center rounded-md py-1 hover:text-indigo-500"
onClick={() => { onClick={() => {
signOut({ callbackUrl: '/' }); signOut({ callbackUrl: '/' });
}} }}

View File

@ -15,7 +15,6 @@ import OpenCart from './open-cart';
export default function CartModal() { export default function CartModal() {
const { cart, setNewCart } = useCart(); const { cart, setNewCart } = useCart();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [userIsLoggedIn, setUserIsLoggedIn] = useState(false);
const openCart = () => setIsOpen(true); const openCart = () => setIsOpen(true);
const closeCart = () => setIsOpen(false); const closeCart = () => setIsOpen(false);
const { data } = useSession(); const { data } = useSession();
@ -100,15 +99,6 @@ export default function CartModal() {
})} })}
</ul> </ul>
<div className="py-4 text-sm text-neutral-500 dark:text-neutral-400"> <div className="py-4 text-sm text-neutral-500 dark:text-neutral-400">
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 dark:border-neutral-700">
<p>Taxes</p>
<Price
className="text-right text-base text-black dark:text-white"
amount={cart.totals?.total_price}
needSplit
currencyCode={cart.totals.currency_code}
/>
</div>
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700"> <div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700">
<p>Shipping</p> <p>Shipping</p>
<p className="text-right">Calculated at checkout</p> <p className="text-right">Calculated at checkout</p>
@ -119,7 +109,7 @@ export default function CartModal() {
className="text-right text-base text-black dark:text-white" className="text-right text-base text-black dark:text-white"
amount={cart.totals?.total_price} amount={cart.totals?.total_price}
needSplit needSplit
currencyCode={cart.totals.currency_code} currencyCode={cart.totals?.currency_code}
/> />
</div> </div>
</div> </div>

View File

@ -0,0 +1,3 @@
export default function PaymentsForm() {
return <div></div>;
}

View File

@ -1,4 +1,5 @@
import { GridTileImage } from 'components/grid/tile'; import { GridTileImage } from 'components/grid/tile';
import { ProductCard } from 'components/product/product-card';
import { Product } from 'lib/woocomerce/models/product'; import { Product } from 'lib/woocomerce/models/product';
import Link from 'next/link'; import Link from 'next/link';
@ -42,9 +43,9 @@ export function ThreeItemGridItem({
export async function ThreeItemGrid({ products }: { products: Product[] }) { export async function ThreeItemGrid({ products }: { products: Product[] }) {
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"> <section className="mx-auto grid gap-4 px-4 pb-4 md:grid-cols-8">
{products.map((product, index) => ( {products.map((product, index) => (
<ThreeItemGridItem key={product.id} size={index === 0 ? 'full' : 'half'} item={product} /> <ProductCard key={product.id} product={product} />
))} ))}
</section> </section>
); );

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { Input } from '@nextui-org/react';
import Form from 'next/form'; import Form from 'next/form';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
@ -8,15 +9,15 @@ export default function Search() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
return ( return (
<Form action="/search" className="w-max-[550px] relative w-full lg:w-80 xl:w-full"> <Form action="/search" className="w-max-[450px] relative ms-4 w-full lg:w-80 xl:w-full">
<input <Input
key={searchParams?.get('q')} key={searchParams?.get('q')}
type="text" type="text"
name="q" name="q"
placeholder="Search for products..." placeholder="Search for products..."
autoComplete="off" autoComplete="off"
defaultValue={searchParams?.get('q') || ''} defaultValue={searchParams?.get('q') || ''}
className="text-md w-full rounded-lg border bg-white px-4 py-2 text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400 md:text-sm" className=""
/> />
<div className="absolute right-0 top-0 mr-3 flex h-full items-center"> <div className="absolute right-0 top-0 mr-3 flex h-full items-center">
<MagnifyingGlassIcon className="h-4" /> <MagnifyingGlassIcon className="h-4" />
@ -28,7 +29,7 @@ export default function Search() {
export function SearchSkeleton() { export function SearchSkeleton() {
return ( return (
<form className="w-max-[550px] relative w-full lg:w-80 xl:w-full"> <form className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
<input <Input
placeholder="Search for products..." placeholder="Search for products..."
className="w-full rounded-lg border bg-white px-4 py-2 text-sm text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400" className="w-full rounded-lg border bg-white px-4 py-2 text-sm text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400"
/> />

View File

@ -1,7 +1,8 @@
'use client'; 'use client';
import { Slider } from '@nextui-org/react'; import { Slider } from '@nextui-org/react';
import { SortFilterItem } from 'lib/constants'; import { SortFilterItem } from 'lib/constants';
import { Suspense } from 'react'; import { useRouter, useSearchParams } from 'next/navigation';
import { Suspense, useState } from 'react';
import FilterItemDropdown from './dropdown'; import FilterItemDropdown from './dropdown';
import { FilterItem } from './item'; import { FilterItem } from './item';
@ -9,19 +10,40 @@ export type ListItem = SortFilterItem | PathFilterItem;
export type PathFilterItem = { title: string; path: string }; export type PathFilterItem = { title: string; path: string };
function FilterItemList({ list }: { list: ListItem[] }) { function FilterItemList({ list }: { list: ListItem[] }) {
const searchParams = useSearchParams();
const router = useRouter();
const [sliderValue, setSliderValue] = useState<number | number[]>(
searchParams.get('minPrice')
? [Number(searchParams.get('minPrice')), Number(searchParams.get('maxPrice'))]
: [0, 120]
);
const addQuerySlider = (value: number | number[]) => {
const newParams = new URLSearchParams(searchParams.toString());
if (Array.isArray(value)) {
newParams.set('minPrice', String(value[0]));
newParams.set('maxPrice', String(value[1]));
}
router.push(`?${newParams.toString()}`, { scroll: false });
};
return ( return (
<> <>
{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="mt-3 max-w-md"
defaultValue={[100, 500]} defaultValue={[40, 120]}
formatOptions={{ style: 'currency', currency: 'USD' }} formatOptions={{ style: 'currency', currency: 'EUR' }}
label="Price Range" label="Price Range"
maxValue={1000} maxValue={150}
minValue={0} minValue={0}
step={50} value={sliderValue}
step={10}
onChange={setSliderValue}
onChangeEnd={addQuerySlider}
/> />
</> </>
); );

View File

@ -0,0 +1,37 @@
'use client';
import { Card, CardBody, CardFooter } from '@nextui-org/react';
import Price from 'components/price';
import { Product } from 'lib/woocomerce/models/product';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
export function ProductCard({ product }: { product: Product }) {
const router = useRouter();
return (
<div className="md:col-span-2 md:row-span-1">
<Card
isPressable
shadow="sm"
onPress={() => router.push(`/product/${product.slug}`)}
className="h-full w-full"
>
<CardBody className="p-0">
<Image
alt={product.name}
className="h-full w-full object-cover"
src={product.images?.[0]?.src || ''}
width={540}
height={540}
/>
</CardBody>
<CardFooter className="flex-col items-center justify-center py-6">
<h3 className="mr-4 line-clamp-2 flex-grow pl-2 font-bold leading-none tracking-tight">
{product.name}
</h3>
<Price className="mt-2" amount={product.price} currencyCode="EUR" />
</CardFooter>
</Card>
</div>
);
}

View File

@ -0,0 +1,33 @@
import clsx from 'clsx';
export default function ProductSuspense() {
return (
<div className="md:col-span-2 md:row-span-1">
<span className="relative block aspect-square h-full w-full">
<div
className={clsx(
'group flex h-full w-full items-center justify-center overflow-hidden rounded-lg border border-neutral-200 bg-white hover:border-blue-600 dark:border-neutral-800 dark:bg-black'
)}
>
<div
className={clsx(
'relative h-full w-full bg-gray-200 object-contain dark:bg-black',
'transition duration-300 ease-in-out group-hover:scale-105'
)}
>
<div
className={clsx(
'absolute bottom-0 left-0 flex w-full px-4 pb-4 @container/label',
'lg:px-20 lg:pb-[35%]'
)}
>
<div className="flex items-center rounded-full border bg-white/70 p-1 text-xs font-semibold text-black backdrop-blur-md dark:border-neutral-800 dark:bg-black/70 dark:text-white">
<span className="mr-4 line-clamp-2 flex-grow pl-2 leading-none tracking-tight"></span>
</div>
</div>
</div>
</div>
</span>
</div>
);
}

View File

@ -1,10 +1,12 @@
'use client'; 'use client';
import { Avatar, Select, SelectItem } from '@nextui-org/react'; import { Avatar, Input, Select, SelectItem } from '@nextui-org/react';
import clsx from 'clsx'; import clsx from 'clsx';
import { getCountries } from 'lib/utils'; import { getCountries } from 'lib/utils';
import { Shipping } from 'lib/woocomerce/models/shipping'; import { Billing } from 'lib/woocomerce/models/billing';
import { useState } from 'react'; import { useState } from 'react';
const optionalFields = ['company'];
export default function ShippingForm({ export default function ShippingForm({
className, className,
title, title,
@ -12,10 +14,10 @@ export default function ShippingForm({
}: { }: {
className?: string; className?: string;
title?: string; title?: string;
handleChangeAction?: (data: Shipping) => void; handleChangeAction?: (data: Billing) => void;
}) { }) {
const countries = getCountries(); const countries = getCountries();
const initialState: Shipping = { const initialState: Billing = {
first_name: '', first_name: '',
last_name: '', last_name: '',
address_1: '', address_1: '',
@ -24,7 +26,9 @@ export default function ShippingForm({
state: '', state: '',
postcode: '', postcode: '',
country: '', country: '',
company: '' company: '',
phone: '',
email: ''
}; };
const [formData, setFormData] = useState(initialState); const [formData, setFormData] = useState(initialState);
@ -36,73 +40,27 @@ export default function ShippingForm({
} }
}; };
const getLabel = (key: string) => key.charAt(0).toUpperCase() + key.slice(1).replace('_', ' ');
return ( return (
<div className={clsx('flex flex-col', className)}> <div className={clsx('flex flex-col', className)}>
{title && <h2 className="mt-2 text-2xl font-bold">{title}</h2>} {title && <h2 className="mt-2 text-2xl font-bold">{title}</h2>}
<div className="mt-4"> {Object.entries(formData)
<label .filter(([key]) => key !== 'country')
htmlFor="address_1" .map(([key, value], index) => (
className="block text-sm font-medium text-gray-700 dark:text-gray-300" <div className={index !== 0 ? 'mt-4' : ''} key={key}>
> <Input
Address <span className="text-red-500">*</span>
</label>
<input
type="text" type="text"
name="address_1" name={key}
value={formData.address_1} value={value}
placeholder={`Insert ${getLabel(key)}`}
isRequired={!optionalFields.includes(key)}
size="md"
onChange={onChange} onChange={onChange}
className="mt-1 block w-full rounded-md border-gray-300 p-3 text-black shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-lg" label={getLabel(key)}
required
/>
</div>
<div className="mt-4">
<label
htmlFor="city"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
City <span className="text-red-500">*</span>
</label>
<input
type="text"
name="city"
value={formData.city}
onChange={onChange}
className="mt-1 block w-full rounded-md border-gray-300 p-3 text-black shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-lg"
required
/>
</div>
<div className="mt-4">
<label
htmlFor="state"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
State <span className="text-red-500">*</span>
</label>
<input
type="text"
name="state"
value={formData.state}
onChange={onChange}
className="mt-1 block w-full rounded-md border-gray-300 p-3 text-black shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-lg"
required
/>
</div>
<div className="mt-4">
<label
htmlFor="postcode"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Postcode <span className="text-red-500">*</span>
</label>
<input
type="text"
name="postcode"
value={formData.postcode}
onChange={onChange}
className="mt-1 block w-full rounded-md border-gray-300 p-3 text-black shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-lg"
required
/> />
</div> </div>
))}
<div className="mt-4"> <div className="mt-4">
<label <label
htmlFor="country" htmlFor="country"
@ -115,9 +73,15 @@ export default function ShippingForm({
className="max-w-xs" className="max-w-xs"
isRequired isRequired
name="country" name="country"
aria-label="Select a country"
value={formData.country} value={formData.country}
onChange={(event) => onChange={(event) =>
onChange({ target: { name: 'country', value: event.target.value } }) onChange({
target: {
name: 'country',
value: event.target.value,
} as unknown as EventTarget & HTMLInputElement,
} as React.ChangeEvent<HTMLInputElement>)
} }
> >
{countries.map((item) => ( {countries.map((item) => (