mirror of
https://github.com/vercel/commerce.git
synced 2025-05-02 15:57:52 +00:00
connected cart on checkout to sfcc
This commit is contained in:
parent
12ca470288
commit
7c4b3b0e0d
@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { CheckoutCart } from '@/components/checkout/checkout-cart';
|
||||
import { LoadingCart } from '@/components/checkout/loading-cart';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@ -10,66 +11,9 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
|
||||
const US_STATES = [
|
||||
{ value: 'AL', label: 'Alabama' },
|
||||
{ value: 'AK', label: 'Alaska' },
|
||||
{ value: 'AZ', label: 'Arizona' },
|
||||
{ value: 'AR', label: 'Arkansas' },
|
||||
{ value: 'CA', label: 'California' },
|
||||
{ value: 'CO', label: 'Colorado' },
|
||||
{ value: 'CT', label: 'Connecticut' },
|
||||
{ value: 'DE', label: 'Delaware' },
|
||||
{ value: 'FL', label: 'Florida' },
|
||||
{ value: 'GA', label: 'Georgia' },
|
||||
{ value: 'HI', label: 'Hawaii' },
|
||||
{ value: 'ID', label: 'Idaho' },
|
||||
{ value: 'IL', label: 'Illinois' },
|
||||
{ value: 'IN', label: 'Indiana' },
|
||||
{ value: 'IA', label: 'Iowa' },
|
||||
{ value: 'KS', label: 'Kansas' },
|
||||
{ value: 'KY', label: 'Kentucky' },
|
||||
{ value: 'LA', label: 'Louisiana' },
|
||||
{ value: 'ME', label: 'Maine' },
|
||||
{ value: 'MD', label: 'Maryland' },
|
||||
{ value: 'MA', label: 'Massachusetts' },
|
||||
{ value: 'MI', label: 'Michigan' },
|
||||
{ value: 'MN', label: 'Minnesota' },
|
||||
{ value: 'MS', label: 'Mississippi' },
|
||||
{ value: 'MO', label: 'Missouri' },
|
||||
{ value: 'MT', label: 'Montana' },
|
||||
{ value: 'NE', label: 'Nebraska' },
|
||||
{ value: 'NV', label: 'Nevada' },
|
||||
{ value: 'NH', label: 'New Hampshire' },
|
||||
{ value: 'NJ', label: 'New Jersey' },
|
||||
{ value: 'NM', label: 'New Mexico' },
|
||||
{ value: 'NY', label: 'New York' },
|
||||
{ value: 'NC', label: 'North Carolina' },
|
||||
{ value: 'ND', label: 'North Dakota' },
|
||||
{ value: 'OH', label: 'Ohio' },
|
||||
{ value: 'OK', label: 'Oklahoma' },
|
||||
{ value: 'OR', label: 'Oregon' },
|
||||
{ value: 'PA', label: 'Pennsylvania' },
|
||||
{ value: 'RI', label: 'Rhode Island' },
|
||||
{ value: 'SC', label: 'South Carolina' },
|
||||
{ value: 'SD', label: 'South Dakota' },
|
||||
{ value: 'TN', label: 'Tennessee' },
|
||||
{ value: 'TX', label: 'Texas' },
|
||||
{ value: 'UT', label: 'Utah' },
|
||||
{ value: 'VT', label: 'Vermont' },
|
||||
{ value: 'VA', label: 'Virginia' },
|
||||
{ value: 'WA', label: 'Washington' },
|
||||
{ value: 'WV', label: 'West Virginia' },
|
||||
{ value: 'WI', label: 'Wisconsin' },
|
||||
{ value: 'WY', label: 'Wyoming' }
|
||||
];
|
||||
import { Suspense } from 'react';
|
||||
|
||||
export default function CheckoutPage() {
|
||||
const [selectedCountry, setSelectedCountry] = useState('us');
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 md:p-8">
|
||||
<h1 className="mb-8 text-2xl font-bold">Checkout</h1>
|
||||
@ -82,7 +26,7 @@ export default function CheckoutPage() {
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" placeholder="you@example.com" />
|
||||
<Input id="email" type="email" placeholder="jdoe@acme.com" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -109,37 +53,26 @@ export default function CheckoutPage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="apartment">Apartment, suite, etc. (optional)</Label>
|
||||
<Input id="apartment" placeholder="Apt 4B" />
|
||||
<Input id="apartment" placeholder="" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="city">City</Label>
|
||||
<Input id="city" placeholder="New York" />
|
||||
<Input id="city" placeholder="Chicago" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="state">State</Label>
|
||||
<Select disabled={selectedCountry !== 'us'}>
|
||||
<SelectTrigger id="state">
|
||||
<SelectValue placeholder="Select state" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{US_STATES.map((state) => (
|
||||
<SelectItem key={state.value} value={state.value}>
|
||||
{state.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input id="state" placeholder="Illinois" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="zipCode">ZIP Code</Label>
|
||||
<Input id="zipCode" placeholder="10001" />
|
||||
<Input id="zipCode" placeholder="60606" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="country">Country</Label>
|
||||
<Select onValueChange={setSelectedCountry} defaultValue={selectedCountry}>
|
||||
<Select>
|
||||
<SelectTrigger id="country">
|
||||
<SelectValue placeholder="Select country" />
|
||||
</SelectTrigger>
|
||||
@ -155,7 +88,9 @@ export default function CheckoutPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button className="w-full">Continue to Shipping</Button>
|
||||
<Button disabled className="w-full">
|
||||
Continue to Shipping
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -164,35 +99,9 @@ export default function CheckoutPage() {
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Image
|
||||
src="/placeholder.svg"
|
||||
alt="Product Image"
|
||||
width={80}
|
||||
height={80}
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="font-semibold">Product Name</h3>
|
||||
<p className="text-sm text-gray-500">Product description</p>
|
||||
</div>
|
||||
<div className="ml-auto font-semibold">$99.99</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between">
|
||||
<span>Subtotal</span>
|
||||
<span>$99.99</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Shipping</span>
|
||||
<span>$9.99</span>
|
||||
</div>
|
||||
<div className="flex justify-between font-bold">
|
||||
<span>Total</span>
|
||||
<span>$109.98</span>
|
||||
</div>
|
||||
</div>
|
||||
<Suspense fallback={<LoadingCart />}>
|
||||
<CheckoutCart />
|
||||
</Suspense>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -8,6 +8,7 @@ import { DEFAULT_OPTION } from 'lib/constants';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { useFormStatus } from 'react-dom';
|
||||
import { createCartAndSetCookie, redirectToCheckout } from './actions';
|
||||
@ -27,6 +28,7 @@ export default function CartModal() {
|
||||
const quantityRef = useRef(cart?.totalQuantity);
|
||||
const openCart = () => setIsOpen(true);
|
||||
const closeCart = () => setIsOpen(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
if (!cart) {
|
||||
@ -47,6 +49,10 @@ export default function CartModal() {
|
||||
}
|
||||
}, [isOpen, cart?.totalQuantity, quantityRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname === '/checkout') closeCart();
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button aria-label="Open cart" onClick={openCart}>
|
||||
|
93
components/checkout/checkout-cart.tsx
Normal file
93
components/checkout/checkout-cart.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { getCart } from '@/lib/sfcc';
|
||||
import { CartItem } from '@/lib/sfcc/types';
|
||||
import { ShoppingCart } from 'lucide-react';
|
||||
import { cookies } from 'next/headers';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import Price from '../price';
|
||||
import { buttonVariants } from '../ui/button';
|
||||
import { Separator } from '../ui/separator';
|
||||
|
||||
export async function CheckoutCart() {
|
||||
const cartId = cookies().get('cartId')?.value;
|
||||
const cart = await getCart(cartId);
|
||||
|
||||
if (!cart || cart.lines.length === 0) {
|
||||
return <EmptyCart />;
|
||||
}
|
||||
|
||||
const { cost } = cart;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{cart.lines.map((line) => (
|
||||
<Line key={line.id} line={line} />
|
||||
))}
|
||||
<Separator />
|
||||
<div className="flex justify-between">
|
||||
<span>Taxes</span>
|
||||
<Price
|
||||
amount={cost.totalTaxAmount.amount}
|
||||
currencyCode={cost.totalTaxAmount.currencyCode}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Subtotal</span>
|
||||
<Price
|
||||
amount={cost.subtotalAmount.amount}
|
||||
currencyCode={cost.subtotalAmount.currencyCode}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Shipping</span>
|
||||
<span className="text-gray-400">Calulated during Shipping</span>
|
||||
</div>
|
||||
<div className="flex justify-between font-bold">
|
||||
<span>Total</span>
|
||||
<Price amount={cost.totalAmount.amount} currencyCode={cost.totalAmount.currencyCode} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyCart() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-6 py-4">
|
||||
<ShoppingCart className="h-16 w-16 text-gray-400" />
|
||||
<p className="text-lg font-semibold text-gray-600">Your cart is empty</p>
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
Looks like you haven't added any items to your cart yet.
|
||||
</p>
|
||||
<Link href="/" prefetch className={buttonVariants({ variant: 'outline' })}>
|
||||
Continue Shopping
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Line({ line }: { line: CartItem }) {
|
||||
return (
|
||||
<div className="flex items-center space-x-4">
|
||||
<Image
|
||||
src={line.merchandise.product.featuredImage.url}
|
||||
alt={line.merchandise.product.featuredImage.altText}
|
||||
width={80}
|
||||
height={80}
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
<div className="flex-grow">
|
||||
<h3 className="font-semibold">{line.merchandise.title}</h3>
|
||||
{/* <p className="text-sm text-gray-500">{line.merchandise.product.description}</p> */}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-semibold">
|
||||
<Price
|
||||
amount={line.cost.totalAmount.amount}
|
||||
currencyCode={line.cost.totalAmount.currencyCode}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">Qty: {line.quantity}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
36
components/checkout/loading-cart.tsx
Normal file
36
components/checkout/loading-cart.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Separator } from '../ui/separator';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
|
||||
export function LoadingCart() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Skeleton className="h-20 w-20 rounded-md" />
|
||||
<div className="flex-grow space-y-2">
|
||||
<Skeleton className="h-6 w-[200px]" />
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<Skeleton className="ml-auto h-4 w-[50px]" />
|
||||
<Skeleton className="ml-auto mt-2 h-4 w-[30px]" />
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-6 w-[100px]" />
|
||||
<Skeleton className="h-6 w-[50px]" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-6 w-[80px]" />
|
||||
<Skeleton className="h-6 w-[100px]" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-6 w-[80px]" />
|
||||
<Skeleton className="h-6 w-[90px]" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-6 w-[60px]" />
|
||||
<Skeleton className="h-6 w-[110px]" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
15
components/ui/skeleton.tsx
Normal file
15
components/ui/skeleton.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-800", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
@ -111,6 +111,7 @@ export type CartProduct = {
|
||||
id: string;
|
||||
handle: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
featuredImage: Image;
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user