ecom / client /src /components /product /product-card.tsx
shashwatIDR's picture
Upload 106 files
1684141 verified
import { Link, useLocation } from "wouter";
import { useCart } from "@/hooks/use-cart";
import { useAuth } from "@/hooks/use-auth";
import { Product } from "@/types";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Heart } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { ShoppingCart, Star, Store } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
interface ProductCardProps {
product: Product;
}
export default function ProductCard({ product }: ProductCardProps) {
const { addItem } = useCart();
const { toast } = useToast();
const { isAuthenticated } = useAuth();
const [, setLocation] = useLocation();
const handleAddToCart = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!isAuthenticated) {
toast({
title: "Login Required",
description: "Please sign in to add items to your cart.",
action: (
<Button
variant="outline"
size="sm"
onClick={() => setLocation('/auth')}
>
Sign In
</Button>
),
});
return;
}
try {
await addItem(product.id, 1);
toast({
title: "Added to cart",
description: `${product.title} has been added to your cart.`,
});
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to add item to cart. Please try again.",
});
}
};
const discountPercentage = product.originalPrice
? Math.round((1 - parseFloat(product.price) / parseFloat(product.originalPrice)) * 100)
: 0;
return (
<Link href={`/product/${product.id}`}>
<Card className="group cursor-pointer bg-gradient-to-b from-white to-orange-50/20 rounded-2xl shadow-sm hover:shadow-xl transition-all duration-300 border-0 overflow-hidden h-full hover:scale-[1.02] transform" data-testid={`product-card-${product.id}`}>
{/* Product Image */}
<div className="aspect-[4/5] bg-gradient-to-br from-orange-50/50 to-amber-50/60 overflow-hidden relative rounded-xl m-3 mb-4">
{/* Wishlist Heart */}
<button className="absolute top-3 right-3 z-10 p-2 bg-white/80 backdrop-blur-sm rounded-full shadow-sm hover:bg-white transition-all duration-200 hover:scale-110" onClick={(e) => { e.preventDefault(); e.stopPropagation(); }} data-testid={`wishlist-${product.id}`}>
<Heart className="w-4 h-4 text-gray-400 hover:text-red-500 transition-colors" />
</button>
{product.images && product.images.length > 0 ? (
<img
src={product.images[0]}
alt={product.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
data-testid={`product-image-${product.id}`}
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-orange-50 to-red-50 flex items-center justify-center">
<span className="text-gray-400 text-sm">No Image</span>
</div>
)}
{/* Discount Badge */}
{discountPercentage > 0 && (
<Badge
className="absolute top-3 left-3 bg-gradient-to-r from-red-500 to-pink-500 text-white border-0 rounded-full px-2 py-1 text-xs font-medium"
data-testid={`discount-badge-${product.id}`}
>
-{discountPercentage}%
</Badge>
)}
</div>
<CardContent className="p-4 pt-0 flex flex-col flex-grow">
{/* Seller Info */}
{(product as any).seller && (
<div className="flex items-center gap-2 mb-2">
<Store className="w-3 h-3 text-gray-400" />
<span className="text-xs text-gray-500 truncate">{(product as any).seller.username}</span>
</div>
)}
{/* Product Title */}
<h3 className="font-semibold text-gray-800 mb-2 line-clamp-2 text-sm leading-relaxed" data-testid={`product-title-${product.id}`}>
{product.title}
</h3>
{/* Rating */}
<div className="flex items-center gap-1 mb-3">
<div className="flex items-center">
<Star className="w-3 h-3 fill-yellow-400 text-yellow-400" />
<Star className="w-3 h-3 fill-yellow-400 text-yellow-400" />
<Star className="w-3 h-3 fill-yellow-400 text-yellow-400" />
<Star className="w-3 h-3 fill-yellow-400 text-yellow-400" />
<Star className="w-3 h-3 fill-gray-200 text-gray-200" />
</div>
<span className="text-xs text-gray-500 ml-1">(4.0)</span>
</div>
{/* Price */}
<div className="mb-3 mt-auto">
<div className="flex items-baseline space-x-2">
<span className="text-lg font-bold text-gray-900" data-testid={`product-price-${product.id}`}>
₹{parseFloat(product.price).toFixed(2)}
</span>
{product.originalPrice && (
<span
className="text-xs text-gray-400 line-through"
data-testid={`original-price-${product.id}`}
>
₹{parseFloat(product.originalPrice).toFixed(2)}
</span>
)}
</div>
{discountPercentage > 0 && (
<p className="text-xs text-green-600 font-medium">Save ₹{(parseFloat(product.originalPrice!) - parseFloat(product.price)).toFixed(2)}</p>
)}
</div>
{/* Quick Actions */}
<div className="flex gap-2">
<Button
size="sm"
onClick={handleAddToCart}
className="flex-1 bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white border-0 rounded-xl py-2 font-medium text-xs shadow-sm hover:shadow-md transition-all duration-300 hover:scale-105"
data-testid={`add-to-cart-${product.id}`}
>
<ShoppingCart className="w-3 h-3 mr-1" />
Add to Cart
</Button>
</div>
{/* Trust Elements */}
<div className="flex items-center justify-between mt-2 text-xs text-gray-500">
<span className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
In Stock
</span>
<span>Free Shipping</span>
</div>
{/* Stock Info */}
{product.stock <= 5 && product.stock > 0 && (
<p className="text-xs text-orange-500 mt-2 font-medium" data-testid={`stock-warning-${product.id}`}>
Only {product.stock} left in stock
</p>
)}
{product.stock === 0 && (
<p className="text-xs text-red-500 mt-2 font-medium" data-testid={`out-of-stock-${product.id}`}>
Out of stock
</p>
)}
</CardContent>
</Card>
</Link>
);
}