import { useEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { Plus, Search, X } from 'lucide-react' import { useLocale } from '@meshkee/dashboard-ui' import { useT } from '../i18n/useT' import { COLOR_PRESETS, getColorHex, normalizeColorName } from '../data/colorPresets' import type { Variation, VariationType } from '../types/variation' import { SIZE_OPTIONS } from '../types/variation' import { localizeVariationOptionLabel } from '../utils/localizeVariationOption' import { Tooltip } from './Tooltip' import styles from './VariationsModal.module.css' interface AddVariationModalProps { open: boolean onClose: () => void onSubmit: (variation: Omit) => void /** When set, form opens in edit mode with values pre-filled. */ initial?: Variation | null } interface SelectedColor { label: string hex: string } const ANIMATION_MS = 220 const DEFAULT_CUSTOM_HEX = '#64748B' function seedCustomValues(values: string[]) { const trimmed = values.map((value) => value.trim()).filter(Boolean) return trimmed.length ? trimmed : [''] } function normalizeHex(value: string): string { const trimmed = value.trim() if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) return trimmed.toUpperCase() if (/^[0-9a-fA-F]{6}$/.test(trimmed)) return `#${trimmed.toUpperCase()}` return DEFAULT_CUSTOM_HEX } function colorsFromInitial(initial: Variation): SelectedColor[] { return initial.values .map((raw, index) => { const label = raw.trim() if (!label) return null const canonical = normalizeColorName(label) const provided = initial.colorHexes?.[index] const hex = (provided && /^#[0-9a-fA-F]{6}$/.test(provided.trim()) ? provided.trim().toUpperCase() : null) ?? getColorHex(canonical) ?? DEFAULT_CUSTOM_HEX return { label: canonical, hex } }) .filter((item): item is SelectedColor => item !== null) } export function AddVariationModal({ open, onClose, onSubmit, initial = null }: AddVariationModalProps) { const t = useT() const { locale } = useLocale() const isFa = locale === 'fa' const isEdit = Boolean(initial) const [mounted, setMounted] = useState(open) const [closing, setClosing] = useState(false) const [type, setType] = useState('color') const [customName, setCustomName] = useState('') const [selectedColors, setSelectedColors] = useState([]) const [selectedSizes, setSelectedSizes] = useState([]) const [customValues, setCustomValues] = useState(['']) const [colorQuery, setColorQuery] = useState('') const [customColorName, setCustomColorName] = useState('') const [customColorHex, setCustomColorHex] = useState(DEFAULT_CUSTOM_HEX) const [customColorError, setCustomColorError] = useState('') const valueInputRefs = useRef<(HTMLInputElement | null)[]>([]) const pendingFocusIndex = useRef(null) useEffect(() => { if (pendingFocusIndex.current === null) return valueInputRefs.current[pendingFocusIndex.current]?.focus() pendingFocusIndex.current = null }, [customValues.length]) useEffect(() => { if (open) { setMounted(true) setClosing(false) setColorQuery('') setCustomColorName('') setCustomColorHex(DEFAULT_CUSTOM_HEX) setCustomColorError('') if (initial) { setType(initial.type) setCustomName(initial.type === 'custom' ? initial.name : '') setSelectedColors(initial.type === 'color' ? colorsFromInitial(initial) : []) setSelectedSizes(initial.type === 'size' ? [...initial.values] : []) setCustomValues( initial.type === 'custom' ? seedCustomValues(initial.values) : [''], ) } else { setType('color') setCustomName('') setSelectedColors([]) setSelectedSizes([]) setCustomValues(['']) } } else if (mounted) { setClosing(true) const timer = setTimeout(() => { setMounted(false) setClosing(false) }, ANIMATION_MS) return () => clearTimeout(timer) } }, [open, mounted, initial]) useEffect(() => { if (!mounted || closing) return const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } document.addEventListener('keydown', onKey) return () => document.removeEventListener('keydown', onKey) }, [mounted, closing, onClose]) const selectedColorLabels = useMemo( () => new Set(selectedColors.map((color) => color.label.toLowerCase())), [selectedColors], ) const filteredColors = useMemo(() => { const q = colorQuery.trim().toLowerCase() if (!q) return COLOR_PRESETS return COLOR_PRESETS.filter((color) => { const en = color.name.toLowerCase() const fa = localizeVariationOptionLabel(color.name, t).toLowerCase() return en.includes(q) || fa.includes(q) }) }, [colorQuery, t]) if (!mounted) return null function togglePresetColor(name: string, hex: string) { setSelectedColors((prev) => { const exists = prev.some((item) => item.label === name) if (exists) return prev.filter((item) => item.label !== name) return [...prev, { label: name, hex }] }) setCustomColorError('') } function removeSelectedColor(label: string) { setSelectedColors((prev) => prev.filter((item) => item.label !== label)) } function toggleSize(value: string) { setSelectedSizes((prev) => prev.includes(value) ? prev.filter((item) => item !== value) : [...prev, value], ) } function addCustomColor() { const label = customColorName.trim() if (!label) { setCustomColorError(t('categories.variations.customColorNameRequired')) return } const hex = normalizeHex(customColorHex) const key = label.toLowerCase() const canonical = normalizeColorName(label) const alreadySelected = selectedColorLabels.has(key) || selectedColorLabels.has(canonical.toLowerCase()) if (alreadySelected) { setCustomColorError(t('categories.variations.customColorExists')) return } setSelectedColors((prev) => [...prev, { label, hex }]) setCustomColorName('') setCustomColorHex(DEFAULT_CUSTOM_HEX) setCustomColorError('') } function updateCustomValue(index: number, value: string) { setCustomValues((prev) => prev.map((v, i) => (i === index ? value : v))) } function addCustomRow() { setCustomValues((prev) => [...prev, '']) } function removeCustomRow(index: number) { setCustomValues((prev) => prev.filter((_, i) => i !== index)) } function handleValueKeyDown(index: number, e: React.KeyboardEvent) { if (e.key !== 'Enter') return e.preventDefault() const nextIndex = index + 1 if (nextIndex < customValues.length) { valueInputRefs.current[nextIndex]?.focus() return } pendingFocusIndex.current = nextIndex setCustomValues((prev) => [...prev, '']) } function handleSubmit(e: React.FormEvent) { e.preventDefault() if (type === 'color') { onSubmit({ name: t('categories.variations.type.color'), type: 'color', values: selectedColors.map((color) => color.label), colorHexes: selectedColors.map((color) => color.hex), }) } else if (type === 'size') { onSubmit({ name: t('categories.variations.type.size'), type: 'size', values: selectedSizes }) } else { const name = customName.trim() || t('categories.variations.type.custom') const values = customValues.map((v) => v.trim()).filter(Boolean) onSubmit({ name, type: 'custom', values }) } } const canSubmit = type === 'color' ? selectedColors.length > 0 : type === 'size' ? selectedSizes.length > 0 : customValues.some((v) => v.trim()) return createPortal(
e.stopPropagation()} role="dialog" aria-modal="true" aria-labelledby="add-variation-title" lang={isFa ? 'fa' : 'en'} dir={isFa ? 'rtl' : 'ltr'} >

{isEdit ? t('categories.variations.editTitle') : t('categories.variations.addTitle')}

{type === 'color' && (
{selectedColors.length > 0 ? (
{selectedColors.map((color) => ( ))}
) : null}
{filteredColors.length === 0 ? (

{t('categories.variations.noColorMatch')}

) : ( filteredColors.map((color) => ( )) )}
{t('categories.variations.addCustomColor')}
{ setCustomColorName(e.target.value) if (customColorError) setCustomColorError('') }} placeholder={t('categories.variations.customColorNamePlaceholder')} aria-label={t('categories.variations.customColorName')} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault() addCustomColor() } }} />
{customColorError ? (

{customColorError}

) : null}
)} {type === 'size' && (
{SIZE_OPTIONS.map((size) => ( ))}
)} {type === 'custom' && ( <>
setCustomName(e.target.value)} />
{customValues.map((value, index) => (
{ valueInputRefs.current[index] = el }} type="text" placeholder={t('categories.variations.valueN', { n: index + 1 })} value={value} onChange={(e) => updateCustomValue(index, e.target.value)} onKeyDown={(e) => handleValueKeyDown(index, e)} /> {customValues.length > 1 && ( )}
))}
)}
, document.body, ) }