mirror of
https://github.com/vercel/commerce.git
synced 2025-06-28 01:11:24 +00:00
feat: change price by product variation
This commit is contained in:
parent
e7be2b4695
commit
a5e995fbe0
@ -1,9 +1,247 @@
|
||||
export default async function CheckoutPage(props: { params: Promise<{ id: number }> }) {
|
||||
const params = await props.params;
|
||||
'use client';
|
||||
|
||||
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 (
|
||||
<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)]">
|
||||
<h1>Checkout</h1>
|
||||
<section className="mx-auto grid h-full gap-4 px-4 pb-4">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -94,6 +94,7 @@ export default async function ProductPage(props: { params: Promise<{ name: strin
|
||||
</div>
|
||||
|
||||
<div className="basis-full lg:basis-2/6">
|
||||
<h1 className="mb-2 text-5xl font-medium">{product.name}</h1>
|
||||
{variations && (
|
||||
<Suspense fallback={null}>
|
||||
<VariantSelector options={product.attributes} variations={variations} />
|
||||
|
@ -7,12 +7,13 @@ export default function SearchLayout({ children }: { children: React.ReactNode }
|
||||
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="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">
|
||||
<ChildrenWrapper>{children}</ChildrenWrapper>
|
||||
</div>
|
||||
<div className="order-none flex-none md:order-last md:w-[125px]">
|
||||
<FilterList list={sorting} title="Sort by" />
|
||||
<div className="order-none flex-none md:order-last md:w-[100px]">
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
|
66
components/cart/cart-item.tsx
Normal file
66
components/cart/cart-item.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -5,14 +5,11 @@ import { ShoppingCartIcon } from '@heroicons/react/24/outline';
|
||||
import LoadingDots from 'components/loading-dots';
|
||||
import Price from 'components/price';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { useFormStatus } from 'react-dom';
|
||||
import { useCart } from './cart-context';
|
||||
import CartItemView from './cart-item';
|
||||
import CloseCart from './close-cart';
|
||||
import { DeleteItemButton } from './delete-item-button';
|
||||
import { EditItemQuantityButton } from './edit-item-quantity-button';
|
||||
import OpenCart from './open-cart';
|
||||
|
||||
export default function CartModal() {
|
||||
@ -92,51 +89,7 @@ export default function CartModal() {
|
||||
key={i}
|
||||
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">
|
||||
<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>
|
||||
<CartItemView item={item} editable={true} closeCart={closeCart} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
@ -11,7 +11,6 @@ export function ProductDescription({ product, variations }: { product: Product,
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<Price amount={productVariant ? productVariant.price : product.price} currencyCode="EUR" />
|
||||
</div>
|
||||
|
@ -1,10 +1,27 @@
|
||||
import axios, { AxiosInstance, RawAxiosRequestHeaders } from 'axios';
|
||||
import { Billing } from './models/billing';
|
||||
import { Cart } from './models/cart';
|
||||
import { Order } from './models/orders';
|
||||
import { Shipping } from './models/shipping';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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 {
|
||||
private client: AxiosInstance;
|
||||
|
||||
@ -62,6 +79,18 @@ class WooCommerceStoreApiClient {
|
||||
.post<Cart>(`/cart/remove-item?key=${payload.key}`)
|
||||
.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.
|
||||
|
@ -11,7 +11,9 @@
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@nextui-org/react": "^2.6.10",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.15.0",
|
||||
"geist": "^1.3.1",
|
||||
"next": "15.0.4",
|
||||
"next-auth": "^4.24.11",
|
||||
|
2869
pnpm-lock.yaml
generated
2869
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user