ifixkart-storefront/components/store/ProductCard.tsx

189 lines
7.4 KiB
TypeScript

/**
* @component ProductCard
* @purpose Renders native iFixKart product with multi-price variants, stock ledger status, quick view modal trigger, wishlist/compare toggles, and Add to Cart.
* @param {ProductResponse} product - Product data object
* @a11y ARIA labeled buttons, focus visible outline
* @dependencies cartStore, catalogService, lucide-react
*/
'use client';
import { useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { Star, Eye, Heart, ArrowLeftRight, ShoppingBag, Check } from 'lucide-react';
import { ProductResponse } from '@/services/api/catalogService';
import { useCartStore } from '@/store/cartStore';
import { formatCurrency, getImageUrl } from '@/lib/utils';
import BlurHashImage from '@/components/ui/BlurHashImage';
import { addToCompareAndNavigate } from '@/lib/compare';
interface ProductCardProps {
product: ProductResponse;
}
export function ProductCard({ product }: ProductCardProps) {
const [added, setAdded] = useState(false);
const addToCart = useCartStore((state) => state.addToCart);
const toggleWishlist = useCartStore((state) => state.toggleWishlist);
const isInWishlist = useCartStore((state) => state.isInWishlist(product.product_id));
const isInCompare = useCartStore((state) => state.isInCompare(product.product_id));
const setQuickViewProduct = useCartStore((state) => state.setQuickViewProduct);
const router = useRouter();
const primaryVariant = product.variants[0];
const primaryImage = getImageUrl(product.images[0]?.image_url);
const secondaryImage = getImageUrl(product.images[1]?.image_url) || primaryImage;
const price = Number(primaryVariant?.price || 0);
const comparePrice = primaryVariant?.compare_price != null ? Number(primaryVariant.compare_price) : null;
const available = primaryVariant?.available_stock;
const outOfStock = typeof available === 'number' && available <= 0;
const handleAddToCart = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!primaryVariant || outOfStock) return;
void addToCart(product, primaryVariant, 1);
setAdded(true);
setTimeout(() => setAdded(false), 2000);
};
return (
<div className="group bg-white border border-gray-200 rounded-xl overflow-hidden shadow-xs hover:shadow-xl hover:border-[#e4382f]/40 transition-all flex flex-col justify-between relative">
{/* Product Badges (HOT, NEW, SALE) */}
<div className="absolute top-3 left-3 z-10 flex flex-col gap-1">
{product.badge && (
<span
className={`text-[9px] font-extrabold px-2 py-0.5 rounded-md uppercase tracking-wider text-white shadow-xs ${
product.badge === 'HOT'
? 'bg-[#e4382f]'
: product.badge === 'NEW'
? 'bg-[#01b574]'
: 'bg-amber-500'
}`}
>
{product.badge}
</span>
)}
{comparePrice && comparePrice > price && (
<span className="bg-red-600 text-white text-[9px] font-extrabold px-2 py-0.5 rounded-md shadow-xs">
-{Math.round(((comparePrice - price) / comparePrice) * 100)}%
</span>
)}
</div>
{/* Floating Action Overlay Icons (Wishlist, Quick View, Compare) */}
<div className="absolute top-3 right-3 z-10 flex flex-col gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<button
onClick={(e) => { e.preventDefault(); toggleWishlist(product); }}
aria-label="Add to Wishlist"
className={`p-2 rounded-full shadow-md transition-colors ${
isInWishlist ? 'bg-[#e4382f] text-white' : 'bg-white text-gray-700 hover:bg-[#e4382f] hover:text-white'
}`}
>
<Heart className="w-3.5 h-3.5 fill-current" />
</button>
<button
onClick={(e) => { e.preventDefault(); setQuickViewProduct(product); }}
aria-label="Quick View"
className="p-2 bg-white text-gray-700 hover:bg-[#e4382f] hover:text-white rounded-full shadow-md transition-colors"
>
<Eye className="w-3.5 h-3.5" />
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
void addToCompareAndNavigate(product, (href) => router.push(href));
}}
aria-label="Compare Product"
className={`p-2 rounded-full shadow-md transition-colors ${
isInCompare ? 'bg-[#1b2559] text-white' : 'bg-white text-gray-700 hover:bg-[#1b2559] hover:text-white'
}`}
>
<ArrowLeftRight className="w-3.5 h-3.5" />
</button>
</div>
{/* Image Gallery Flip Container */}
<Link href={`/products/${product.slug}`} className="block relative w-full pt-[85%] bg-gray-50 overflow-hidden">
<BlurHashImage
src={primaryImage}
blurHash={(product as any).blur_hash || null}
alt={product.name}
fill
sizes="(max-width: 768px) 100vw, 300px"
className="object-contain p-3 group-hover:scale-105 transition-transform duration-300"
/>
</Link>
{/* Product Details Section */}
<div className="p-4 flex-1 flex flex-col justify-between">
<div>
{/* Star Rating */}
<div className="flex items-center gap-1 mb-1.5 text-amber-400 text-xs">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className={`w-3 h-3 ${
i < Math.floor(product.rating || 5) ? 'fill-amber-400' : 'text-gray-200'
}`}
/>
))}
<span className="text-[10px] text-gray-400 font-medium ml-1">
({product.review_count || 12})
</span>
</div>
{/* Title */}
<Link
href={`/products/${product.slug}`}
className="text-xs font-bold text-[#1b2559] hover:text-[#e4382f] transition-colors line-clamp-2 leading-snug mb-2"
>
{product.name}
</Link>
</div>
<div>
{/* Pricing */}
<div className="flex items-baseline gap-2 mb-3">
<span className="text-sm font-extrabold text-[#e4382f]">
{formatCurrency(price)}
</span>
{comparePrice && (
<span className="text-xs text-gray-400 line-through">
{formatCurrency(comparePrice)}
</span>
)}
</div>
{/* Add to Cart CTA Button */}
<button
onClick={handleAddToCart}
disabled={outOfStock}
className={`w-full py-2 px-3 rounded-lg font-bold text-xs flex items-center justify-center gap-1.5 transition-colors cursor-pointer shadow-xs ${
outOfStock
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
: added
? 'bg-[#01b574] text-white'
: 'bg-[#1b2559] hover:bg-[#e4382f] text-white'
}`}
>
{outOfStock ? (
'Out of Stock'
) : added ? (
<>
<Check className="w-3.5 h-3.5" /> Added to Cart
</>
) : (
<>
<ShoppingBag className="w-3.5 h-3.5" /> Add to Cart
</>
)}
</button>
</div>
</div>
</div>
);
}