mirror of
https://github.com/vercel/commerce.git
synced 2025-07-25 11:11:24 +00:00
feat: add vehicle details to cart attributes
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
'use server';
|
||||
|
||||
import { TAGS } from 'lib/constants';
|
||||
import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/shopify';
|
||||
import {
|
||||
addToCart,
|
||||
createCart,
|
||||
getCart,
|
||||
removeFromCart,
|
||||
setCartAttributes,
|
||||
updateCart
|
||||
} from 'lib/shopify';
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
@@ -34,6 +41,35 @@ export async function addItem(prevState: any, selectedVariantIds: Array<string>)
|
||||
}
|
||||
}
|
||||
|
||||
export async function setMetafields(
|
||||
prevState: any,
|
||||
formData: { customer_vin: string; customer_mileage: string }
|
||||
) {
|
||||
const cartId = cookies().get('cartId')?.value;
|
||||
|
||||
if (!cartId) {
|
||||
return 'Missing cart ID';
|
||||
}
|
||||
|
||||
try {
|
||||
await setCartAttributes(cartId, [
|
||||
{
|
||||
key: 'customer_vin',
|
||||
value: formData.customer_vin
|
||||
},
|
||||
{
|
||||
key: 'customer_mileage',
|
||||
value: formData.customer_mileage
|
||||
}
|
||||
]);
|
||||
|
||||
revalidateTag(TAGS.cart);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return 'Error set cart attributes';
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeItem(prevState: any, lineIds: string[]) {
|
||||
const cartId = cookies().get('cartId')?.value;
|
||||
|
||||
|
@@ -2,18 +2,31 @@
|
||||
|
||||
import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react';
|
||||
import { ShoppingCartIcon } from '@heroicons/react/24/outline';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import clsx from 'clsx';
|
||||
import LoadingDots from 'components/loading-dots';
|
||||
import Price from 'components/price';
|
||||
import type { Cart } from 'lib/shopify/types';
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { setMetafields } from './actions';
|
||||
import CloseCart from './close-cart';
|
||||
import LineItem from './line-item';
|
||||
import OpenCart from './open-cart';
|
||||
import VehicleDetails, { VehicleFormSchema, vehicleFormSchema } from './vehicle-details';
|
||||
|
||||
export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const quantityRef = useRef(cart?.totalQuantity);
|
||||
const openCart = () => setIsOpen(true);
|
||||
const closeCart = () => setIsOpen(false);
|
||||
const { control, handleSubmit } = useForm<VehicleFormSchema>({
|
||||
resolver: zodResolver(vehicleFormSchema)
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState<string | undefined>();
|
||||
const linkRef = useRef<HTMLAnchorElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Open cart modal when quantity changes.
|
||||
@@ -28,6 +41,25 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
||||
}
|
||||
}, [isOpen, cart?.totalQuantity, quantityRef]);
|
||||
|
||||
const onSubmit = async (data: VehicleFormSchema) => {
|
||||
if (!cart) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const message = await setMetafields(cart.id, data);
|
||||
if (message) {
|
||||
setMessage(message);
|
||||
} else {
|
||||
linkRef.current?.click();
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage('Error updating vehicle details');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button aria-label="Open cart" onClick={openCart}>
|
||||
@@ -76,34 +108,50 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
||||
return <LineItem item={item} closeCart={closeCart} key={item.id} />;
|
||||
})}
|
||||
</ul>
|
||||
<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.cost.totalTaxAmount.amount}
|
||||
currencyCode={cart.cost.totalTaxAmount.currencyCode}
|
||||
/>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="py-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
<VehicleDetails control={control} />
|
||||
<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.cost.totalTaxAmount.amount}
|
||||
currencyCode={cart.cost.totalTaxAmount.currencyCode}
|
||||
/>
|
||||
</div>
|
||||
<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 className="text-right">Calculated at checkout</p>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700">
|
||||
<p>Total</p>
|
||||
<Price
|
||||
className="text-right text-base text-black dark:text-white"
|
||||
amount={cart.cost.totalAmount.amount}
|
||||
currencyCode={cart.cost.totalAmount.currencyCode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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 className="text-right">Calculated at checkout</p>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700">
|
||||
<p>Total</p>
|
||||
<Price
|
||||
className="text-right text-base text-black dark:text-white"
|
||||
amount={cart.cost.totalAmount.amount}
|
||||
currencyCode={cart.cost.totalAmount.currencyCode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={cart.checkoutUrl}
|
||||
className="block w-full rounded-full bg-secondary p-3 text-center text-sm font-medium text-white opacity-90 hover:opacity-100"
|
||||
>
|
||||
Proceed to Checkout
|
||||
</a>
|
||||
<a href={cart.checkoutUrl} ref={linkRef} className="hidden">
|
||||
Proceed to Checkout
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(
|
||||
'flex w-full flex-row items-center justify-center gap-2 rounded-full bg-secondary p-3 text-sm font-medium text-white',
|
||||
{ 'cursor-not-allowed opacity-60 hover:opacity-60': loading },
|
||||
{ 'cursor-pointer opacity-90 hover:opacity-100': !loading }
|
||||
)}
|
||||
aria-disabled={loading}
|
||||
>
|
||||
{loading && <LoadingDots className="bg-white" />}
|
||||
Proceed to Checkout
|
||||
</button>
|
||||
|
||||
<p aria-live="polite" className="sr-only" role="status">
|
||||
{message}
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</DialogPanel>
|
||||
|
60
components/cart/vehicle-details.tsx
Normal file
60
components/cart/vehicle-details.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Description, Field, Input, Label } from '@headlessui/react';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import * as zod from 'zod';
|
||||
|
||||
export const vehicleFormSchema = zod.object({
|
||||
customer_vin: zod.string({ required_error: 'Vin number is required' }).min(0),
|
||||
customer_mileage: zod.string({ required_error: 'Mileage is required' }).min(0)
|
||||
});
|
||||
|
||||
export type VehicleFormSchema = zod.infer<typeof vehicleFormSchema>;
|
||||
|
||||
type VehicleDetailsProps = {
|
||||
control: Control<VehicleFormSchema>;
|
||||
};
|
||||
|
||||
const VehicleDetails = ({ control }: VehicleDetailsProps) => {
|
||||
return (
|
||||
<div className="mb-5 mt-3 border-y border-gray-300 pb-5 pt-3">
|
||||
<div className="text-base font-medium text-gray-900">Vehicle Details</div>
|
||||
<Controller
|
||||
name="customer_vin"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<Field className="mt-4">
|
||||
<Label className="block text-sm font-medium text-gray-700">Vin Number</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="mt-1 block w-full rounded-md border-gray-300 text-dark shadow-sm focus:outline-none data-[focus]:border-primary/50 data-[focus]:ring-primary/50 sm:text-sm"
|
||||
autoFocus
|
||||
{...field}
|
||||
/>
|
||||
{error && (
|
||||
<Description className="mt-1 text-sm text-red-500">{error.message}</Description>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="customer_mileage"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<Field className="mt-4">
|
||||
<Label className="block text-sm font-medium text-gray-700">Current Mileage</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="mt-1 block w-full rounded-md border-gray-300 text-dark shadow-sm focus:outline-none data-[focus]:border-primary/50 data-[focus]:ring-primary/50 sm:text-sm"
|
||||
{...field}
|
||||
/>
|
||||
{error && (
|
||||
<Description className="mt-1 text-sm text-red-500">{error.message}</Description>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VehicleDetails;
|
Reference in New Issue
Block a user