116 lines
4.4 KiB
TypeScript
116 lines
4.4 KiB
TypeScript
"use client"
|
|
|
|
import { useMemo } from 'react'
|
|
import Image from 'next/image'
|
|
import Link from './LocalizedLink'
|
|
import type { Product, Variant } from '@homaroma/types'
|
|
import { formatPrice } from '@homaroma/types'
|
|
import { resolveLocalized } from '../lib/localization'
|
|
import { generateProductSlug } from '../lib/slugify'
|
|
import { useLang } from '../hooks/useLang'
|
|
|
|
interface ProductSwatchCardProps {
|
|
product: Product
|
|
badgeText?: string | null
|
|
}
|
|
|
|
export default function ProductSwatchCard({ product, badgeText }: ProductSwatchCardProps) {
|
|
const lang = useLang()
|
|
const productName = resolveLocalized(product.name, lang) || product.brand?.name || 'Product'
|
|
const productSlug = generateProductSlug(productName, product.id)
|
|
|
|
// Find all unique color swatches from variants
|
|
const colorSwatches = useMemo(() => {
|
|
const swatches: { colorCode: string; variant: Variant }[] = []
|
|
const seenColors = new Set<string>()
|
|
|
|
if (Array.isArray(product.variants)) {
|
|
for (const v of product.variants) {
|
|
if (v.colorCode && !seenColors.has(v.colorCode.toLowerCase())) {
|
|
seenColors.add(v.colorCode.toLowerCase())
|
|
swatches.push({ colorCode: v.colorCode, variant: v })
|
|
}
|
|
}
|
|
}
|
|
return swatches
|
|
}, [product.variants])
|
|
|
|
// Active selected variant
|
|
const activeVariant = useMemo<Variant | null>(() => {
|
|
return colorSwatches[0]?.variant || product.variants?.[0] || null
|
|
}, [colorSwatches, product.variants])
|
|
|
|
// Display Image: variant image if available, else first product image
|
|
const displayImageUrl = useMemo(() => {
|
|
if (activeVariant?.imageUrl) return activeVariant.imageUrl
|
|
if (product.images && product.images.length > 0 && product.images[0].url) {
|
|
return product.images[0].url
|
|
}
|
|
return product.category?.imageUrl || '/assets/hero.png'
|
|
}, [activeVariant, product.images, product.category])
|
|
|
|
const displayPrice = activeVariant?.price ?? product.variants?.[0]?.price ?? 0
|
|
|
|
return (
|
|
<div className="group flex flex-col h-full select-none">
|
|
{/* 1:1 Image Frame */}
|
|
<Link href={`/products/${productSlug}`} className="relative aspect-square w-full rounded-2xl overflow-hidden bg-[#F6F5F2] dark:bg-neutral-800/80 border border-black/[0.04] dark:border-white/[0.05] transition-all duration-300 group-hover:shadow-md block">
|
|
{/* Badge (e.g. NEW) */}
|
|
{badgeText && (
|
|
<div className="absolute top-2.5 right-2.5 z-10">
|
|
<span className="inline-block px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-widest bg-[#485645] text-[#F0F3EE] shadow-sm">
|
|
{badgeText}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Product Image */}
|
|
<div className="relative w-full h-full p-3 flex items-center justify-center">
|
|
<Image
|
|
src={displayImageUrl}
|
|
alt={productName}
|
|
fill
|
|
unoptimized
|
|
sizes="(max-width: 768px) 50vw, (max-width: 1200px) 25vw, 20vw"
|
|
className="object-contain p-2 transition-transform duration-500 ease-out group-hover:scale-105"
|
|
/>
|
|
</div>
|
|
</Link>
|
|
|
|
{/* Meta (Title, Price, Swatches) */}
|
|
<div className="pt-2.5 pb-1 flex flex-col gap-1">
|
|
{/* Product Title */}
|
|
<Link href={`/products/${productSlug}`}>
|
|
<h3 className="text-xs md:text-sm font-medium text-neutral-800 dark:text-neutral-200 line-clamp-1 hover:underline transition-colors">
|
|
{productName}
|
|
</h3>
|
|
</Link>
|
|
|
|
{/* Price */}
|
|
<div className="text-xs md:text-sm font-bold text-neutral-900 dark:text-neutral-100">
|
|
{formatPrice(displayPrice)}
|
|
</div>
|
|
|
|
{/* Color Swatch Dots (Display Only) */}
|
|
{colorSwatches.length > 0 && (
|
|
<div className="flex items-center gap-1.5 pt-1 flex-wrap">
|
|
{colorSwatches.slice(0, 5).map(({ colorCode, variant }) => (
|
|
<span
|
|
key={colorCode}
|
|
className="relative w-3.5 h-3.5 rounded-full border border-black/15 dark:border-white/20 shadow-sm shrink-0"
|
|
style={{ backgroundColor: colorCode }}
|
|
title={variant.option1 || variant.option2 || colorCode}
|
|
/>
|
|
))}
|
|
{colorSwatches.length > 5 && (
|
|
<span className="text-[10px] font-semibold text-neutral-500 dark:text-neutral-400 pl-0.5">
|
|
+{colorSwatches.length - 5}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|