"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { categoryOptionLabel, type MeshkeeEntityCategory, } from "@/services/meshkee-catalog"; import styles from "./CatalogCategorySelect.module.css"; type TreeNode = MeshkeeEntityCategory & { children: TreeNode[] }; function buildTree(categories: MeshkeeEntityCategory[]): TreeNode[] { const byId = new Map(); for (const category of categories) { byId.set(category.id, { ...category, children: [] }); } const roots: TreeNode[] = []; for (const node of byId.values()) { if (node.parentId && byId.has(node.parentId)) { byId.get(node.parentId)!.children.push(node); } else { roots.push(node); } } const sortNodes = (nodes: TreeNode[]) => { nodes.sort((a, b) => categoryOptionLabel(a).localeCompare(categoryOptionLabel(b), undefined, { sensitivity: "base", }), ); for (const node of nodes) sortNodes(node.children); }; sortNodes(roots); return roots; } function matchesQuery(category: MeshkeeEntityCategory, q: string) { if (!q) return true; const hay = [category.name, category.nameFa ?? "", category.description ?? ""] .join(" ") .toLowerCase(); return hay.includes(q); } function filterTree(nodes: TreeNode[], q: string): TreeNode[] { if (!q) return nodes; const result: TreeNode[] = []; for (const node of nodes) { const children = filterTree(node.children, q); if (matchesQuery(node, q) || children.length) { result.push({ ...node, children }); } } return result; } function collectExpandIds(nodes: TreeNode[], into: Set) { for (const node of nodes) { if (node.children.length) { into.add(node.id); collectExpandIds(node.children, into); } } } export default function CatalogCategorySelect({ categories, value, onChange, disabled = false, label = "Category", optional = false, placeholder = "Select category", }: { categories: MeshkeeEntityCategory[]; value: string; onChange: (id: string) => void; disabled?: boolean; label?: string; optional?: boolean; placeholder?: string; }) { const rootRef = useRef(null); const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const [expanded, setExpanded] = useState>(() => new Set()); const tree = useMemo(() => buildTree(categories), [categories]); const q = query.trim().toLowerCase(); const visibleTree = useMemo(() => filterTree(tree, q), [tree, q]); const selected = useMemo( () => categories.find((item) => item.id === value) ?? null, [categories, value], ); useEffect(() => { if (!q) return; const next = new Set(); collectExpandIds(visibleTree, next); setExpanded(next); }, [q, visibleTree]); useEffect(() => { if (!open) return; function onPointerDown(event: MouseEvent) { if (!rootRef.current?.contains(event.target as Node)) { setOpen(false); setQuery(""); } } function onKey(event: KeyboardEvent) { if (event.key === "Escape") { setOpen(false); setQuery(""); } } document.addEventListener("mousedown", onPointerDown); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onPointerDown); document.removeEventListener("keydown", onKey); }; }, [open]); function select(id: string) { onChange(id); setOpen(false); setQuery(""); } function toggleExpand(id: string) { setExpanded((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } function renderNodes(nodes: TreeNode[], depth: number) { return nodes.map((node) => { const hasChildren = node.children.length > 0; const isExpanded = q ? true : expanded.has(node.id); const active = value === node.id; return (
  • {hasChildren ? ( ) : ( 0 ? ` ${styles.leafMark}` : ""}`} aria-hidden /> )}
    {hasChildren && isExpanded ? (
      {renderNodes(node.children, depth + 1)}
    ) : null}
  • ); }); } return (
    {label} {optional ? ( (optional) ) : null}
    { setOpen(true); setQuery(""); }} onChange={(event) => { setQuery(event.target.value); setOpen(true); }} onClick={() => setOpen(true)} />
    {open ? (
    {optional ? ( ) : null} {visibleTree.length ? (
      {renderNodes(visibleTree, 0)}
    ) : (

    No categories found

    )}
    ) : null}
    ); }