This commit is contained in:
Your Name committed 2026-06-07 17:17:01 +07:00
1 parent d325cd8763
commit c6bf2f70ac
45 files changed
+5195 -338

No files matched your search

+99 -27
View File
@@ -1,36 +1,51 @@
import { Link } from 'react-router-dom'
import type { Product } from '../types'
import { getCategoryImage, formatPrice } from '../types'
import { Heart } from 'lucide-react'
import { useWishlistStore } from '../store/wishlistStore'
import { useNavigate } from 'react-router-dom';
import { useState } from 'react';
import type { Product } from '../types';
import { getCategoryImage, formatPrice } from '../types';
import { Heart, ShoppingBag } from 'lucide-react';
import { useWishlistStore } from '../store/wishlistStore';
import { useCartStore } from '../store/cartStore';
interface ProductCardProps {
product: Product
product: Product;
enableQuickShop?: boolean;
}
export default function ProductCard({ product }: ProductCardProps) {
const { addItem, removeItem, isInWishlist } = useWishlistStore()
const isWished = isInWishlist(product.id)
export default function ProductCard({ product, enableQuickShop = false }: ProductCardProps) {
const navigate = useNavigate();
const { addItem, removeItem, isInWishlist } = useWishlistStore();
const { addToCart } = useCartStore();
const isWished = isInWishlist(product.id);
const [selectedVariant, setSelectedVariant] = useState(product.variants?.[0] || null);
const handleWishlistClick = (e: React.MouseEvent) => {
e.preventDefault() // prevent navigating to product detail
e.preventDefault();
e.stopPropagation();
if (isWished) {
removeItem(product.id)
removeItem(product.id);
} else {
addItem(product)
addItem(product);
}
}
};
const handleCardClick = () => {
navigate(`/products/${product.id}`);
};
return (
<Link
to={`/products/${product.id}`}
className="bg-white shadow-sm border border-spa-stone/10 rounded-2xl overflow-hidden flex flex-col group p-2 transition-all duration-300 hover:shadow-lg hover:border-spa-forest/30"
<div
onClick={handleCardClick}
className="bg-white shadow-sm border border-spa-stone/10 rounded-2xl overflow-hidden flex flex-col group p-2 transition-all duration-300 hover:shadow-lg hover:border-spa-forest/30 cursor-pointer"
>
{/* Image Area */}
<div className="h-48 md:h-64 overflow-hidden rounded-xl relative bg-spa-sage/20">
<img
src={product.images?.[0]?.url || getCategoryImage(product.category.name)}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
alt={product.scent.name}
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
{/* Category badge */}
<div className="absolute top-3 left-3 bg-white/90 backdrop-blur-sm px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-widest text-spa-forest shadow-sm">
@@ -38,8 +53,9 @@ export default function ProductCard({ product }: ProductCardProps) {
</div>
{/* Wishlist Button */}
<button
type="button"
onClick={handleWishlistClick}
className="absolute top-3 right-3 p-2 bg-white/90 backdrop-blur-sm rounded-full shadow-sm hover:scale-110 transition-transform z-10"
className="absolute top-3 right-3 p-2 bg-white/90 backdrop-blur-sm rounded-full shadow-sm hover:scale-110 transition-transform z-10 cursor-pointer"
>
<Heart
className={`w-4 h-4 transition-colors ${isWished ? 'fill-rose-500 text-rose-500' : 'text-spa-stone/60 hover:text-rose-500'}`}
@@ -47,8 +63,9 @@ export default function ProductCard({ product }: ProductCardProps) {
</button>
</div>
{/* Content Area */}
<div className="p-4 flex flex-col flex-1">
<div className="mb-4">
<div className="mb-2">
<h3 className="text-base md:text-lg font-bold text-pop-black leading-tight line-clamp-1 group-hover:text-spa-forest transition-colors">
{product.scent.name}
</h3>
@@ -57,15 +74,70 @@ export default function ProductCard({ product }: ProductCardProps) {
</p>
</div>
<div className="mt-auto flex items-center justify-between">
<span className="text-rose-500 font-black text-sm md:text-lg">
{formatPrice(product.variants[0]?.price)}
</span>
<span className="text-[10px] md:text-xs font-bold text-spa-forest uppercase tracking-widest flex items-center gap-1 group-hover:translate-x-1 transition-transform">
View Details &rarr;
</span>
{/* Quick Shop Variant Swatches */}
{enableQuickShop && product.variants && product.variants.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2 mb-1">
{product.variants.map((variant) => {
const isSelected = selectedVariant?.id === variant.id;
return (
<button
key={variant.id}
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setSelectedVariant(variant);
}}
className={`px-3 py-1 rounded-full text-[10px] font-bold tracking-wider uppercase border transition-all cursor-pointer ${
isSelected
? 'bg-spa-forest border-spa-forest text-white shadow-sm'
: 'bg-white border-spa-stone/10 text-spa-stone hover:border-spa-forest/30'
}`}
>
{variant.size}
</button>
);
})}
</div>
)}
{/* Bottom Area */}
<div className="mt-auto pt-3 flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="text-rose-500 font-black text-sm md:text-lg">
{formatPrice(selectedVariant?.price ?? product.variants[0]?.price)}
</span>
{!enableQuickShop && (
<span className="text-[10px] md:text-xs font-bold text-spa-forest uppercase tracking-widest flex items-center gap-1 group-hover:translate-x-1 transition-transform">
View Details &rarr;
</span>
)}
</div>
{enableQuickShop && selectedVariant && (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
addToCart({
productId: product.id,
variantId: selectedVariant.id,
quantity: 1,
price: selectedVariant.price || 0,
productName: product.scent.name,
variantSize: selectedVariant.size,
productImage: product.images?.[0]?.url || getCategoryImage(product.category.name)
});
}}
className="w-full py-2.5 bg-spa-forest text-white hover:bg-spa-forest/90 rounded-xl font-bold text-xs uppercase tracking-widest transition-all shadow-sm hover:shadow flex items-center justify-center gap-1.5 cursor-pointer"
>
<ShoppingBag className="w-3.5 h-3.5" />
Add to Cart
</button>
)}
</div>
</div>
</Link>
)
</div>
);
}