'use client'; import { useMemo } from 'react'; import type { Product, ProductVariant } from 'brainerce'; import { getVariantOptions, getProductSwatches, formatPrice } from 'brainerce'; import type { InventoryInfo } from 'brainerce'; import { useCurrency } from '@/core/lib/use-currency'; import { useTranslations } from '@/core/lib/translations'; import { cn } from '@/core/lib/utils'; interface VariantSelectorProps { product: Product; selectedVariant: ProductVariant | null; onVariantChange: (variant: ProductVariant) => void; className?: string; } interface AttributeGroup { name: string; displayType: string; values: Array<{ value: string; swatchColor?: string | null; swatchColor2?: string | null; swatchImageUrl?: string | null; variants: ProductVariant[]; }>; } export function VariantSelector({ product, selectedVariant, onVariantChange, className, }: VariantSelectorProps) { const t = useTranslations('productDetail'); const currency = useCurrency(); const variants = useMemo(() => product.variants || [], [product.variants]); // Get swatch metadata from product attribute options const swatchData = useMemo(() => getProductSwatches(product), [product]); const swatchMap = useMemo(() => { const map = new Map< string, { displayType: string; options: Map< string, { swatchColor?: string | null; swatchColor2?: string | null; swatchImageUrl?: string | null; } >; } >(); for (const attr of swatchData) { const optMap = new Map< string, { swatchColor?: string | null; swatchColor2?: string | null; swatchImageUrl?: string | null; } >(); for (const opt of attr.options) { optMap.set(opt.name, { swatchColor: opt.swatchColor, swatchColor2: opt.swatchColor2, swatchImageUrl: opt.swatchImageUrl, }); } map.set(attr.attributeName, { displayType: attr.displayType, options: optMap }); } return map; }, [swatchData]); // Build attribute groups from variant data, enriched with swatch info const attributeGroups = useMemo(() => { const groups = new Map>(); for (const variant of variants) { const options = getVariantOptions(variant); for (const { name, value } of options) { if (!groups.has(name)) { groups.set(name, new Map()); } const valuesMap = groups.get(name)!; if (!valuesMap.has(value)) { valuesMap.set(value, []); } valuesMap.get(value)!.push(variant); } } return Array.from(groups.entries()).map(([name, valuesMap]) => { const attrSwatch = swatchMap.get(name); return { name, displayType: attrSwatch?.displayType || 'DROPDOWN', values: Array.from(valuesMap.entries()).map(([value, variantList]) => { const optSwatch = attrSwatch?.options.get(value); return { value, swatchColor: optSwatch?.swatchColor, swatchColor2: optSwatch?.swatchColor2, swatchImageUrl: optSwatch?.swatchImageUrl, variants: variantList, }; }), }; }); }, [variants, swatchMap]); // Get currently selected attribute values const selectedOptions = useMemo(() => { if (!selectedVariant) return new Map(); const opts = getVariantOptions(selectedVariant); return new Map(opts.map(({ name, value }) => [name, value])); }, [selectedVariant]); // Find the variant that matches all selected attributes function findMatchingVariant( attributeName: string, newValue: string ): ProductVariant | undefined { const nextSelection = new Map(selectedOptions); nextSelection.set(attributeName, newValue); return variants.find((v) => { const opts = getVariantOptions(v); return Array.from(nextSelection.entries()).every(([name, value]) => opts.some((o) => o.name === name && o.value === value) ); }); } if (attributeGroups.length === 0) return null; return (
{attributeGroups.map((group) => (
{group.values.map( ({ value, swatchColor, swatchColor2, swatchImageUrl, variants: matchingVariants, }) => { const isSelected = selectedOptions.get(group.name) === value; const matchedVariant = findMatchingVariant(group.name, value); const isAvailable = matchedVariant?.inventory?.canPurchase !== false; // Color swatch rendering if (group.displayType === 'COLOR_SWATCH' && swatchColor) { return ( ); } // Image swatch rendering if (group.displayType === 'IMAGE_SWATCH' && swatchImageUrl) { return ( ); } // Default button rendering (BUTTON, DROPDOWN, or fallback) return ( ); } )}
))} {/* Variant-specific info */} {selectedVariant && (
{selectedVariant.price && ( { formatPrice(selectedVariant.salePrice || selectedVariant.price, { currency, }) as string } )} {getTranslatedStockStatus(selectedVariant.inventory, t)}
)}
); } function getTranslatedStockStatus( inventory: InventoryInfo | null | undefined, t: (key: string) => string ): string { if (!inventory) return t('outOfStock'); const { trackingMode, inStock, available } = inventory; if (trackingMode === 'DISABLED') return t('unavailable'); if (!inStock) return t('outOfStock'); if (trackingMode === 'UNLIMITED') return t('inStock'); return t('availableInStock').replace('{available}', String(available)); }