migration wip
This commit is contained in:
251
components/cards/ProductCard.tsx
Normal file
251
components/cards/ProductCard.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
'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 };
|
||||
Reference in New Issue
Block a user