import { useState, useRef, useEffect, useMemo } from 'react'; import { Search, Loader2, ChevronDown, Cloud, CloudOff } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { FilamentSectionProps, FilamentOption } from './types'; import { KNOWN_VARIANTS } from './constants'; import { parsePresetName } from './utils'; export function FilamentSection({ formData, updateField, cloudAuthenticated, loadingCloudPresets, presetInputValue, setPresetInputValue, selectedPresetOption, filamentOptions, availableBrands, availableMaterials, quickAdd, quantity, onQuantityChange, errors, }: FilamentSectionProps) { const { t } = useTranslation(); const [presetDropdownOpen, setPresetDropdownOpen] = useState(false); const [brandDropdownOpen, setBrandDropdownOpen] = useState(false); const [subtypeDropdownOpen, setSubtypeDropdownOpen] = useState(false); const [materialDropdownOpen, setMaterialDropdownOpen] = useState(false); const [brandSearch, setBrandSearch] = useState(''); const [subtypeSearch, setSubtypeSearch] = useState(''); const [materialSearch, setMaterialSearch] = useState(''); const [labelInput, setLabelInput] = useState(String(formData.label_weight)); const [isLabelFocused, setIsLabelFocused] = useState(false); const presetRef = useRef(null); const brandRef = useRef(null); const subtypeRef = useRef(null); const materialRef = useRef(null); // Close dropdowns on outside click useEffect(() => { const handleClick = (e: MouseEvent) => { if (presetRef.current && !presetRef.current.contains(e.target as Node)) { setPresetDropdownOpen(false); } if (materialRef.current && !materialRef.current.contains(e.target as Node)) { setMaterialDropdownOpen(false); } if (brandRef.current && !brandRef.current.contains(e.target as Node)) { setBrandDropdownOpen(false); } if (subtypeRef.current && !subtypeRef.current.contains(e.target as Node)) { setSubtypeDropdownOpen(false); } }; document.addEventListener('mousedown', handleClick); return () => document.removeEventListener('mousedown', handleClick); }, []); // Filtered presets based on search const filteredPresets = useMemo(() => { if (!presetInputValue) return filamentOptions; const search = presetInputValue.toLowerCase(); return filamentOptions.filter(o => o.displayName.toLowerCase().includes(search) || o.code.toLowerCase().includes(search), ); }, [filamentOptions, presetInputValue]); // Filtered brands const filteredBrands = useMemo(() => { if (!brandSearch) return availableBrands; const search = brandSearch.toLowerCase(); const filtered = availableBrands.filter(b => b.toLowerCase().includes(search)); // Sort: exact match first, then others return filtered.sort((a, b) => { const aExact = a.toLowerCase() === search; const bExact = b.toLowerCase() === search; if (aExact && !bExact) return -1; if (!aExact && bExact) return 1; return a.localeCompare(b); }); }, [availableBrands, brandSearch]); const filteredVariants = useMemo(() => { if (!subtypeSearch) return KNOWN_VARIANTS; const search = subtypeSearch.toLowerCase(); return KNOWN_VARIANTS.filter(v => v.toLowerCase().includes(search)); }, [subtypeSearch]); const filteredMaterials = useMemo(() => { if (!materialSearch) return availableMaterials; const search = materialSearch.toLowerCase(); const filtered = availableMaterials.filter(m => m.toLowerCase().includes(search)); // Sort: exact match first, then others return filtered.sort((a, b) => { const aExact = a.toLowerCase() === search; const bExact = b.toLowerCase() === search; if (aExact && !bExact) return -1; if (!aExact && bExact) return 1; return a.localeCompare(b); }); }, [materialSearch, availableMaterials]); useEffect(() => { if (!isLabelFocused) { setLabelInput(String(formData.label_weight)); } }, [formData.label_weight, isLabelFocused]); // Handle preset selection const handlePresetSelect = (option: FilamentOption) => { updateField('slicer_filament', option.code); setPresetInputValue(option.displayName); setPresetDropdownOpen(false); // Auto-fill material, brand, subtype from preset name const parsed = parsePresetName(option.name); if (parsed.material) updateField('material', parsed.material); if (parsed.brand) updateField('brand', parsed.brand); if (parsed.variant) updateField('subtype', parsed.variant); }; return (
{/* Cloud status indicator */} {!quickAdd && (
{loadingCloudPresets ? ( <> {t('inventory.loadingPresets')} ) : cloudAuthenticated ? ( <> {t('inventory.cloudConnected')} ) : ( <> {t('inventory.cloudNotConnected')} )}
)} {/* Slicer Preset (autocomplete) — hidden in quick-add mode */} {!quickAdd && (
{ setPresetInputValue(e.target.value); setPresetDropdownOpen(true); }} onFocus={() => { setPresetDropdownOpen(true); setPresetInputValue(''); }} /> {presetDropdownOpen && (
{filteredPresets.length === 0 ? (
{t('inventory.noPresetsFound')}
) : ( filteredPresets.map(option => ( )) )}
)}
{selectedPresetOption && (
{t('inventory.selectedPreset')}: {selectedPresetOption.code}
)} {errors?.slicer_filament && (

{errors.slicer_filament}

)}
)} {/* Material */}
{ setMaterialSearch(e.target.value); setMaterialDropdownOpen(true); }} onFocus={() => { setMaterialDropdownOpen(true); setMaterialSearch(''); }} /> {materialDropdownOpen && (
{filteredMaterials.length === 0 ? (
{t('inventory.noResults')}
) : ( filteredMaterials.map((material) => ( )) )} {/* Allow custom material */} {materialSearch && !filteredMaterials.includes(materialSearch) && ( )}
)}
{errors?.material && (

{errors.material}

)}
{/* Brand (dropdown with search) */}
{ setBrandSearch(e.target.value); setBrandDropdownOpen(true); }} onFocus={() => { setBrandDropdownOpen(true); setBrandSearch(''); }} /> {brandDropdownOpen && (
{filteredBrands.length === 0 ? (
{t('inventory.noResults')}
) : ( filteredBrands.map(brand => ( )) )} {/* Allow custom brand */} {brandSearch && !filteredBrands.includes(brandSearch) && ( )}
)}
{errors?.brand && (

{errors.brand}

)}
{/* Variant / Subtype */}
{ setSubtypeSearch(e.target.value); setSubtypeDropdownOpen(true); }} onFocus={() => { setSubtypeDropdownOpen(true); setSubtypeSearch(''); }} placeholder="Basic, Matte, Silk..." className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm placeholder:text-bambu-gray/50 focus:outline-none focus:border-bambu-green" /> {subtypeDropdownOpen && (
{filteredVariants.length === 0 ? (
{t('inventory.noResults')}
) : ( filteredVariants.map(variant => ( )) )} {subtypeSearch && !KNOWN_VARIANTS.some(v => v.toLowerCase() === subtypeSearch.toLowerCase().trim()) && ( )}
)}
{errors?.subtype && (

{errors.subtype}

)}
{/* Label Weight */}
setIsLabelFocused(true)} onChange={(e) => setLabelInput(e.target.value)} onBlur={() => { setIsLabelFocused(false); const raw = labelInput.trim(); const next = Number(raw); if (!raw || !Number.isFinite(next) || next < 0) { setLabelInput(String(formData.label_weight)); return; } const rounded = Math.round(next); updateField('label_weight', rounded); setLabelInput(String(rounded)); }} /> g
{/* Quantity — only in quick-add mode */} {quickAdd && (
{ const val = Math.max(1, Math.min(100, parseInt(e.target.value) || 1)); onQuantityChange(val); }} />
)}
); }