wgp-next-shop-ui-lib
Version:
Библиотека UI-компонентов для Next-Shop с поддержкой e-commerce функций
209 lines (190 loc) • 6.51 kB
JSX
'use client';
import React, { useState } from 'react';
import { Heart, ShoppingCart, Eye, Star } from 'lucide-react';
const ProductCard = ({
product,
onAddToCart = null,
onToggleFavorite = null,
onViewDetails = null,
showRating = true,
showFavorite = true,
showQuickView = true,
customStyles = {},
renderPrice = null,
renderImage = null,
className = ''
}) => {
const [isHovered, setIsHovered] = useState(false);
const [isImageLoading, setIsImageLoading] = useState(true);
if (!product) return null;
const {
id,
name,
price,
originalPrice,
discount,
image,
images = [],
rating = 0,
reviewCount = 0,
inStock = true,
isFavorite = false,
badge = null
} = product;
const defaultStyles = {
card: "bg-white rounded-lg shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 overflow-hidden group",
imageContainer: "relative overflow-hidden aspect-square",
image: "w-full h-full object-cover transition-transform duration-300 group-hover:scale-105",
badge: "absolute top-2 left-2 px-2 py-1 rounded text-xs font-medium z-10",
actions: "absolute top-2 right-2 flex flex-col gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300",
actionButton: "p-2 bg-white rounded-full shadow-md hover:shadow-lg transition-all duration-200 hover:scale-110",
content: "p-4",
title: "font-medium text-gray-900 line-clamp-2 mb-2",
rating: "flex items-center gap-1 text-sm text-gray-600 mb-2",
priceContainer: "flex items-center gap-2 mb-3",
price: "font-bold text-lg text-gray-900",
originalPrice: "text-sm text-gray-500 line-through",
discount: "text-sm text-red-600 font-medium",
addToCartButton: "w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors duration-200 font-medium",
outOfStockButton: "w-full bg-gray-300 text-gray-500 py-2 px-4 rounded-md cursor-not-allowed",
loadingImage: "w-full h-full bg-gray-200 animate-pulse flex items-center justify-center"
};
const styles = { ...defaultStyles, ...customStyles };
const getBadgeColor = (badgeType) => {
switch (badgeType) {
case 'sale': return 'bg-red-500 text-white';
case 'new': return 'bg-green-500 text-white';
case 'hot': return 'bg-orange-500 text-white';
default: return 'bg-gray-500 text-white';
}
};
const renderStars = (rating) => {
return Array.from({ length: 5 }, (_, i) => (
<Star
key={i}
size={14}
className={i < rating ? 'fill-yellow-400 text-yellow-400' : 'text-gray-300'}
/>
));
};
const handleAddToCart = (e) => {
e.preventDefault();
e.stopPropagation();
onAddToCart?.(product);
};
const handleToggleFavorite = (e) => {
e.preventDefault();
e.stopPropagation();
onToggleFavorite?.(product);
};
const handleQuickView = (e) => {
e.preventDefault();
e.stopPropagation();
onViewDetails?.(product);
};
return (
<div
className={`${styles.card} ${className}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Изображение */}
<div className={styles.imageContainer}>
{renderImage ? renderImage(product, isHovered) : (
<>
{isImageLoading && (
<div className={styles.loadingImage}>
<div className="w-12 h-12 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin"></div>
</div>
)}
<img
src={isHovered && images[1] ? images[1] : image}
alt={name}
className={styles.image}
onLoad={() => setIsImageLoading(false)}
onError={() => setIsImageLoading(false)}
style={{ display: isImageLoading ? 'none' : 'block' }}
/>
</>
)}
{/* Бейдж */}
{badge && (
<div className={`${styles.badge} ${getBadgeColor(badge.type)}`}>
{badge.text}
</div>
)}
{/* Экшены */}
<div className={styles.actions}>
{showFavorite && (
<button
onClick={handleToggleFavorite}
className={`${styles.actionButton} ${isFavorite ? 'text-red-500' : 'text-gray-600'}`}
>
<Heart size={16} fill={isFavorite ? 'currentColor' : 'none'} />
</button>
)}
{showQuickView && (
<button
onClick={handleQuickView}
className={`${styles.actionButton} text-gray-600`}
>
<Eye size={16} />
</button>
)}
</div>
</div>
{/* Контент */}
<div className={styles.content}>
{/* Название */}
<h3 className={styles.title}>{name}</h3>
{/* Рейтинг */}
{showRating && rating > 0 && (
<div className={styles.rating}>
<div className="flex">
{renderStars(rating)}
</div>
<span>({reviewCount})</span>
</div>
)}
{/* Цена */}
<div className={styles.priceContainer}>
{renderPrice ? renderPrice(product) : (
<>
<span className={styles.price}>
{typeof price === 'number' ? `${price.toLocaleString()} ₽` : price}
</span>
{originalPrice && originalPrice > price && (
<span className={styles.originalPrice}>
{typeof originalPrice === 'number' ? `${originalPrice.toLocaleString()} ₽` : originalPrice}
</span>
)}
{discount && (
<span className={styles.discount}>
-{discount}%
</span>
)}
</>
)}
</div>
{/* Кнопка добавления в корзину */}
{inStock ? (
<button
onClick={handleAddToCart}
className={styles.addToCartButton}
>
<ShoppingCart size={16} className="inline mr-2" />
В корзину
</button>
) : (
<button
disabled
className={styles.outOfStockButton}
>
Нет в наличии
</button>
)}
</div>
</div>
);
};
export default ProductCard;