mirror of
https://github.com/vercel/commerce.git
synced 2025-06-28 01:11:24 +00:00
refactor: all components
This commit is contained in:
parent
23f9f55fab
commit
a16571aca0
@ -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
14
app/api/payments/route.ts
Normal 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
20
app/article/[id]/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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,52 +101,81 @@ 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">
|
{cart && (
|
||||||
<ShippingForm handleChangeAction={handleChangeShipping} />
|
<div className="ms-4 flex flex-col justify-between overflow-hidden">
|
||||||
<Checkbox onValueChange={(v) => setSameBilling(v)} className="mt-2">
|
<ul className="flex-grow overflow-auto">
|
||||||
Use same address for billing?
|
{cart.items?.length &&
|
||||||
</Checkbox>
|
cart.items
|
||||||
</AccordionItem>
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
<AccordionItem key="2" title="Billing Info" className="text-white">
|
.map((item, i) => {
|
||||||
<ShippingForm handleChangeAction={handleChangeBilling} />
|
return (
|
||||||
</AccordionItem>
|
<li
|
||||||
<AccordionItem key="3" title="Payment" className="text-white">
|
key={i}
|
||||||
<div className="flex flex-col justify-between overflow-hidden p-1">
|
className="flex w-full flex-col border-b border-neutral-300 dark:border-neutral-700"
|
||||||
<h2 className="mt-2 text-2xl font-bold">Payment</h2>
|
>
|
||||||
|
<CartItemView item={item} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<div className="mb-3 flex items-center justify-between pb-4 pt-4 dark:border-neutral-700">
|
||||||
|
<p>Total</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>
|
</div>
|
||||||
</AccordionItem>
|
)}
|
||||||
</Accordion>
|
</div>
|
||||||
|
<div className="order-2 md:order-1 md:w-2/3">
|
||||||
{cart && (
|
<Accordion
|
||||||
<div className="ms-4 flex flex-col justify-between overflow-hidden md:w-1/3">
|
defaultExpandedKeys={['1']}
|
||||||
<ul className="flex-grow overflow-auto">
|
disabledKeys={sameBilling ? ['2'] : []}
|
||||||
{cart.items?.length &&
|
selectionMode="multiple"
|
||||||
cart.items
|
className="text-white sm:w-full md:w-2/3"
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
>
|
||||||
.map((item, i) => {
|
<AccordionItem key="1" title="Shipping Info" className="text-white">
|
||||||
return (
|
<ShippingForm handleChangeAction={handleChangeShipping} />
|
||||||
<li
|
<Checkbox defaultSelected onValueChange={(v) => setSameBilling(v)} className="mt-2">
|
||||||
key={i}
|
Use same address for billing?
|
||||||
className="flex w-full flex-col border-b border-neutral-300 dark:border-neutral-700"
|
</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
|
||||||
|
}));
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<CartItemView item={item} />
|
{gateway.title}
|
||||||
</li>
|
</Radio>
|
||||||
);
|
))}
|
||||||
})}
|
</RadioGroup>
|
||||||
</ul>
|
</div>
|
||||||
<div className="mb-3 flex items-center justify-between pb-4 pt-4 dark:border-neutral-700">
|
</div>
|
||||||
<p>Total</p>
|
</AccordionItem>
|
||||||
<Price
|
</Accordion>
|
||||||
className="text-right text-base text-black dark:text-white"
|
</div>
|
||||||
amount={cart?.totals?.total_price}
|
|
||||||
needSplit
|
|
||||||
currencyCode={cart?.totals.currency_code}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</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()}>
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
@ -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">
|
||||||
|
127
app/page.tsx
127
app/page.tsx
@ -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>
|
||||||
))}
|
))}
|
||||||
<div className="my-6 flex flex-col px-4">
|
</>
|
||||||
<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">
|
}
|
||||||
{posts.map((post: any) => (
|
|
||||||
<div
|
async function LatestPosts() {
|
||||||
key={post.id + '-post'}
|
const posts = await wordpress.get('posts?_embed');
|
||||||
className="rounded-lg border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-black"
|
|
||||||
>
|
return (
|
||||||
<img
|
<div className="my-6 flex flex-col px-4">
|
||||||
src={post.featured_image}
|
<span className="mb-2 text-2xl font-bold">Latest posts</span>
|
||||||
alt={post.title.rendered}
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
className="h-48 w-full object-cover"
|
{posts.map((post: any) => (
|
||||||
/>
|
<div
|
||||||
<div className="p-4">
|
key={post.id + '-post'}
|
||||||
<h2 className="text-xl font-bold">{post.title.rendered}</h2>
|
className="flex flex-col rounded-lg border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-black"
|
||||||
<div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
|
>
|
||||||
</div>
|
<img
|
||||||
</div>
|
src={post._embedded?.['wp:featuredmedia']?.[0]?.source_url}
|
||||||
))}
|
alt={post.title.rendered}
|
||||||
</div>
|
className="h-48 w-full object-cover"
|
||||||
|
/>
|
||||||
|
<Link href={`/article/${post.id}`} className="p-4">
|
||||||
|
<h2 className="text-xl font-bold">{post.title.rendered}</h2>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
|
||||||
|
</Link>
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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">
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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: '/' });
|
||||||
}}
|
}}
|
||||||
|
@ -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>
|
||||||
|
3
components/checkout/payments-form.tsx
Normal file
3
components/checkout/payments-form.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function PaymentsForm() {
|
||||||
|
return <div></div>;
|
||||||
|
}
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
37
components/product/product-card.tsx
Normal file
37
components/product/product-card.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
33
components/product/product-suspense.tsx
Normal file
33
components/product/product-suspense.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
type="text"
|
||||||
</label>
|
name={key}
|
||||||
<input
|
value={value}
|
||||||
type="text"
|
placeholder={`Insert ${getLabel(key)}`}
|
||||||
name="address_1"
|
isRequired={!optionalFields.includes(key)}
|
||||||
value={formData.address_1}
|
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>
|
))}
|
||||||
<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 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) => (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user