import { useEffect, useState } from 'react' import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { ApiError } from '../lib/api' import { countSelectedOptions, getProductVariationValues, saveProductVariationValues, type ProductVariationSelection, } from '../services/productVariationService' import { MultiSelectDropdown } from './MultiSelectDropdown' import styles from './VariationsModal.module.css' interface ProductVariantsModalProps { open: boolean productId: string categoryId: string productName: string onClose: () => void onVariantsChange?: (count: number) => void } const ANIMATION_MS = 220 export function ProductVariantsModal({ open, productId, categoryId, productName, onClose, onVariantsChange, }: ProductVariantsModalProps) { const [mounted, setMounted] = useState(open) const [closing, setClosing] = useState(false) const [variations, setVariations] = useState([]) const [isLoading, setIsLoading] = useState(false) const [isSaving, setIsSaving] = useState(false) const [error, setError] = useState('') useEffect(() => { if (open) { setMounted(true) setClosing(false) } else if (mounted) { setClosing(true) const timer = setTimeout(() => { setMounted(false) setClosing(false) }, ANIMATION_MS) return () => clearTimeout(timer) } }, [open, mounted]) useEffect(() => { if (!open || !productId) return const controller = new AbortController() void loadData(controller.signal) return () => controller.abort() }, [open, productId, categoryId]) async function loadData(signal?: AbortSignal) { setIsLoading(true) setError('') try { const data = await getProductVariationValues(productId, signal) setVariations(data.variations) onVariantsChange?.(countSelectedOptions(data.variations)) } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') return if (err instanceof ApiError) { setError(err.message) } else { setError('Unable to load product variations.') } } finally { setIsLoading(false) } } 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]) if (!mounted) return null function handleSelectionChange(variationId: string, optionIds: string[]) { setVariations((prev) => prev.map((variation) => variation.id === variationId ? { ...variation, selectedOptionIds: optionIds } : variation, ), ) } async function handleSave() { setIsSaving(true) setError('') try { const data = await saveProductVariationValues(productId, variations) setVariations(data.variations) onVariantsChange?.(countSelectedOptions(data.variations)) onClose() } catch (err) { if (err instanceof ApiError) { setError(err.message) } else { setError('Unable to save product variations.') } } finally { setIsSaving(false) } } return createPortal(
e.stopPropagation()} role="dialog" aria-modal="true" aria-labelledby="product-variants-title" >

Product Variations

{productName &&

{productName}

}
{!categoryId && (

Assign a category to this product before managing variations.

)} {error &&

{error}

} {isLoading ? (

Loading variations...

) : variations.length === 0 ? (

{categoryId ? 'This category has no variations. Add variations on the category first.' : 'No variations available.'}

) : (
{variations.map((variation) => (
({ value: option.id, label: option.label, }))} value={variation.selectedOptionIds} onChange={(optionIds) => handleSelectionChange(variation.id, optionIds)} placeholder={`Select ${variation.name.toLowerCase()} values`} disabled={isSaving} />
))}
)}
, document.body, ) }