189 lines
7.4 KiB
TypeScript
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>
|
|
);
|
|
}
|