Rocky_Mountain_Vending/components/cart.tsx

269 lines
9.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState } from "react"
import {
Minus,
Plus,
ShoppingCart,
Trash2,
AlertCircle,
CheckCircle,
Loader2,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetFooter,
} from "@/components/ui/sheet"
import { Separator } from "@/components/ui/separator"
import { useCart } from "@/lib/cart/context"
import { Alert, AlertDescription } from "@/components/ui/alert"
import Image from "next/image"
interface CartProps {
isOpen: boolean
onClose: () => void
}
export function Cart({ isOpen, onClose }: CartProps) {
const { items, removeItem, updateQuantity, getTotal, clearCart } = useCart()
const [isCheckingOut, setIsCheckingOut] = useState(false)
const [checkoutError, setCheckoutError] = useState<string | null>(null)
const [isRemoving, setIsRemoving] = useState<string | null>(null)
const handleCheckout = async () => {
if (items.length === 0) return
setIsCheckingOut(true)
setCheckoutError(null)
try {
const response = await fetch("/api/stripe/checkout", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
items: items.map((item) => ({
priceId: item.priceId,
quantity: item.quantity,
})),
}),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || "Failed to create checkout session")
}
if (data.url) {
// Redirect to Stripe Checkout
window.location.href = data.url
} else {
throw new Error("No checkout URL returned from server")
}
} catch (error) {
console.error("Error creating checkout session:", error)
const errorMessage =
error instanceof Error
? error.message
: "Failed to start checkout. Please try again."
setCheckoutError(errorMessage)
// Auto-hide error after 5 seconds
setTimeout(() => setCheckoutError(null), 5000)
} finally {
setIsCheckingOut(false)
}
}
const handleRemoveItem = async (itemId: string) => {
setIsRemoving(itemId)
try {
// Simulate API call delay for better UX
await new Promise((resolve) => setTimeout(resolve, 300))
removeItem(itemId)
} catch (error) {
console.error("Error removing item:", error)
} finally {
setIsRemoving(null)
}
}
return (
<Sheet open={isOpen} onOpenChange={onClose}>
<SheetContent side="right" className="w-full max-w-md flex flex-col p-0">
<SheetHeader className="p-4 border-b border-border">
<SheetTitle>Shopping Cart</SheetTitle>
<div className="flex items-center gap-2 mt-1">
<CheckCircle className="h-4 w-4 text-green-500" />
<span className="text-sm text-muted-foreground">
{items.length} {items.length === 1 ? "item" : "items"} in cart
</span>
</div>
</SheetHeader>
{/* Error Alert */}
{checkoutError && (
<div className="p-4 border-b border-border">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{checkoutError}</AlertDescription>
</Alert>
</div>
)}
{/* Cart Items */}
<div className="flex-1 overflow-y-auto p-4">
{items.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center">
<ShoppingCart className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium mb-2">Your cart is empty</h3>
<p className="text-sm text-muted-foreground">
Add some products from our catalog to get started
</p>
<Button
className="mt-4"
onClick={() => {
onClose()
// Navigate to products page
window.location.href = "/products"
}}
>
Browse Products
</Button>
</div>
) : (
<div className="space-y-4">
{items.map((item) => (
<div
key={item.id}
className="flex gap-4 p-4 border border-border rounded-lg hover:border-secondary/50 transition-colors"
>
<div className="relative w-20 h-20 flex-shrink-0 overflow-hidden rounded-md bg-muted">
<Image
src={item.image}
alt={item.name}
fill
className="object-cover transition-transform duration-300 hover:scale-105"
sizes="80px"
/>
</div>
<div className="flex-1 min-w-0">
<h3 className="font-medium truncate mb-1">{item.name}</h3>
<p className="text-sm text-muted-foreground mb-2">
${item.price.toFixed(2)} × {item.quantity}
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
className="h-7 w-7"
onClick={() =>
updateQuantity(item.id, item.quantity - 1)
}
disabled={item.quantity <= 1}
title="Decrease quantity"
>
<Minus className="h-3 w-3" />
</Button>
<span className="w-8 text-center font-medium">
{item.quantity}
</span>
<Button
variant="outline"
size="icon"
className="h-7 w-7"
onClick={() =>
updateQuantity(item.id, item.quantity + 1)
}
title="Increase quantity"
>
<Plus className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 ml-auto text-destructive hover:text-destructive/80"
onClick={() => handleRemoveItem(item.id)}
disabled={isRemoving === item.id}
title="Remove item"
>
{isRemoving === item.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
</Button>
</div>
</div>
<div className="flex flex-col justify-end items-end">
<div className="text-lg font-semibold">
${(item.price * item.quantity).toFixed(2)}
</div>
<div className="text-xs text-muted-foreground">
${item.price.toFixed(2)} each
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Footer */}
{items.length > 0 && (
<SheetFooter className="flex-col gap-4 p-4 border-t border-border">
<div className="flex justify-between items-end">
<div>
<div className="text-sm text-muted-foreground">Subtotal</div>
<div className="flex items-baseline gap-1">
<span className="text-2xl font-bold">
${getTotal().toFixed(2)}
</span>
<span className="text-sm text-muted-foreground">USD</span>
</div>
</div>
<div className="text-right text-sm text-muted-foreground">
{items.reduce((total, item) => total + item.quantity, 0)} items
</div>
</div>
<Separator />
<Button
onClick={handleCheckout}
disabled={isCheckingOut}
variant="brand"
size="lg"
className="w-full h-12"
>
{isCheckingOut ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Processing...
</>
) : (
<>
<ShoppingCart className="h-4 w-4 mr-2" />
Checkout - ${getTotal().toFixed(2)}
</>
)}
</Button>
<Button
variant="outline"
onClick={clearCart}
className="w-full"
size="sm"
disabled={isCheckingOut}
>
Clear Cart
</Button>
</SheetFooter>
)}
</SheetContent>
</Sheet>
)
}