import { useState, useEffect, useRef, useCallback } from "react" import { cn } from "@/lib/utils" import { Input } from "@/components/ui/input" import { Search, X, Check, Loader2, ChevronLeft, ChevronRight } from "lucide-react" import { searchStocks, StockResult } from "@/lib/api" interface StockSearchProps { onSelect: (stock: StockResult) => void disabled?: boolean selectedStock?: StockResult | null onClear?: () => void onSearchChange?: (isSearching: boolean) => void } export function StockSearch({ onSelect, disabled = false, selectedStock, onClear, onSearchChange, }: StockSearchProps) { const [query, setQuery] = useState("") const [results, setResults] = useState([]) const [isOpen, setIsOpen] = useState(false) const [isLoading, setIsLoading] = useState(false) const [highlightedIndex, setHighlightedIndex] = useState(0) const inputRef = useRef(null) const listRef = useRef(null) const debounceRef = useRef() const nameScrollRef = useRef(null) const scrollName = (direction: 'left' | 'right') => { if (nameScrollRef.current) { const scrollAmount = direction === 'left' ? -80 : 80 nameScrollRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' }) } } // Debounced search const performSearch = useCallback(async (searchQuery: string) => { if (searchQuery.length < 1) { setResults([]) setIsOpen(false) return } setIsLoading(true) try { const response = await searchStocks(searchQuery) setResults(response.results) setIsOpen(response.results.length > 0) setHighlightedIndex(0) } catch (error) { console.error("Stock search error:", error) setResults([]) } finally { setIsLoading(false) } }, []) useEffect(() => { if (debounceRef.current) { clearTimeout(debounceRef.current) } debounceRef.current = setTimeout(() => { performSearch(query) }, 150) // 150ms debounce return () => { if (debounceRef.current) { clearTimeout(debounceRef.current) } } }, [query, performSearch]) // Notify parent when search state changes useEffect(() => { onSearchChange?.(query.length > 0) }, [query, onSearchChange]) // Keyboard navigation const handleKeyDown = (e: React.KeyboardEvent) => { if (!isOpen) return switch (e.key) { case "ArrowDown": e.preventDefault() setHighlightedIndex((prev) => prev < results.length - 1 ? prev + 1 : prev ) break case "ArrowUp": e.preventDefault() setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : 0)) break case "Enter": e.preventDefault() if (results[highlightedIndex]) { handleSelect(results[highlightedIndex]) } break case "Escape": e.preventDefault() setIsOpen(false) inputRef.current?.blur() break } } const handleSelect = (stock: StockResult) => { onSelect(stock) setQuery("") setIsOpen(false) setResults([]) } const handleClear = () => { setQuery("") setResults([]) setIsOpen(false) onClear?.() inputRef.current?.focus() } // Close on click outside useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if ( listRef.current && !listRef.current.contains(e.target as Node) && inputRef.current && !inputRef.current.contains(e.target as Node) ) { setIsOpen(false) } } document.addEventListener("mousedown", handleClickOutside) return () => document.removeEventListener("mousedown", handleClickOutside) }, []) // Show selected stock if (selectedStock) { return (
{selectedStock.name} ({selectedStock.symbol})
{selectedStock.exchange} {!disabled && ( )}
) } return (
setQuery(e.target.value)} onKeyDown={handleKeyDown} onFocus={() => query.length > 0 && results.length > 0 && setIsOpen(true)} placeholder="Search U.S. listed companies..." disabled={disabled} className="pl-10 pr-10 bg-background border-input text-foreground focus:border-primary" /> {isLoading && ( )} {!isLoading && query && ( )}
{/* Dropdown */} {isOpen && results.length > 0 && (
{results.map((stock, index) => ( ))}
↑↓ navigate · Enter select · Esc close
)}
) } export default StockSearch