import React, { useEffect, useState, useRef, KeyboardEvent } from 'react'; import { Search as SearchIcon } from 'lucide-react'; import { ArrowDown, ArrowUp, Close } from '../icons'; import debounce from 'lodash/debounce'; import { Button } from '../ui/button'; /** * Props for the SearchBar component */ interface SearchBarProps { /** Callback fired when search term or case sensitivity changes */ onSearch: (term: string, caseSensitive: boolean) => void; /** Callback fired when the search bar is closed */ onClose: () => void; /** Optional callback for navigating between search results */ onNavigate?: (direction: 'next' | 'prev') => void; /** Current search results state */ searchResults?: { count: number; currentIndex: number; }; /** Optional ref for the search input element */ inputRef?: React.RefObject; /** Initial search term */ initialSearchTerm?: string; /** Placeholder text for the search input */ placeholder?: string; } /** * SearchBar provides a search input with case-sensitive toggle and result navigation. */ export const SearchBar: React.FC = ({ onSearch, onClose, onNavigate, searchResults, inputRef: externalInputRef, initialSearchTerm = '', placeholder = 'Search conversation...', }: SearchBarProps) => { const [searchTerm, setSearchTerm] = useState(initialSearchTerm); const [caseSensitive, setCaseSensitive] = useState(false); const [isExiting, setIsExiting] = useState(false); const internalInputRef = React.useRef(null); const inputRef = externalInputRef || internalInputRef; const debouncedSearchRef = useRef | null>(null); // Create debounced search function useEffect(() => { const debouncedFn = debounce((term: string, caseSensitive: boolean) => { onSearch(term, caseSensitive); }, 200); debouncedSearchRef.current = debouncedFn; return () => { debouncedFn.cancel(); }; }, [onSearch]); useEffect(() => { inputRef.current?.focus(); }, [inputRef]); // Handle changes to initialSearchTerm useEffect(() => { if (initialSearchTerm) { setSearchTerm(initialSearchTerm); if (initialSearchTerm.length >= 2) { debouncedSearchRef.current?.(initialSearchTerm, caseSensitive); } } }, [initialSearchTerm, caseSensitive, debouncedSearchRef]); const [localSearchResults, setLocalSearchResults] = useState(undefined); // Sync external search results with local state useEffect(() => { // Only set results if we have a search term if (!searchTerm) { setLocalSearchResults(undefined); } else { setLocalSearchResults(searchResults); } }, [searchResults, searchTerm]); const handleSearch = (event: React.ChangeEvent) => { const value = event.target.value; // Always cancel pending searches first if (debouncedSearchRef.current) { debouncedSearchRef.current.cancel(); } // Update display term immediately for UI feedback setSearchTerm(value); // Only trigger search if we have 2 or more characters if (value.length >= 2) { debouncedSearchRef.current?.(value, caseSensitive); } else { // Clear results if less than 2 characters onSearch('', caseSensitive); } }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'ArrowUp') { handleNavigate('prev', event); } else if (event.key === 'ArrowDown' || event.key === 'Enter') { handleNavigate('next', event); } else if (event.key === 'Escape') { event.preventDefault(); handleClose(); } }; const handleNavigate = (direction: 'next' | 'prev', e?: React.MouseEvent | KeyboardEvent) => { e?.preventDefault(); if (searchResults && searchResults.count > 0) { inputRef.current?.focus(); onNavigate?.(direction); } }; const toggleCaseSensitive = () => { const newCaseSensitive = !caseSensitive; setCaseSensitive(newCaseSensitive); // Immediately trigger a new search with updated case sensitivity if (searchTerm) { debouncedSearchRef.current?.(searchTerm, newCaseSensitive); } inputRef.current?.focus(); }; const handleClose = () => { setIsExiting(true); debouncedSearchRef.current?.cancel(); // Cancel any pending searches setTimeout(() => { onClose(); }, 150); // Match animation duration }; const hasResults = searchResults && searchResults.count > 0; return (
{(() => { return localSearchResults?.count && localSearchResults.count > 0 && searchTerm ? `${localSearchResults.currentIndex}/${localSearchResults.count}` : null; })()}
); }; export default SearchBar;