'use client'; /* ════════════════════════════ SearchBar ════════════════════════════ * * Reusable search bar met debounce, resultaten dropdown, en toetsenbordnavigatie. * Gebruikt de client-side search index (search-index.ts) of een custom onSearch callback. */ import { useState, useRef, useEffect, useCallback } from 'react'; import { Search, X, FileIcon, FolderIcon, Loader2 } from 'lucide-react'; import { searchIndex, type SearchResult } from '@/lib/search-index'; /* ════════════════════════════ Types ════════════════════════════ */ export interface SearchBarProps { /** External search handler (optioneel — gebruikt search-index.ts anders) */ onSearch?: (query: string) => Promise | SearchResult[]; /** Called when user selects a result */ onSelect: (result: SearchResult) => void; /** Placeholder text */ placeholder?: string; /** Auto-focus on mount */ autoFocus?: boolean; /** Extra CSS classes */ className?: string; } /* ════════════════════════════ Component ════════════════════════════ */ export default function SearchBar({ onSearch, onSelect, placeholder = 'Search files, CIDs, and content…', autoFocus = false, className = '', }: SearchBarProps) { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [showResults, setShowResults] = useState(false); const [selectedIdx, setSelectedIdx] = useState(-1); const inputRef = useRef(null); const debounceRef = useRef | null>(null); const panelRef = useRef(null); /* ── Search logic ── */ const doSearch = useCallback(async (q: string) => { if (!q || q.trim().length < 2) { setResults([]); setShowResults(false); return; } setLoading(true); try { const hits = onSearch ? await onSearch(q) : searchIndex(q); setResults(hits); setShowResults(true); setSelectedIdx(-1); } finally { setLoading(false); } }, [onSearch]); /* ── Debounced input ── */ function handleChange(value: string) { setQuery(value); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => doSearch(value), 250); } /* ── Keyboard navigation ── */ function handleKeyDown(e: React.KeyboardEvent) { if (!showResults || results.length === 0) return; switch (e.key) { case 'ArrowDown': e.preventDefault(); setSelectedIdx((prev) => Math.min(prev + 1, results.length - 1)); break; case 'ArrowUp': e.preventDefault(); setSelectedIdx((prev) => Math.max(prev - 1, 0)); break; case 'Enter': e.preventDefault(); if (selectedIdx >= 0 && selectedIdx < results.length) { handleSelect(results[selectedIdx]); } break; case 'Escape': setShowResults(false); inputRef.current?.blur(); break; } } /* ── Select ── */ function handleSelect(result: SearchResult) { setShowResults(false); setQuery(''); onSelect(result); } /* ── Click outside ── */ useEffect(() => { function handleClick(e: MouseEvent) { if (panelRef.current && !panelRef.current.contains(e.target as Node)) { setShowResults(false); } } document.addEventListener('mousedown', handleClick); return () => document.removeEventListener('mousedown', handleClick); }, []); /* ── Cleanup ── */ useEffect(() => { if (autoFocus) inputRef.current?.focus(); return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); /* ════════════════════════════ Render ════════════════════════════ */ return (
{/* Input */}
handleChange(e.target.value)} onKeyDown={handleKeyDown} onFocus={() => { if (results.length > 0) setShowResults(true); }} placeholder={placeholder} className="w-full pl-10 pr-10 py-2.5 rounded-xl bg-surface-900 border border-surface-700 text-sm text-surface-200 placeholder:text-surface-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/50 focus-visible:border-brand-500 transition-colors" autoFocus={autoFocus} role="combobox" aria-expanded={showResults} aria-haspopup="listbox" aria-autocomplete="list" aria-controls="search-results-listbox" /> {loading && ( )} {!loading && query && ( )}
{/* Live region for screen readers */}
{showResults ? `${results.length} resultaten` : ''}
{/* Results dropdown */} {showResults && (
{results.length === 0 && !loading ? (
No results found
) : ( results.map((result, idx) => ( )) )}
)}
); }