ecom / client /src /pages /cart.tsx
shashwatIDR's picture
Upload 106 files
1684141 verified
import { useLocation } from "wouter";
import { useAuth } from "@/hooks/use-auth";
import { useCart } from "@/hooks/use-cart";
import Header from "@/components/layout/header";
import { ShoppingCart, Plus, Minus, Trash2, ArrowLeft } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { useState } from "react";
export default function Cart() {
const [, setLocation] = useLocation();
const { user, isLoading } = useAuth();
const [searchQuery, setSearchQuery] = useState("");
const { items, itemCount, subtotal, tax, total, updateQuantity, removeItem } = useCart();
const { toast } = useToast();
// Show loading while authentication is being verified
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-gray-600">Loading...</p>
</div>
</div>
);
}
// Redirect if not authenticated after loading completes
if (!user) {
setLocation('/auth');
return null;
}
const handleUpdateQuantity = async (id: string, newQuantity: number) => {
if (newQuantity < 1) return;
try {
await updateQuantity(id, newQuantity);
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to update quantity.",
});
}
};
const handleRemoveItem = async (id: string) => {
try {
await removeItem(id);
toast({
title: "Item removed",
description: "Item has been removed from your cart.",
});
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to remove item.",
});
}
};
const handleCheckout = () => {
setLocation('/checkout');
};
return (
<div className="min-h-screen bg-gray-50">
<Header searchQuery={searchQuery} setSearchQuery={setSearchQuery} />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8" data-testid="cart-page">
{/* Back Button */}
<div className="mb-6">
<button
onClick={() => setLocation('/')}
className="flex items-center text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md transition-colors"
data-testid="back-to-home"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Continue Shopping
</button>
</div>
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900" data-testid="cart-title">
Shopping Cart
</h1>
<p className="text-gray-600 mt-2">
{itemCount > 0 ? `${itemCount} item${itemCount > 1 ? 's' : ''} in your cart` : 'Your cart is empty'}
</p>
</div>
{/* Cart Content */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Cart Items */}
<div className="lg:col-span-2">
<section className="bg-white rounded-lg shadow-sm border border-gray-200">
<header className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900 flex items-center">
<ShoppingCart className="h-5 w-5 mr-2" />
Cart Items
</h2>
</header>
<div className="p-6">
{items.length === 0 ? (
<div className="text-center py-12" data-testid="empty-cart">
<ShoppingCart className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">Your cart is empty</h3>
<p className="text-gray-500 mb-6">Add some items to get started!</p>
<button
onClick={() => setLocation('/')}
className="inline-flex items-center px-4 py-2 bg-primary text-primary-foreground font-medium rounded-md hover:bg-primary/90 transition-all"
data-testid="start-shopping"
>
Start Shopping
</button>
</div>
) : (
<div className="space-y-4" data-testid="cart-items">
{items.map((item) => (
<article
key={item.id}
className="flex flex-col sm:flex-row sm:items-center gap-4 p-4 border border-gray-200 rounded-lg hover:shadow-md transition-shadow"
data-testid={`cart-item-${item.id}`}
>
{/* Product Image */}
<div className="w-20 h-20 sm:w-24 sm:h-24 bg-gray-100 rounded-lg overflow-hidden flex-shrink-0 mx-auto sm:mx-0">
{item.product?.images && item.product.images.length > 0 ? (
<img
src={item.product.images[0]}
alt={item.product.title}
className="w-full h-full object-cover"
data-testid={`cart-item-image-${item.id}`}
/>
) : (
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
<span className="text-xs text-gray-400">No Image</span>
</div>
)}
</div>
{/* Product Details */}
<div className="flex-1 space-y-2">
<h3 className="font-medium text-gray-900 text-lg" data-testid={`cart-item-title-${item.id}`}>
{item.product?.title || 'Unknown Product'}
</h3>
<p className="text-sm text-gray-500" data-testid={`cart-item-seller-${item.id}`}>
Sold by {item.product?.store?.name || item.product?.seller?.username || 'Unknown Seller'}
</p>
<p className="text-xl font-bold text-primary" data-testid={`cart-item-price-${item.id}`}>
₹{item.product ? parseFloat(item.product.price).toFixed(2) : '0.00'}
</p>
</div>
{/* Quantity Controls and Remove */}
<div className="flex flex-col sm:flex-row items-center gap-4">
{/* Quantity Controls */}
<div className="flex items-center space-x-3">
<button
className="flex items-center justify-center w-8 h-8 text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
onClick={() => handleUpdateQuantity(item.id, item.quantity - 1)}
disabled={item.quantity <= 1}
data-testid={`cart-decrease-${item.id}`}
>
<Minus className="h-4 w-4" />
</button>
<span className="text-lg font-medium w-12 text-center" data-testid={`cart-quantity-${item.id}`}>
{item.quantity}
</span>
<button
className="flex items-center justify-center w-8 h-8 text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
onClick={() => handleUpdateQuantity(item.id, item.quantity + 1)}
data-testid={`cart-increase-${item.id}`}
>
<Plus className="h-4 w-4" />
</button>
</div>
{/* Remove Button */}
<button
className="flex items-center justify-center px-4 py-2 text-sm font-medium text-red-500 bg-white border border-red-300 rounded-md hover:bg-red-50 hover:text-red-700 transition-colors"
onClick={() => handleRemoveItem(item.id)}
data-testid={`cart-remove-${item.id}`}
>
<Trash2 className="h-4 w-4 mr-2" />
Remove
</button>
</div>
</article>
))}
</div>
)}
</div>
</section>
</div>
{/* Order Summary */}
{items.length > 0 && (
<div className="lg:col-span-1">
<section className="bg-white rounded-lg shadow-sm border border-gray-200 sticky top-8">
<header className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900">Order Summary</h2>
</header>
<div className="p-6">
<div className="space-y-3 mb-6" data-testid="cart-summary">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Subtotal ({itemCount} items):</span>
<span className="text-gray-900" data-testid="cart-subtotal">
₹{subtotal.toFixed(2)}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Shipping:</span>
<span className="text-green-600" data-testid="cart-shipping">Free</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Tax:</span>
<span className="text-gray-900" data-testid="cart-tax">
₹{tax.toFixed(2)}
</span>
</div>
<div className="border-t border-gray-200 pt-3">
<div className="flex justify-between text-lg font-bold">
<span className="text-gray-900">Total:</span>
<span className="text-gray-900" data-testid="cart-total">
₹{total.toFixed(2)}
</span>
</div>
</div>
</div>
<div className="space-y-3">
<button
className="w-full px-4 py-3 bg-primary text-primary-foreground font-medium rounded-md hover:bg-primary/90 transition-all"
onClick={handleCheckout}
data-testid="checkout-button"
>
Proceed to Checkout
</button>
<button
className="w-full px-4 py-2 text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
onClick={() => setLocation('/')}
data-testid="continue-shopping"
>
Continue Shopping
</button>
</div>
</div>
</section>
</div>
)}
</div>
</div>
</div>
);
}