import { useEffect, useMemo, useRef, useState } from 'react' import { ChevronDown, Loader2, Search, X } from 'lucide-react' import { useLocale } from '@meshkee/dashboard-ui' import { useT } from '../i18n/useT' import styles from './SearchableSelect.module.css' export const PRODUCT_SEARCH_DEBOUNCE_MS = 350 export interface ProductSearchOption { id: string title: string nameFa: string } interface ProductSearchSelectProps { options: ProductSearchOption[] value: string onChange: (value: string) => void placeholder?: string disabled?: boolean id?: string minSearchLength?: number isSearching?: boolean /** * When set, typing is debounced and the parent fetches results. * Local filtering is skipped. */ onQueryChange?: (query: string) => void debounceMs?: number 'aria-label'?: string } function formatProductLabel(option: ProductSearchOption, preferFa: boolean) { if (preferFa && option.nameFa) { return option.title ? `${option.nameFa} / ${option.title}` : option.nameFa } return option.nameFa ? `${option.title} / ${option.nameFa}` : option.title } export function ProductSearchSelect({ options, value, onChange, placeholder = 'Search products...', disabled = false, id, minSearchLength = 3, isSearching = false, onQueryChange, debounceMs = PRODUCT_SEARCH_DEBOUNCE_MS, 'aria-label': ariaLabel, }: ProductSearchSelectProps) { const t = useT() const { locale } = useLocale() const preferFa = locale === 'fa' const [open, setOpen] = useState(false) const [query, setQuery] = useState('') const containerRef = useRef(null) const inputRef = useRef(null) const onQueryChangeRef = useRef(onQueryChange) onQueryChangeRef.current = onQueryChange const selected = options.find((option) => option.id === value) const selectedLabel = selected ? formatProductLabel(selected, preferFa) : '' const trimmedQuery = query.trim() const canSearch = trimmedQuery.length >= minSearchLength useEffect(() => { const cb = onQueryChangeRef.current if (!cb) return if (!canSearch) { cb('') return } const timer = window.setTimeout(() => cb(trimmedQuery), debounceMs) return () => window.clearTimeout(timer) }, [canSearch, trimmedQuery, debounceMs]) const filtered = useMemo(() => { if (!canSearch) return [] if (onQueryChange) return options const q = trimmedQuery.toLowerCase() return options.filter( (option) => option.title.toLowerCase().includes(q) || option.nameFa.includes(trimmedQuery), ) }, [canSearch, onQueryChange, options, trimmedQuery]) useEffect(() => { if (!open) return const onClickOutside = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false) setQuery('') } } document.addEventListener('mousedown', onClickOutside) return () => document.removeEventListener('mousedown', onClickOutside) }, [open]) function selectOption(id: string) { onChange(id) setOpen(false) setQuery('') onQueryChangeRef.current?.('') } function handleFocus() { if (!disabled) setOpen(true) } const showDropdown = open && (canSearch || trimmedQuery.length > 0) return (
{ setQuery(e.target.value) if (!open) setOpen(true) if (value) onChange('') }} onFocus={handleFocus} disabled={disabled} aria-label={ariaLabel ?? placeholder} dir={preferFa ? 'rtl' : 'ltr'} /> {value && !open && ( )} {isSearching && open ? ( ) : ( )}
{showDropdown && canSearch && (
    {isSearching && filtered.length === 0 ? (
  • {t('storeItems.search.searching')}
  • ) : filtered.length === 0 ? (
  • {t('storeItems.search.noMatch')}
  • ) : ( filtered.map((option) => (
  • )) )}
)} {open && trimmedQuery.length > 0 && trimmedQuery.length < minSearchLength && (
  • {t('storeItems.search.minChars', { count: minSearchLength })}
)}
) }