Files
klz-cables.com/components/cards/ProductCard.tsx
2025-12-29 18:18:48 +01:00

251 lines
6.6 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import { BaseCard, BaseCardProps, CardSize, CardLayout } from './BaseCard';
import { Badge, BadgeGroup, Button } from '@/components/ui';
import { Product } from '@/lib/data';
import { cn } from '@/lib/utils';
// ProductCard specific props
export interface ProductCardProps extends Omit<BaseCardProps, 'title' | 'description' | 'image' | 'footer'> {
/** Product data from WordPress */
product: Product;
/** Display price */
showPrice?: boolean;
/** Display stock status */
showStock?: boolean;
/** Display SKU */
showSku?: boolean;
/** Display categories */
showCategories?: boolean;
/** Display add to cart button */
showAddToCart?: boolean;
/** Display view details button */
showViewDetails?: boolean;
/** Enable image hover swap */
enableImageSwap?: boolean;
/** Locale for formatting */
locale?: string;
/** Add to cart handler */
onAddToCart?: (product: Product) => void;
}
// Helper to get price display
const getPriceDisplay = (product: Product) => {
const { regularPrice, salePrice, currency } = product;
if (salePrice && salePrice !== regularPrice) {
return {
current: salePrice,
original: regularPrice,
isOnSale: true,
formatted: `${salePrice} ${currency} ~~${regularPrice} ${currency}~~`
};
}
return {
current: regularPrice,
original: null,
isOnSale: false,
formatted: `${regularPrice} ${currency}`
};
};
// Helper to get stock status
const getStockStatus = (stockStatus: string, locale: string = 'de') => {
const statusMap = {
instock: {
text: locale === 'de' ? 'Auf Lager' : 'In Stock',
variant: 'success' as const,
},
outofstock: {
text: locale === 'de' ? 'Nicht auf Lager' : 'Out of Stock',
variant: 'error' as const,
},
onbackorder: {
text: locale === 'de' ? 'Nachbestellung' : 'On Backorder',
variant: 'warning' as const,
},
};
return statusMap[stockStatus as keyof typeof statusMap] || statusMap.outofstock;
};
// Helper to get product image
const getProductImage = (product: Product, index: number = 0): string | undefined => {
if (product.images && product.images.length > index) {
return product.images[index];
}
if (product.featuredImage) {
return product.featuredImage;
}
return undefined;
};
export const ProductCard: React.FC<ProductCardProps> = ({
product,
size = 'md',
layout = 'vertical',
showPrice = true,
showStock = true,
showSku = false,
showCategories = true,
showAddToCart = true,
showViewDetails = false,
enableImageSwap = true,
locale = 'de',
onAddToCart,
className = '',
...props
}) => {
const [currentImageIndex, setCurrentImageIndex] = useState(0);
// Get product data
const title = product.name;
const description = product.shortDescriptionHtml ?
product.shortDescriptionHtml.replace(/<[^>]*>/g, '').substring(0, 100) + '...' :
'';
const primaryImage = getProductImage(product, currentImageIndex);
const priceInfo = showPrice ? getPriceDisplay(product) : null;
const stockInfo = showStock ? getStockStatus(product.stockStatus, locale) : null;
const categories = showCategories ? product.categories.map(c => c.name) : [];
const sku = showSku ? product.sku : null;
// Build badge component for categories and stock
const badge = (
<BadgeGroup gap="xs">
{showStock && stockInfo && (
<Badge variant={stockInfo.variant} size={size === 'sm' ? 'sm' : 'md'}>
{stockInfo.text}
</Badge>
)}
{categories.map((category, index) => (
<Badge
key={index}
variant="neutral"
size={size === 'sm' ? 'sm' : 'md'}
>
{category}
</Badge>
))}
</BadgeGroup>
);
// Build header with SKU
const header = sku ? (
<span className="text-xs text-gray-500 font-mono">
SKU: {sku}
</span>
) : null;
// Build price display
const priceDisplay = priceInfo ? (
<div className="flex items-center gap-2">
<span className={cn(
'font-bold',
priceInfo.isOnSale ? 'text-red-600' : 'text-gray-900',
size === 'sm' && 'text-sm',
size === 'md' && 'text-base',
size === 'lg' && 'text-lg'
)}>
{priceInfo.current}
</span>
{priceInfo.isOnSale && (
<span className="text-sm text-gray-400 line-through">
{priceInfo.original}
</span>
)}
</div>
) : null;
// Build footer with buttons
const footer = (
<div className="flex gap-2 w-full">
{showAddToCart && (
<Button
size={size === 'sm' ? 'sm' : 'md'}
variant="primary"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
if (onAddToCart) onAddToCart(product);
}}
className="flex-1"
>
{locale === 'de' ? 'In den Warenkorb' : 'Add to Cart'}
</Button>
)}
{showViewDetails && (
<Button
size={size === 'sm' ? 'sm' : 'md'}
variant="outline"
className="flex-1"
>
{locale === 'de' ? 'Details' : 'Details'}
</Button>
)}
</div>
);
// Build description with price
const descriptionContent = (
<div>
{description && <div className="text-gray-600 mb-2">{description}</div>}
{priceDisplay}
</div>
);
// Handle image hover for swap
const handleMouseEnter = () => {
if (enableImageSwap && product.images && product.images.length > 1) {
setCurrentImageIndex(1);
}
};
const handleMouseLeave = () => {
if (enableImageSwap) {
setCurrentImageIndex(0);
}
};
return (
<BaseCard
title={title}
description={descriptionContent}
image={primaryImage}
imageAlt={title}
size={size}
layout={layout}
href={product.path}
badge={badge}
header={header}
footer={footer}
hoverable={true}
variant="elevated"
className={className}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...props}
/>
);
};
// ProductCard variations
export const ProductCardVertical: React.FC<ProductCardProps> = (props) => (
<ProductCard {...props} layout="vertical" />
);
export const ProductCardHorizontal: React.FC<ProductCardProps> = (props) => (
<ProductCard {...props} layout="horizontal" />
);
export const ProductCardSmall: React.FC<ProductCardProps> = (props) => (
<ProductCard {...props} size="sm" />
);
export const ProductCardLarge: React.FC<ProductCardProps> = (props) => (
<ProductCard {...props} size="lg" />
);
// Export types
export type { CardSize, CardLayout };