feat: change price by product variation

This commit is contained in:
paolosantarsiero 2024-12-30 01:18:53 +01:00
parent e7be2b4695
commit a5e995fbe0
10 changed files with 3212 additions and 63 deletions

1
.npmrc Normal file
View File

@ -0,0 +1 @@
public-hoist-pattern[]=*@nextui-org/*

View File

@ -1,9 +1,247 @@
export default async function CheckoutPage(props: { params: Promise<{ id: number }> }) { 'use client';
const params = await props.params;
import { useCart } from 'components/cart/cart-context';
import CartItemView from 'components/cart/cart-item';
import { OrderPayload } from 'lib/woocomerce/storeApi';
import { useState } from 'react';
export default function CheckoutPage() {
const { cart } = useCart();
const initialState: OrderPayload = {
shipping_address: {
first_name: '',
last_name: '',
company: '',
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: ''
},
billing_address: {
first_name: '',
last_name: '',
company: '',
email: '',
phone: '',
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: ''
},
payment_method: '',
payment_data: []
};
const [formData, setFormData] = useState(initialState);
const handleChangeShipping = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
shipping_address: { ...prev.shipping_address, [e.target.name]: e.target.value }
}));
};
const handleChangeBilling = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
billing_address: { ...prev.billing_address, [e.target.name]: e.target.value }
}));
};
return ( return (
<section className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2 lg:max-h-[calc(100vh-200px)]"> <section className="mx-auto grid h-full gap-4 px-4 pb-4">
<h1>Checkout</h1> <div className="col-span-4 row-span-2 h-full rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black">
<p>Checkout</p>
<div className="flex flex-col justify-between overflow-hidden p-1">
<ul className="flex-grow overflow-auto py-4">
{cart &&
cart.items?.length &&
cart.items
.sort((a, b) => a.name.localeCompare(b.name))
.map((item, i) => {
return (
<li
key={i}
className="flex w-full flex-col border-b border-neutral-300 dark:border-neutral-700"
>
<CartItemView item={item} />
</li>
);
})}
</ul>
<h2 className="mt-2 text-2xl font-bold">Shipping info</h2>
<form className="md:grid-cols-6 md:grid-rows-2 gap-4">
<div className="mt-4">
<label
htmlFor="address_1"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Address
</label>
<input
type="text"
name="address_1"
value={formData.shipping_address.address_1}
onChange={handleChangeShipping}
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"
/>
</div>
<div className="mt-4">
<label
htmlFor="city"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
City
</label>
<input
type="text"
name="city"
value={formData.shipping_address.city}
onChange={handleChangeShipping}
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"
/>
</div>
<div className="mt-4">
<label
htmlFor="state"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
State
</label>
<input
type="text"
name="state"
value={formData.shipping_address.state}
onChange={handleChangeShipping}
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"
/>
</div>
<div className="mt-4">
<label
htmlFor="postcode"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Postcode
</label>
<input
type="text"
name="postcode"
value={formData.shipping_address.postcode}
onChange={handleChangeShipping}
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"
/>
</div>
<div className="mt-4">
<label
htmlFor="country"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Country
</label>
<input
type="text"
name="country"
value={formData.shipping_address.country}
onChange={handleChangeShipping}
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"
/>
</div>
</form>
</div>
</div>
<div className="col-span-4 row-span-2 h-full rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black">
<div className="flex flex-col justify-between overflow-hidden p-1">
<form className="flex flex-col gap-4">
<h2 className="mt-2 text-2xl font-bold">Billing info</h2>
<div className="mt-4">
<label
htmlFor="address_1"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Address
</label>
<input
type="text"
name="address_1"
value={formData.billing_address.address_1}
onChange={handleChangeBilling}
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"
/>
</div>
<div className="mt-4">
<label
htmlFor="city"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
City
</label>
<input
type="text"
name="city"
value={formData.billing_address.city}
onChange={handleChangeBilling}
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"
/>
</div>
<div className="mt-4">
<label
htmlFor="state"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
State
</label>
<input
type="text"
name="state"
value={formData.billing_address.state}
onChange={handleChangeBilling}
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"
/>
</div>
<div className="mt-4">
<label
htmlFor="postcode"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Postcode
</label>
<input
type="text"
name="postcode"
value={formData.billing_address.postcode}
onChange={handleChangeBilling}
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"
/>
</div>
<div className="mt-4">
<label
htmlFor="country"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Country
</label>
<input
type="text"
name="country"
value={formData.billing_address.country}
onChange={handleChangeBilling}
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"
/>
</div>
</form>
</div>
</div>
<div className="col-span-4 row-span-2 h-full rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black">
<div className="flex flex-col justify-between overflow-hidden p-1">
<h2 className="mt-2 text-2xl font-bold">Payment</h2>
</div>
</div>
</section> </section>
); );
} }

View File

@ -94,13 +94,14 @@ export default async function ProductPage(props: { params: Promise<{ name: strin
</div> </div>
<div className="basis-full lg:basis-2/6"> <div className="basis-full lg:basis-2/6">
<h1 className="mb-2 text-5xl font-medium">{product.name}</h1>
{variations && ( {variations && (
<Suspense fallback={null}> <Suspense fallback={null}>
<VariantSelector options={product.attributes} variations={variations} /> <VariantSelector options={product.attributes} variations={variations} />
</Suspense> </Suspense>
)} )}
<Suspense fallback={null}> <Suspense fallback={null}>
<ProductDescription product={product} variations={variations} /> <ProductDescription product={product} variations={variations}/>
</Suspense> </Suspense>
<AddToCart product={product} variations={variations}/> <AddToCart product={product} variations={variations}/>
</div> </div>

View File

@ -7,12 +7,13 @@ export default function SearchLayout({ children }: { children: React.ReactNode }
return ( return (
<> <>
<div className="mx-auto flex max-w-screen-2xl flex-col gap-8 px-4 pb-4 text-black md:flex-row dark:text-white"> <div className="mx-auto flex max-w-screen-2xl flex-col gap-8 px-4 pb-4 text-black md:flex-row dark:text-white">
<div className="order-first w-full flex-none md:max-w-[125px]"></div> <div className="order-first w-full flex-none md:max-w-[150px]">
<FilterList list={sorting} title="Sort by" />
</div>
<div className="order-last min-h-screen w-full md:order-none"> <div className="order-last min-h-screen w-full md:order-none">
<ChildrenWrapper>{children}</ChildrenWrapper> <ChildrenWrapper>{children}</ChildrenWrapper>
</div> </div>
<div className="order-none flex-none md:order-last md:w-[125px]"> <div className="order-none flex-none md:order-last md:w-[100px]">
<FilterList list={sorting} title="Sort by" />
</div> </div>
</div> </div>
<Footer /> <Footer />

View File

@ -0,0 +1,66 @@
import Price from 'components/price';
import type { CartItem } from 'lib/woocomerce/models/cart';
import Image from 'next/image';
import Link from 'next/link';
import { DeleteItemButton } from './delete-item-button';
import { EditItemQuantityButton } from './edit-item-quantity-button';
export default function CartItemView({
item,
deletable = false,
editable = false,
closeCart = () => {}
}: {
item: CartItem;
deletable?: boolean;
editable?: boolean;
closeCart?: () => void;
}) {
return (
<div className="relative flex w-full flex-row justify-between px-1 py-4">
{deletable && (
<div className="absolute z-40 -ml-1 -mt-2">
<DeleteItemButton item={item} />
</div>
)}
<div className="flex flex-row">
<div className="relative h-16 w-16 overflow-hidden rounded-md border border-neutral-300 bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800">
<Image
className="h-full w-full object-cover"
width={64}
height={64}
alt={item.name}
src={item.images?.[0]?.src || ''}
/>
</div>
<Link href={''} onClick={closeCart} className="z-30 ml-2 flex flex-row space-x-4">
<div className="flex flex-1 flex-col text-base">
<span className="leading-tight">{item.name}</span>
{item.variation.map((variation, i) => (
<span key={i} className="text-sm text-neutral-500 dark:text-neutral-400">
{variation.attribute}: {variation.value}
</span>
))}
</div>
</Link>
</div>
<div className="flex h-16 flex-col justify-between">
<Price
className="flex justify-end space-y-2 text-right text-sm"
amount={item.prices?.price}
needSplit
currencyCode={item.prices.currency_code}
/>
{editable && (
<div className="ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-200 dark:border-neutral-700">
<EditItemQuantityButton item={item} type="minus" />
<p className="w-6 text-center">
<span className="w-full text-sm">{item.quantity}</span>
</p>
<EditItemQuantityButton item={item} type="plus" />
</div>
)}
</div>
</div>
);
}

View File

@ -5,14 +5,11 @@ import { ShoppingCartIcon } from '@heroicons/react/24/outline';
import LoadingDots from 'components/loading-dots'; import LoadingDots from 'components/loading-dots';
import Price from 'components/price'; import Price from 'components/price';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import Image from 'next/image';
import Link from 'next/link';
import { Fragment, useEffect, useState } from 'react'; import { Fragment, useEffect, useState } from 'react';
import { useFormStatus } from 'react-dom'; import { useFormStatus } from 'react-dom';
import { useCart } from './cart-context'; import { useCart } from './cart-context';
import CartItemView from './cart-item';
import CloseCart from './close-cart'; import CloseCart from './close-cart';
import { DeleteItemButton } from './delete-item-button';
import { EditItemQuantityButton } from './edit-item-quantity-button';
import OpenCart from './open-cart'; import OpenCart from './open-cart';
export default function CartModal() { export default function CartModal() {
@ -92,51 +89,7 @@ export default function CartModal() {
key={i} key={i}
className="flex w-full flex-col border-b border-neutral-300 dark:border-neutral-700" className="flex w-full flex-col border-b border-neutral-300 dark:border-neutral-700"
> >
<div className="relative flex w-full flex-row justify-between px-1 py-4"> <CartItemView item={item} editable={true} closeCart={closeCart} />
<div className="absolute z-40 -ml-1 -mt-2">
<DeleteItemButton item={item} />
</div>
<div className="flex flex-row">
<div className="relative h-16 w-16 overflow-hidden rounded-md border border-neutral-300 bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800">
<Image
className="h-full w-full object-cover"
width={64}
height={64}
alt={item.name}
src={item.images?.[0]?.src || ''}
/>
</div>
<Link
href={''}
onClick={closeCart}
className="z-30 ml-2 flex flex-row space-x-4"
>
<div className="flex flex-1 flex-col text-base">
<span className="leading-tight">{item.name}</span>
{item.variation.map((variation, i) => (
<span key={i} className="text-sm text-neutral-500 dark:text-neutral-400">
{variation.attribute}: {variation.value}
</span>
))}
</div>
</Link>
</div>
<div className="flex h-16 flex-col justify-between">
<Price
className="flex justify-end space-y-2 text-right text-sm"
amount={item.prices?.price}
needSplit
currencyCode={item.prices.currency_code}
/>
<div className="ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-200 dark:border-neutral-700">
<EditItemQuantityButton item={item} type="minus" />
<p className="w-6 text-center">
<span className="w-full text-sm">{item.quantity}</span>
</p>
<EditItemQuantityButton item={item} type="plus" />
</div>
</div>
</div>
</li> </li>
); );
})} })}

View File

@ -11,7 +11,6 @@ export function ProductDescription({ product, variations }: { product: Product,
return ( return (
<> <>
<div className="mb-6 flex flex-col border-b pb-6 dark:border-neutral-700"> <div className="mb-6 flex flex-col border-b pb-6 dark:border-neutral-700">
<h1 className="mb-2 text-5xl font-medium">{product.name}</h1>
<div className="mr-auto w-auto rounded-full bg-blue-600 p-2 text-sm text-white"> <div className="mr-auto w-auto rounded-full bg-blue-600 p-2 text-sm text-white">
<Price amount={productVariant ? productVariant.price : product.price} currencyCode="EUR" /> <Price amount={productVariant ? productVariant.price : product.price} currencyCode="EUR" />
</div> </div>

View File

@ -1,10 +1,27 @@
import axios, { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; import axios, { AxiosInstance, RawAxiosRequestHeaders } from 'axios';
import { Billing } from './models/billing';
import { Cart } from './models/cart'; import { Cart } from './models/cart';
import { Order } from './models/orders';
import { Shipping } from './models/shipping';
/** /**
* WooCommerce Store API Client for server-side requests. * WooCommerce Store API Client for server-side requests.
* To use this in the client-side, you need to create a new route of api endpoint in your Next.js app. * To use this in the client-side, you need to create a new route of api endpoint in your Next.js app.
*/ */
export type OrderPayload = {
billing_address: Billing;
shipping_address: Shipping;
payment_method: string;
payment_data?: PaymentMethodData[];
customer_note?: string;
}
export type PaymentMethodData = {
key: string;
value: string;
}
class WooCommerceStoreApiClient { class WooCommerceStoreApiClient {
private client: AxiosInstance; private client: AxiosInstance;
@ -62,6 +79,18 @@ class WooCommerceStoreApiClient {
.post<Cart>(`/cart/remove-item?key=${payload.key}`) .post<Cart>(`/cart/remove-item?key=${payload.key}`)
.then((response) => response.data); .then((response) => response.data);
} }
async createOrder(order: OrderPayload): Promise<Order> {
return this.client.post('/checkout', order).then((response) => response.data);
}
async getOrders(params?: Record<string, string | number>): Promise<Order[]> {
return this.client.get<Order[]>('/checkout', { params }).then((response) => response.data);
}
async getOrder(id: string | number): Promise<Order> {
return this.client.get<Order>(`/checkout/${id}`).then((response) => response.data);
}
} }
// Example usage. // Example usage.

View File

@ -11,7 +11,9 @@
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.0",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@nextui-org/react": "^2.6.10",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^11.15.0",
"geist": "^1.3.1", "geist": "^1.3.1",
"next": "15.0.4", "next": "15.0.4",
"next-auth": "^4.24.11", "next-auth": "^4.24.11",

2869
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff