218 lines
8.2 KiB
TypeScript
218 lines
8.2 KiB
TypeScript
import React from 'react';
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
import { ShoppingCart, Heart } from 'lucide-react';
|
|
import { formatPrice, formatDiscount } from '@/lib/currency';
|
|
import { Button } from './ui/button';
|
|
|
|
import { useShopSettings } from '@/hooks/useAppearanceSettings';
|
|
import { useWishlist } from '@/hooks/useWishlist';
|
|
import { useModules } from '@/hooks/useModules';
|
|
|
|
interface ProductCardProps {
|
|
product: {
|
|
id: number;
|
|
name: string;
|
|
slug: string;
|
|
price: string;
|
|
regular_price?: string;
|
|
sale_price?: string;
|
|
image?: string;
|
|
on_sale?: boolean;
|
|
stock_status?: string;
|
|
type?: string;
|
|
};
|
|
onAddToCart?: (product: any) => void;
|
|
}
|
|
|
|
export function ProductCard({ product, onAddToCart }: ProductCardProps) {
|
|
const navigate = useNavigate();
|
|
const { layout, elements, addToCart, saleBadge, isLoading } = useShopSettings();
|
|
const { isEnabled: wishlistEnabled, isInWishlist, toggleWishlist } = useWishlist();
|
|
const { isEnabled: isModuleEnabled } = useModules();
|
|
|
|
const showWishlist = isModuleEnabled('wishlist') && wishlistEnabled;
|
|
const inWishlist = showWishlist && isInWishlist(product.id);
|
|
|
|
const handleWishlistClick = async (e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
await toggleWishlist(product.id);
|
|
};
|
|
|
|
// Aspect ratio classes
|
|
const aspectRatioClass = {
|
|
'square': 'aspect-square',
|
|
'portrait': 'aspect-[3/4]',
|
|
'landscape': 'aspect-[4/3]',
|
|
}[layout.aspect_ratio] || 'aspect-square';
|
|
|
|
const isVariable = product.type === 'variable';
|
|
|
|
const handleAddToCart = (e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Variable products need to go to product page for attribute selection
|
|
if (isVariable) {
|
|
navigate(`/product/${product.slug}`);
|
|
return;
|
|
}
|
|
|
|
onAddToCart?.(product);
|
|
};
|
|
|
|
// Calculate discount if on sale
|
|
const discount = product.on_sale && product.regular_price && product.sale_price
|
|
? formatDiscount(parseFloat(product.regular_price), parseFloat(product.sale_price))
|
|
: null;
|
|
|
|
// Show skeleton while settings are loading to prevent layout shift
|
|
if (isLoading) {
|
|
return (
|
|
<div className="animate-pulse">
|
|
<div className="bg-muted aspect-square rounded-lg mb-4" />
|
|
<div className="h-4 bg-muted rounded mb-2" />
|
|
<div className="h-4 bg-muted rounded w-2/3" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Determine button variant and position based on settings
|
|
const buttonVariant = addToCart.style === 'outline' ? 'outline' : addToCart.style === 'text' ? 'ghost' : 'default';
|
|
const showButtonOnHover = addToCart.position === 'overlay';
|
|
const isTextOnly = addToCart.style === 'text';
|
|
|
|
// Card style variations - adapt to column count
|
|
const gridCols = parseInt(layout.grid_columns) || 3;
|
|
|
|
// More columns = cleaner styling
|
|
const getCardClasses = () => {
|
|
const cardStyle = layout.card_style || 'card';
|
|
if (cardStyle === 'minimal') {
|
|
return gridCols >= 4
|
|
? 'overflow-hidden hover:opacity-90 transition-opacity'
|
|
: 'overflow-hidden hover:opacity-80 transition-opacity border-b border-border pb-4';
|
|
}
|
|
if (cardStyle === 'overlay') {
|
|
return gridCols >= 4
|
|
? 'relative overflow-hidden group-hover:shadow-lg transition-all rounded-md'
|
|
: 'relative overflow-hidden group-hover:shadow-xl transition-all rounded-lg bg-card';
|
|
}
|
|
// Default 'card' style
|
|
return gridCols >= 4
|
|
? 'border border-border rounded-md overflow-hidden hover:shadow-md transition-shadow bg-card'
|
|
: 'border border-border rounded-lg overflow-hidden hover:shadow-lg transition-shadow bg-card';
|
|
};
|
|
|
|
const cardClasses = getCardClasses();
|
|
|
|
// Text alignment class
|
|
const textAlignClass = {
|
|
'left': 'text-left',
|
|
'center': 'text-center',
|
|
'right': 'text-right',
|
|
}[layout.card_text_align || 'left'] || 'text-left';
|
|
|
|
// Unified card render — adapts via card_style (card / minimal / overlay) + shop settings
|
|
return (
|
|
<Link to={`/product/${product.slug}`} className="group h-full">
|
|
<div className={`${cardClasses} h-full flex flex-col`}>
|
|
{/* Image */}
|
|
<div className={`relative w-full overflow-hidden bg-muted ${aspectRatioClass}`}>
|
|
{product.image ? (
|
|
<img
|
|
src={product.image}
|
|
alt={product.name}
|
|
className="absolute inset-0 w-full !h-full object-cover object-center group-hover:scale-105 transition-transform duration-300"
|
|
/>
|
|
) : (
|
|
<div className="absolute inset-0 flex items-center justify-center text-muted-foreground">
|
|
No Image
|
|
</div>
|
|
)}
|
|
|
|
{/* Sale Badge */}
|
|
{elements.sale_badges && product.on_sale && discount && (
|
|
<div
|
|
className="absolute top-2 right-2 text-white text-xs font-bold px-2 py-1 rounded"
|
|
style={{ backgroundColor: saleBadge.color }}
|
|
>
|
|
{discount}
|
|
</div>
|
|
)}
|
|
|
|
{/* Wishlist Button */}
|
|
{showWishlist && (
|
|
<div className="absolute top-2 left-2 z-10">
|
|
<button
|
|
onClick={handleWishlistClick}
|
|
className={`font-[inherit] p-2 rounded-full shadow-md border flex items-center justify-center transition-all ${
|
|
inWishlist
|
|
? 'bg-red-50 border-red-100 dark:bg-red-950 dark:border-red-900'
|
|
: 'bg-background border-border hover:bg-muted text-foreground'
|
|
}`}
|
|
title={inWishlist ? 'Remove from wishlist' : 'Add to wishlist'}
|
|
>
|
|
<Heart className={`w-4 h-4 block transition-all ${inWishlist ? 'fill-red-500 text-red-500' : ''}`} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Hover/Overlay Button */}
|
|
{showButtonOnHover && (
|
|
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-10 transition-all duration-300 flex items-center justify-center">
|
|
<Button
|
|
onClick={handleAddToCart}
|
|
variant={buttonVariant}
|
|
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
|
disabled={product.stock_status === 'outofstock'}
|
|
>
|
|
{!isTextOnly && addToCart.show_icon && <ShoppingCart className="w-4 h-4 mr-2" />}
|
|
{product.stock_status === 'outofstock' ? 'Out of Stock' : isVariable ? 'Select Options' : 'Add to Cart'}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className={`p-4 flex-1 flex flex-col ${textAlignClass}`}>
|
|
<h3 className="text-sm font-medium text-foreground mb-2 line-clamp-2 leading-snug group-hover:text-primary transition-colors">
|
|
{product.name}
|
|
</h3>
|
|
|
|
{/* Price */}
|
|
<div className={`flex items-center gap-2 mb-3 ${(layout.card_text_align || 'left') === 'center' ? 'justify-center' : (layout.card_text_align || 'left') === 'right' ? 'justify-end' : ''}`}>
|
|
{product.on_sale && product.regular_price ? (
|
|
<>
|
|
<span className="text-base font-bold text-primary">
|
|
{formatPrice(product.sale_price || product.price)}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground line-through">
|
|
{formatPrice(product.regular_price)}
|
|
</span>
|
|
</>
|
|
) : (
|
|
<span className="text-base font-bold text-foreground">
|
|
{formatPrice(product.price)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Add to Cart Button */}
|
|
{!showButtonOnHover && (
|
|
<Button
|
|
onClick={handleAddToCart}
|
|
variant={buttonVariant}
|
|
className={`w-full mt-auto ${isTextOnly ? 'border-0 shadow-none hover:bg-transparent hover:underline' : ''}`}
|
|
disabled={product.stock_status === 'outofstock'}
|
|
>
|
|
{!isTextOnly && addToCart.show_icon && <ShoppingCart className="w-4 h-4 mr-2" />}
|
|
{product.stock_status === 'outofstock' ? 'Out of Stock' : isVariable ? 'Select Options' : 'Add to Cart'}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
);
|
|
}
|