connected cart on checkout to sfcc

This commit is contained in:
Alex 2024-09-05 00:49:35 -05:00
parent 12ca470288
commit 7c4b3b0e0d
6 changed files with 166 additions and 106 deletions

View File

@ -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>

View File

@ -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}>

View 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>
);
}

View 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>
);
}

View 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 }

View File

@ -111,6 +111,7 @@ export type CartProduct = {
id: string;
handle: string;
title: string;
description?: string;
featuredImage: Image;
};