'use client' import React, { useState, useMemo, useRef, useCallback } from 'react' import { ChevronRight, ChevronDown, Folder, FolderOpen, Check, Search, } from 'lucide-react' import { Notebook } from '@/lib/types' import { motion, AnimatePresence } from 'motion/react' import { createPortal } from 'react-dom' interface HierarchicalNotebookSelectorProps { notebooks: { id: string; name: string; icon?: string | null; parentId?: string | null; trashedAt?: Date | string | null }[] selectedId: string | null onSelect: (id: string) => void className?: string placeholder?: string dropUp?: boolean size?: 'default' | 'sm' } export function HierarchicalNotebookSelector({ notebooks, selectedId, onSelect, className = '', placeholder = 'Select a notebook...', dropUp = false, size = 'default', }: HierarchicalNotebookSelectorProps) { const [isOpen, setIsOpen] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [expandedIds, setExpandedIds] = useState>(new Set()) const triggerRef = useRef(null) const getDropdownStyle = useCallback((): React.CSSProperties => { if (!triggerRef.current) return { position: 'fixed', top: 0, left: 0, width: 280, zIndex: 9999 } const rect = triggerRef.current.getBoundingClientRect() const dropdownHeight = 350 const viewportHeight = window.innerHeight const wouldOverflowBottom = rect.bottom + 8 + dropdownHeight > viewportHeight if (dropUp || wouldOverflowBottom) { return { position: 'fixed', bottom: window.innerHeight - rect.top + 8, left: rect.left, width: Math.max(rect.width, 280), zIndex: 9999, } } return { position: 'fixed', top: rect.bottom + 8, left: rect.left, width: Math.max(rect.width, 280), zIndex: 9999, } }, [dropUp]) const selectedNotebook = notebooks.find(nb => nb.id === selectedId) const path = useMemo(() => { if (!selectedNotebook) return [] const trail: typeof notebooks = [] let current: typeof notebooks[0] | undefined = selectedNotebook while (current) { trail.unshift(current) if (!current.parentId) break const parent = notebooks.find(nb => nb.id === current!.parentId) if (!parent) break current = parent } return trail }, [selectedNotebook, notebooks]) const toggleExpand = (e: React.MouseEvent, id: string) => { e.stopPropagation() const next = new Set(expandedIds) if (next.has(id)) next.delete(id) else next.add(id) setExpandedIds(next) } const renderTree = (parentId: string | null | undefined, level = 0): React.ReactNode => { const children = notebooks.filter( nb => (nb.parentId ?? null) === (parentId ?? null) ) if (children.length === 0) return null return (
0 ? 'ml-4 border-l border-border/40 pl-2' : ''}> {children.map(notebook => { const isExpanded = expandedIds.has(notebook.id) || searchQuery.length > 0 const hasChildren = notebooks.some(nb => nb.parentId === notebook.id) const isSelected = selectedId === notebook.id if (searchQuery && !notebook.name.toLowerCase().includes(searchQuery.toLowerCase())) { const hasMatchingChild = (id: string): boolean => { const kids = notebooks.filter(nb => nb.parentId === id) return kids.some(nb => nb.name.toLowerCase().includes(searchQuery.toLowerCase()) || hasMatchingChild(nb.id)) } if (!hasMatchingChild(notebook.id)) return null } return (
{ onSelect(notebook.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-brand-accent/15 text-brand-accent font-bold dark:bg-brand-accent/20' : 'hover:bg-muted dark:hover:bg-white/5 text-ink'}`} >
{hasChildren ? ( ) : null}
{isExpanded && hasChildren ? : }
{notebook.name} {isSelected && }
{isExpanded && ( {renderTree(notebook.id, level + 1)} )}
) })}
) } return (
setIsOpen(prev => !prev)} className={`w-full bg-card dark:bg-white/5 border border-border/80 rounded-xl outline-none focus:ring-4 ring-brand-accent/10 focus:border-brand-accent/40 transition-all cursor-pointer text-ink flex items-center gap-3 ${size === 'sm' ? 'px-3 py-2 text-xs' : 'px-4 py-3 text-sm'}`} >
{path.length > 0 ? (
{path.map((item, i) => ( {i > 0 && /} {item.name} ))}
) : ( {placeholder} )}
{isOpen && typeof window !== 'undefined' && createPortal( <>
setIsOpen(false)} />
setSearchQuery(e.target.value)} placeholder={placeholder} className="w-full bg-card border border-border rounded-lg pl-9 pr-4 py-2 text-xs outline-none focus:border-brand-accent transition-colors" />
{renderTree(null)}
{notebooks.length} notebooks
, document.body )}
) }