import { useEffect, useMemo, useRef, useState } from 'react' import { ChevronDown, ChevronRight, Search, X } from 'lucide-react' import { useLocale } from '@meshkee/dashboard-ui' import { useT } from '../i18n/useT' import type { Category } from '../types/category' import { getAncestorIds, getChildren } from '../utils/categories' import { Tooltip } from './Tooltip' import treeStyles from './CategoryTree.module.css' import styles from './CategoryTreeSelect.module.css' interface CategoryTreeSelectProps { categories: Category[] value: string onChange: (value: string) => void placeholder?: string disabled?: boolean id?: string /** Overlay floats over content; inFlow expands the parent (use inside modals). */ dropdownVariant?: 'overlay' | 'inFlow' } function categoryLabel(category: Category, isFa: boolean) { if (isFa) { return category.nameFa ? `${category.nameFa}${category.nameEn ? ` / ${category.nameEn}` : ''}` : category.nameEn } return category.nameEn ? `${category.nameEn}${category.nameFa ? ` / ${category.nameFa}` : ''}` : category.nameFa } function categoryMatches(category: Category, query: string) { const q = query.trim().toLowerCase() if (!q) return true return ( category.nameEn.toLowerCase().includes(q) || category.nameFa.toLowerCase().includes(q) || category.nameFa.includes(query.trim()) ) } function visibleIdsForQuery(categories: Category[], query: string): Set | null { const q = query.trim() if (!q) return null const matched = categories.filter((category) => categoryMatches(category, q)) const visible = new Set() for (const category of matched) { visible.add(category.id) for (const ancestorId of getAncestorIds(categories, category.id)) { visible.add(ancestorId) } } return visible } function TreeNodes({ categories, parentId, value, expandedIds, visibleIds, forceExpanded, onToggle, onSelect, }: { categories: Category[] parentId: string | null value: string expandedIds: Set visibleIds: Set | null forceExpanded: boolean onToggle: (id: string) => void onSelect: (id: string) => void }) { const t = useT() const { locale } = useLocale() const isFa = locale === 'fa' const nodes = getChildren(categories, parentId).filter( (category) => !visibleIds || visibleIds.has(category.id), ) return ( <> {nodes.map((category) => { const childCount = getChildren(categories, category.id).some( (child) => !visibleIds || visibleIds.has(child.id), ) const expanded = forceExpanded || expandedIds.has(category.id) const selected = value === category.id const primary = isFa ? category.nameFa || category.nameEn : category.nameEn const secondary = isFa ? category.nameFa && category.nameEn && category.nameFa !== category.nameEn ? category.nameEn : '' : category.nameFa return (
{childCount ? ( ) : ( )}
{childCount ? (
) : null}
) })} ) } export function CategoryTreeSelect({ categories, value, onChange, placeholder, disabled = false, id, dropdownVariant = 'overlay', }: CategoryTreeSelectProps) { const t = useT() const { locale } = useLocale() const isFa = locale === 'fa' const [open, setOpen] = useState(false) const [query, setQuery] = useState('') const [expandedIds, setExpandedIds] = useState>(() => new Set()) const containerRef = useRef(null) const selected = categories.find((category) => category.id === value) const selectedLabel = selected ? categoryLabel(selected, isFa) : '' const searchPlaceholder = placeholder ?? t('categories.select.search') const visibleIds = useMemo( () => visibleIdsForQuery(categories, query), [categories, query], ) useEffect(() => { setOpen(false) setQuery('') setExpandedIds(new Set()) }, [categories]) 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 (!value) return setExpandedIds((prev) => { const next = new Set(prev) for (const ancestorId of getAncestorIds(categories, value)) { next.add(ancestorId) } return next }) }, [categories, value]) function handleToggle(id: string) { setExpandedIds((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } function selectOption(id: string) { onChange(id) setOpen(false) setQuery('') } const searching = query.trim().length > 0 const hasVisible = searching ? (visibleIds?.size ?? 0) > 0 : categories.length > 0 return (
{ setQuery(e.target.value) if (!open) setOpen(true) }} onClick={() => { if (!disabled) setOpen(true) }} autoComplete="off" role="combobox" aria-expanded={open} aria-autocomplete="list" /> {value && !open && !disabled ? ( ) : null}
{open && !disabled ? (
{!hasVisible ? (

{t('categories.select.empty')}

) : ( )}
) : null}
) }