import { useEffect, useMemo, useRef, useState } from 'react' import { ChevronDown, Search, X } from 'lucide-react' import { useLocale } from '@meshkee/dashboard-ui' import { useT } from '../i18n/useT' import type { UserProductCategoryOption } from '../services/userProductsService' import styles from './CategorySearchSelect.module.css' export interface FlatCategoryOption extends UserProductCategoryOption { depth: number } interface CategorySearchSelectProps { options: UserProductCategoryOption[] value: string onChange: (value: string) => void disabled?: boolean placeholder?: string id?: string allowEmpty?: boolean dropdownVariant?: 'overlay' | 'inFlow' } function categoryLabel(category: UserProductCategoryOption, isFa: boolean) { if (isFa) { return category.nameFa?.trim() || category.name } return category.name || category.nameFa?.trim() || '' } export function flattenCategoryTree( categories: UserProductCategoryOption[], ): FlatCategoryOption[] { const byParent = new Map() for (const category of categories) { const parentKey = category.parentId const list = byParent.get(parentKey) ?? [] list.push(category) byParent.set(parentKey, list) } for (const list of byParent.values()) { list.sort((a, b) => { const aLabel = (a.nameFa || a.name).localeCompare(b.nameFa || b.name, 'fa') return aLabel }) } const result: FlatCategoryOption[] = [] const ids = new Set(categories.map((item) => item.id)) function walk(parentId: string | null, depth: number) { const children = byParent.get(parentId) ?? [] for (const child of children) { result.push({ ...child, depth }) walk(child.id, depth + 1) } } walk(null, 0) for (const category of categories) { if (category.parentId && !ids.has(category.parentId)) { if (!result.some((item) => item.id === category.id)) { result.push({ ...category, depth: 0 }) walk(category.id, 1) } } } return result } function filterCategoryTree( flat: FlatCategoryOption[], query: string, ): FlatCategoryOption[] { const q = query.trim().toLowerCase() if (!q) return flat const byId = new Map(flat.map((item) => [item.id, item])) const matchedIds = new Set() for (const item of flat) { const nameEn = item.name.toLowerCase() const nameFa = (item.nameFa || '').toLowerCase() if (nameEn.includes(q) || nameFa.includes(q) || (item.nameFa || '').includes(query.trim())) { matchedIds.add(item.id) let parentId = item.parentId while (parentId) { matchedIds.add(parentId) parentId = byId.get(parentId)?.parentId ?? null } } } return flat.filter((item) => matchedIds.has(item.id)) } function isLastChildInGroup(items: FlatCategoryOption[], index: number): boolean { const current = items[index] if (current.depth === 0) return false const next = items[index + 1] return !next || next.depth < current.depth } function CategoryTreeGutters({ depth, isLast }: { depth: number; isLast: boolean }) { if (depth === 0) return null return (
{Array.from({ length: depth }, (_, level) => { const isBranchLevel = level === depth - 1 return ( ) })}
) } export function CategorySearchSelect({ options, value, onChange, disabled = false, placeholder, id, allowEmpty = false, dropdownVariant = 'overlay', }: CategorySearchSelectProps) { const t = useT() const { locale } = useLocale() const isFa = locale === 'fa' const [open, setOpen] = useState(false) const [query, setQuery] = useState('') const containerRef = useRef(null) const flat = useMemo(() => flattenCategoryTree(options), [options]) const filtered = useMemo(() => filterCategoryTree(flat, query), [flat, query]) const selected = flat.find((option) => option.id === value) const selectedLabel = selected ? categoryLabel(selected, isFa) : '' const searchPlaceholder = placeholder ?? t('myProducts.fields.searchCategory') useEffect(() => { if (!open) return function 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]) useEffect(() => { if (disabled) { setOpen(false) setQuery('') } }, [disabled]) function selectOption(nextId: string) { onChange(nextId) setOpen(false) setQuery('') } const dropdownClassName = [ styles.dropdown, dropdownVariant === 'inFlow' ? styles.dropdownInFlow : styles.dropdownOverlay, ] .filter(Boolean) .join(' ') return (
{ setQuery(e.target.value) if (!open) setOpen(true) }} onFocus={() => { if (!disabled) setOpen(true) }} autoComplete="off" role="combobox" aria-expanded={open} aria-autocomplete="list" /> {allowEmpty && value && !open && !disabled ? ( ) : null}
{open && !disabled ? (
    {filtered.length === 0 ? (
  • {t('myProducts.fields.noCategories')}
  • ) : ( filtered.map((option, index) => { const isLastChild = isLastChildInGroup(filtered, index) return (
  • ) }) )}
) : null}
) }