200 lines
6.1 KiB
TypeScript
200 lines
6.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import TreeNode from './TreeNode';
|
|
import { taxonomy, getDomainSubcodeCount, getSubcodeCount } from '@/lib/taxonomy/data';
|
|
import type { SelectedSubcode } from '@/lib/taxonomy/types';
|
|
|
|
interface TaxonomyTreeProps {
|
|
searchQuery: string;
|
|
searchResults: {
|
|
domains: string[];
|
|
categories: string[];
|
|
subcodes: string[];
|
|
};
|
|
selectedSubcode: SelectedSubcode | null;
|
|
onSelectSubcode: (subcode: SelectedSubcode | null) => void;
|
|
}
|
|
|
|
export default function TaxonomyTree({
|
|
searchQuery,
|
|
searchResults,
|
|
selectedSubcode,
|
|
onSelectSubcode,
|
|
}: TaxonomyTreeProps) {
|
|
const [expandedDomains, setExpandedDomains] = useState<Set<string>>(new Set());
|
|
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
|
|
|
|
// Auto-expand nodes when search results change
|
|
useEffect(() => {
|
|
if (searchQuery) {
|
|
const domainsToExpand = new Set<string>();
|
|
const categoriesToExpand = new Set<string>();
|
|
|
|
// Expand domains that have matches
|
|
searchResults.domains.forEach((d) => domainsToExpand.add(d));
|
|
|
|
// Expand parent domains for matched categories
|
|
searchResults.categories.forEach((c) => {
|
|
domainsToExpand.add(c[0]);
|
|
categoriesToExpand.add(c);
|
|
});
|
|
|
|
// Expand parent domains and categories for matched subcodes
|
|
searchResults.subcodes.forEach((s) => {
|
|
const domainKey = s[0];
|
|
const categoryKey = s.slice(0, 2);
|
|
domainsToExpand.add(domainKey);
|
|
categoriesToExpand.add(categoryKey);
|
|
});
|
|
|
|
setExpandedDomains(domainsToExpand);
|
|
setExpandedCategories(categoriesToExpand);
|
|
}
|
|
}, [searchQuery, searchResults]);
|
|
|
|
const toggleDomain = useCallback((domainKey: string) => {
|
|
setExpandedDomains((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(domainKey)) {
|
|
next.delete(domainKey);
|
|
} else {
|
|
next.add(domainKey);
|
|
}
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const toggleCategory = useCallback((categoryKey: string) => {
|
|
setExpandedCategories((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(categoryKey)) {
|
|
next.delete(categoryKey);
|
|
} else {
|
|
next.add(categoryKey);
|
|
}
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const handleSelectSubcode = (
|
|
subcodeKey: string,
|
|
domainKey: string,
|
|
domainName: string,
|
|
categoryKey: string,
|
|
categoryName: string,
|
|
subcode: SelectedSubcode['subcode']
|
|
) => {
|
|
onSelectSubcode({
|
|
code: subcodeKey,
|
|
domainKey,
|
|
domainName,
|
|
categoryKey,
|
|
categoryName,
|
|
subcode,
|
|
});
|
|
};
|
|
|
|
const isSubcodeSelected = (subcodeKey: string) => {
|
|
return selectedSubcode?.code === subcodeKey;
|
|
};
|
|
|
|
const isSearchMatch = (key: string) => {
|
|
if (!searchQuery) return false;
|
|
return (
|
|
searchResults.domains.includes(key) ||
|
|
searchResults.categories.includes(key) ||
|
|
searchResults.subcodes.includes(key)
|
|
);
|
|
};
|
|
|
|
// Filter to show only matches when searching
|
|
const shouldShowDomain = (domainKey: string) => {
|
|
if (!searchQuery) return true;
|
|
// Show domain if it matches or any of its children match
|
|
if (searchResults.domains.includes(domainKey)) return true;
|
|
if (searchResults.categories.some((c) => c.startsWith(domainKey))) return true;
|
|
if (searchResults.subcodes.some((s) => s.startsWith(domainKey))) return true;
|
|
return false;
|
|
};
|
|
|
|
const shouldShowCategory = (categoryKey: string) => {
|
|
if (!searchQuery) return true;
|
|
if (searchResults.categories.includes(categoryKey)) return true;
|
|
if (searchResults.subcodes.some((s) => s.startsWith(categoryKey))) return true;
|
|
return false;
|
|
};
|
|
|
|
const shouldShowSubcode = (subcodeKey: string) => {
|
|
if (!searchQuery) return true;
|
|
return searchResults.subcodes.includes(subcodeKey);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col gap-0.5">
|
|
{Object.entries(taxonomy.domains).map(([domainKey, domain]) => {
|
|
if (!shouldShowDomain(domainKey)) return null;
|
|
|
|
return (
|
|
<TreeNode
|
|
key={domainKey}
|
|
code={domainKey}
|
|
name={domain.name}
|
|
count={getDomainSubcodeCount(domainKey)}
|
|
isExpanded={expandedDomains.has(domainKey)}
|
|
level="domain"
|
|
domainKey={domainKey}
|
|
onToggle={() => toggleDomain(domainKey)}
|
|
searchMatch={isSearchMatch(domainKey)}
|
|
>
|
|
{Object.entries(domain.categories).map(([categoryKey, category]) => {
|
|
if (!shouldShowCategory(categoryKey)) return null;
|
|
|
|
return (
|
|
<TreeNode
|
|
key={categoryKey}
|
|
code={categoryKey}
|
|
name={category.name}
|
|
count={getSubcodeCount(categoryKey)}
|
|
isExpanded={expandedCategories.has(categoryKey)}
|
|
level="category"
|
|
domainKey={domainKey}
|
|
onToggle={() => toggleCategory(categoryKey)}
|
|
searchMatch={isSearchMatch(categoryKey)}
|
|
>
|
|
{Object.entries(category.subcodes).map(([subcodeKey, subcode]) => {
|
|
if (!shouldShowSubcode(subcodeKey)) return null;
|
|
|
|
return (
|
|
<TreeNode
|
|
key={subcodeKey}
|
|
code={subcodeKey}
|
|
name={subcode.name}
|
|
isLeaf
|
|
isSelected={isSubcodeSelected(subcodeKey)}
|
|
level="subcode"
|
|
domainKey={domainKey}
|
|
onClick={() =>
|
|
handleSelectSubcode(
|
|
subcodeKey,
|
|
domainKey,
|
|
domain.name,
|
|
categoryKey,
|
|
category.name,
|
|
subcode
|
|
)
|
|
}
|
|
searchMatch={isSearchMatch(subcodeKey)}
|
|
/>
|
|
);
|
|
})}
|
|
</TreeNode>
|
|
);
|
|
})}
|
|
</TreeNode>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|