import React, { useState, KeyboardEvent, useRef, useEffect, useCallback, FocusEvent } from 'react'; import { SearchIcon, ChevronDownIcon, GlobeIcon, TrendingIcon, HistoryIcon, TrashIcon } from './Icons'; import { SearchEngine } from '../types'; import { fetchSuggestions } from '../utils/suggestions'; import { useToast } from '../context/ToastContext'; import { useTranslation } from '../i18n'; interface SearchBoxProps { engines: SearchEngine[]; selectedEngineName: string; onSelectEngine: (name: string) => void; themeColor: string; opacity: number; onInteractionChange?: (isActive: boolean) => void; enableHistory: boolean; history: string[]; onUpdateHistory: (history: string[]) => void; } const SearchBox: React.FC = ({ engines, selectedEngineName, onSelectEngine, themeColor, opacity, onInteractionChange, enableHistory, history, onUpdateHistory }) => { const [query, setQuery] = useState(''); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [suggestions, setSuggestions] = useState([]); const [selectedIndex, setSelectedIndex] = useState(-1); const [showSuggestions, setShowSuggestions] = useState(false); const [isFocused, setIsFocused] = useState(false); const dropdownRef = useRef(null); const menuRef = useRef(null); const containerRef = useRef(null); const inputRef = useRef(null); const { showToast } = useToast(); const { t } = useTranslation(); const selectedEngine = React.useMemo(() => { return engines.find(e => e.name === selectedEngineName) || engines[0]; }, [engines, selectedEngineName]); // Determine if the search box should be in "Active/Expanded" mode const isActive = isFocused || query.length > 0 || isDropdownOpen; // Sync active state with parent useEffect(() => { onInteractionChange?.(isActive); }, [isActive, onInteractionChange]); // Handle outside clicks to close dropdowns and clear query useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as Node; const isClickInButton = dropdownRef.current && dropdownRef.current.contains(target); const isClickInMenu = menuRef.current && menuRef.current.contains(target); if (!isClickInButton && !isClickInMenu) { setIsDropdownOpen(false); } if (containerRef.current && !containerRef.current.contains(target)) { setShowSuggestions(false); setQuery(''); // Clear query when clicking outside to restore unfocused state } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // Fetch suggestions with debounce useEffect(() => { const timer = setTimeout(async () => { if (query.trim()) { const results = await fetchSuggestions(selectedEngine.name, query); setSuggestions(results.slice(0, 8)); setShowSuggestions(true); } else { setSuggestions([]); setShowSuggestions(false); } setSelectedIndex(-1); }, 200); return () => clearTimeout(timer); }, [query, selectedEngine.name]); const performSearch = useCallback((text: string) => { if (!text.trim()) return; // Record History if (enableHistory) { // Remove duplicate if exists, then add to front, limit to 20 const newHistory = [text, ...history.filter(h => h !== text)].slice(0, 20); onUpdateHistory(newHistory); } let url = selectedEngine.urlPattern; if (url.includes('%s')) { url = url.replace('%s', encodeURIComponent(text)); } else { url = `${url}${encodeURIComponent(text)}`; } // Security check: only allow http and https protocols try { const urlObj = new URL(url); if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') { console.error('Unsafe URL protocol:', urlObj.protocol); showToast(t.unsupportedProtocol, 'error'); return; } window.location.href = url; } catch (error) { console.error('Invalid URL:', url, error); showToast(t.invalidSearchUrl, 'error'); } }, [selectedEngine.urlPattern, showToast, enableHistory, history, onUpdateHistory, t]); const handleSearch = useCallback(() => performSearch(query), [performSearch, query]); const handleSuggestionClick = useCallback((suggestion: string) => { setQuery(suggestion); performSearch(suggestion); setShowSuggestions(false); }, [performSearch]); const handleClearHistory = useCallback((e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); onUpdateHistory([]); // Keep focus inputRef.current?.focus(); }, [onUpdateHistory]); const showHistoryDropdown = isFocused && !query && enableHistory && history.length > 0; // Calculate total items for keyboard navigation const visibleItems = showSuggestions && suggestions.length > 0 ? suggestions : (showHistoryDropdown ? history : []); const handleKeyDown = useCallback((e: KeyboardEvent) => { if (e.key === 'Enter') { if (selectedIndex >= 0 && visibleItems[selectedIndex]) { handleSuggestionClick(visibleItems[selectedIndex]); } else { handleSearch(); } (e.target as HTMLInputElement).blur(); } else if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex(prev => prev < visibleItems.length - 1 ? prev + 1 : 0 ); } else if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex(prev => prev > -1 ? prev - 1 : visibleItems.length - 1 ); } else if (e.key === 'Escape') { setShowSuggestions(false); (e.target as HTMLInputElement).blur(); } }, [selectedIndex, visibleItems, handleSuggestionClick, handleSearch]); const handleFocus = useCallback(() => { setIsFocused(true); if (suggestions.length > 0) setShowSuggestions(true); }, [suggestions.length]); const handleBlur = useCallback((e: FocusEvent) => { setIsFocused(false); // If focus moves outside the component (e.g. clicking background or tabbing out), clear the query if (containerRef.current && !containerRef.current.contains(e.relatedTarget as Node)) { setQuery(''); } }, []); // IMPORTANT: This handler stops the context menu event from bubbling up to the App component. // This prevents the right-click from triggering the dashboard switch when interacting with the search box. const handleElementContextMenu = useCallback((e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }, []); return (
{/* Enhanced Aperture/Glow Effect Layers This provides the requested "increased Gaussian blur effect" */} {/* 1. Large ambient blur (The outer aura) */}
{/* 2. Intense core glow (The ring) */}
{/* Search Input Container */}
{/* Engine Selector - Collapsible Area */}
{/* Vertical Separator */}
{/* Engine Dropdown Menu */} {isDropdownOpen && (
e.preventDefault()} > {engines.map((engine) => ( ))}
)} {/* Input Field */} setQuery(e.target.value)} onKeyDown={handleKeyDown} onFocus={handleFocus} onBlur={handleBlur} placeholder={isActive ? `${t.searchOn} ${selectedEngine.name}...` : t.search} className={` flex-1 w-full min-w-0 bg-transparent border-none outline-none text-base placeholder-white/40 font-light px-4 transition-all duration-500 ease-out ${isActive ? 'text-left' : 'text-center placeholder-white/70'} `} style={{ caretColor: themeColor }} /> {/* Search Button / Icon */}
{/* Suggestions / History Dropdown */} {((showSuggestions && suggestions.length > 0) || (showHistoryDropdown && history.length > 0)) && (
{/* Suggestions List */} {showSuggestions && suggestions.length > 0 && (
{suggestions.map((suggestion, index) => ( ))}
)} {/* History List */} {!showSuggestions && showHistoryDropdown && (
{t.recentSearches}
{history.map((item, index) => ( ))}
)}
)}
); }; export default SearchBox;