first commit
This commit is contained in:
281
app/(public)/cart/page.tsx
Normal file
281
app/(public)/cart/page.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { ArrowLeft, ShoppingCart, Trash2, Plus, Minus, Heart } from 'lucide-react'
|
||||
import { cartManager } from '@/lib/cart'
|
||||
import { toast } from 'sonner'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { motion } from 'framer-motion'
|
||||
import PageHero from '@/components/sections/PageHero'
|
||||
|
||||
interface CartItem {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
quantity: number
|
||||
image: string | null
|
||||
}
|
||||
|
||||
export default function CartPage() {
|
||||
const router = useRouter()
|
||||
const [cart, setCart] = useState<CartItem[]>([])
|
||||
const [updating, setUpdating] = useState<string | null>(null)
|
||||
|
||||
const loadCart = useCallback(() => {
|
||||
const cartItems = cartManager.getCart()
|
||||
setCart(cartItems || [])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadCart()
|
||||
|
||||
const handleCartUpdate = () => loadCart()
|
||||
window.addEventListener('cartUpdated', handleCartUpdate)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('cartUpdated', handleCartUpdate)
|
||||
}
|
||||
}, [loadCart])
|
||||
|
||||
const updateQuantity = async (productId: string, newQuantity: number) => {
|
||||
setUpdating(productId)
|
||||
cartManager.updateQuantity(productId, newQuantity)
|
||||
loadCart()
|
||||
setUpdating(null)
|
||||
}
|
||||
|
||||
const removeItem = (productId: string) => {
|
||||
cartManager.removeFromCart(productId)
|
||||
toast.success('Item removed from cart')
|
||||
loadCart()
|
||||
}
|
||||
|
||||
const getTotalPrice = () => {
|
||||
return cartManager.getTotalPrice()
|
||||
}
|
||||
|
||||
const getTotalItems = () => {
|
||||
return cart.reduce((sum, item) => sum + item.quantity, 0)
|
||||
}
|
||||
|
||||
if (cart.length === 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-8">
|
||||
<Button variant="ghost" asChild className="mb-6 text-gray-600 hover:text-gray-900">
|
||||
<Link href="/products">
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Continue Shopping
|
||||
</Link>
|
||||
</Button>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">Shopping Cart</h1>
|
||||
<p className="text-gray-600">Your cart is currently empty</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center py-16">
|
||||
<div className="w-24 h-24 mx-auto mb-6 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<ShoppingCart className="h-12 w-12 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">Your cart is empty</h3>
|
||||
<p className="text-gray-600 mb-8 max-w-md mx-auto">
|
||||
Looks like you haven't added anything to your cart yet. Start shopping to fill it up!
|
||||
</p>
|
||||
<Button size="lg" asChild className="bg-black text-white hover:bg-gray-800">
|
||||
<Link href="/products">Start Shopping</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Button variant="ghost" asChild className="mb-6 text-gray-600 hover:text-gray-900">
|
||||
<Link href="/products">
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Continue Shopping
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">Shopping Cart</h1>
|
||||
<p className="text-gray-600">{getTotalItems()} {getTotalItems() === 1 ? 'item' : 'items'} in your cart</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-12">
|
||||
{/* Cart Items */}
|
||||
<div className="xl:col-span-2 space-y-6">
|
||||
{cart.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
className="group"
|
||||
>
|
||||
<div className="bg-white border border-gray-200 rounded-2xl p-6 hover:border-gray-300 transition-colors duration-200">
|
||||
<div className="flex items-start gap-6">
|
||||
{/* Product Image */}
|
||||
<div className="relative flex-shrink-0">
|
||||
<Image
|
||||
src={item.image || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
|
||||
alt={item.name}
|
||||
width={120}
|
||||
height={120}
|
||||
className="rounded-xl object-cover bg-gray-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Product Details */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-1 line-clamp-2">
|
||||
{item.name}
|
||||
</h3>
|
||||
<p className="text-2xl font-bold text-gray-900">
|
||||
₹{(item.price || 0).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-gray-400 hover:text-gray-600 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Heart className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeItem(item.id)}
|
||||
className="text-gray-400 hover:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quantity Controls */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center bg-gray-100 rounded-full">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => updateQuantity(item.id, item.quantity - 1)}
|
||||
disabled={item.quantity <= 1 || updating === item.id}
|
||||
className="h-10 w-10 rounded-full hover:bg-gray-200"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<span className="w-12 text-center font-semibold text-gray-900">
|
||||
{updating === item.id ? '...' : item.quantity}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => updateQuantity(item.id, item.quantity + 1)}
|
||||
disabled={updating === item.id}
|
||||
className="h-10 w-10 rounded-full hover:bg-gray-200"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-bold text-gray-900">
|
||||
₹{((item.price || 0) * item.quantity).toFixed(2)}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
₹{(item.price || 0).toFixed(2)} each
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Order Summary */}
|
||||
<div className="xl:col-span-1">
|
||||
<div className="sticky top-8">
|
||||
<div className="bg-gray-50 rounded-2xl p-8 border border-gray-200">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-6">Order Summary</h2>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span>Subtotal ({getTotalItems()} items)</span>
|
||||
<span className="font-semibold">₹{getTotalPrice().toFixed(2)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span>Shipping</span>
|
||||
<span className="font-semibold text-green-600">Free</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span>Tax</span>
|
||||
<span className="font-semibold">Calculated at checkout</span>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-lg font-bold text-gray-900">Total</span>
|
||||
<span className="text-2xl font-bold text-gray-900">₹{getTotalPrice().toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
className="w-full h-12 bg-black text-white hover:bg-gray-800 rounded-xl text-base font-semibold"
|
||||
asChild
|
||||
>
|
||||
<Link href="/checkout">
|
||||
Proceed to Checkout
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-12 border-gray-300 hover:bg-gray-50 rounded-xl text-base"
|
||||
asChild
|
||||
>
|
||||
<Link href="/products">
|
||||
Continue Shopping
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Security Badge */}
|
||||
<div className="mt-6 pt-6 border-t border-gray-200">
|
||||
<div className="flex items-center justify-center text-sm text-gray-500">
|
||||
<div className="w-4 h-4 bg-green-500 rounded-full mr-2"></div>
|
||||
Secure checkout powered by Razorpay
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user