251 lines
6.6 KiB
TypeScript
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 }; |