import React, { useState, useMemo } from 'react'; import { ChevronRight, ChevronDown, Folder, FolderOpen, Check, Search } from 'lucide-react'; import { Carnet } from '../types'; import { motion, AnimatePresence } from 'motion/react'; interface HierarchicalCarnetSelectorProps { carnets: Carnet[]; selectedId: string | null; onSelect: (id: string) => void; className?: string; placeholder?: string; } export const HierarchicalCarnetSelector: React.FC = ({ carnets, selectedId, onSelect, className = "", placeholder = "Sélectionner un carnet..." }) => { const [isOpen, setIsOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [expandedIds, setExpandedIds] = useState>(new Set(['1', '4'])); // Default expand some const selectedCarnet = carnets.find(c => c.id === selectedId); // Derive the path for display const path = useMemo(() => { if (!selectedCarnet) return []; const trail: Carnet[] = []; let current = selectedCarnet; while (current) { trail.unshift(current); if (!current.parentId) break; const parent = carnets.find(c => c.id === current.parentId); if (!parent) break; current = parent; } return trail; }, [selectedCarnet, carnets]); const toggleExpand = (e: React.MouseEvent, id: string) => { e.stopPropagation(); const newExpanded = new Set(expandedIds); if (newExpanded.has(id)) { newExpanded.delete(id); } else { newExpanded.add(id); } setExpandedIds(newExpanded); }; const filteredCarnets = useMemo(() => { if (!searchQuery) return carnets; return carnets.filter(c => c.name.toLowerCase().includes(searchQuery.toLowerCase())); }, [carnets, searchQuery]); const renderTree = (parentId?: string, level = 0) => { const children = carnets.filter(c => c.parentId === parentId); if (children.length === 0) return null; return (
0 ? "ml-4 border-l border-border/40 pl-2" : ""}> {children.map(carnet => { const isExpanded = expandedIds.has(carnet.id) || searchQuery.length > 0; const hasChildren = carnets.some(c => c.parentId === carnet.id); const isSelected = selectedId === carnet.id; // If searching and this carnet doesn't match AND none of its children match, skip it if (searchQuery && !carnet.name.toLowerCase().includes(searchQuery.toLowerCase())) { const hasMatchingChild = (id: string): boolean => { const childrenOfId = carnets.filter(c => c.parentId === id); return childrenOfId.some(c => c.name.toLowerCase().includes(searchQuery.toLowerCase()) || hasMatchingChild(c.id)); }; if (!hasMatchingChild(carnet.id)) return null; } return (
{ onSelect(carnet.id); if (!searchQuery) setIsOpen(false); }} className={`flex items-center gap-2.5 px-2 py-1.5 rounded-lg cursor-pointer transition-all group ${isSelected ? 'bg-blueprint/10 text-blueprint font-bold' : 'hover:bg-slate-50 dark:hover:bg-white/5 text-ink'}`} >
{hasChildren ? ( ) : null}
{isExpanded && hasChildren ? : }
{carnet.name} {isSelected && }
{isExpanded && ( {renderTree(carnet.id, level + 1)} )}
); })}
); }; return (
setIsOpen(!isOpen)} className="w-full bg-slate-50 dark:bg-black/20 border border-border/80 rounded-xl px-4 py-4 text-sm outline-none focus:ring-4 ring-blueprint/5 focus:border-blueprint/40 transition-all cursor-pointer text-ink flex items-center gap-3" >
{path.length > 0 ? (
{path.map((item, i) => ( {i > 0 && /} {item.name} ))}
) : ( {placeholder} )}
{isOpen && ( <>
setIsOpen(false)} />
setSearchQuery(e.target.value)} placeholder="Filtrer les carnets..." className="w-full bg-white border border-border rounded-lg pl-9 pr-4 py-2 text-xs outline-none focus:border-blueprint transition-colors" />
{renderTree(undefined)}
Structure des carnets
)}
); };