/** * SearchPage component - Main search interface for PodTUI */ import { createSignal, Show } from "solid-js" import { useKeyboard } from "@opentui/solid" import { useSearchStore } from "../stores/search" import { SearchResults } from "./SearchResults" import { SearchHistory } from "./SearchHistory" import type { SearchResult } from "../types/source" type SearchPageProps = { focused: boolean onSubscribe?: (result: SearchResult) => void onInputFocusChange?: (focused: boolean) => void onExit?: () => void } type FocusArea = "input" | "results" | "history" export function SearchPage(props: SearchPageProps) { const searchStore = useSearchStore() const [focusArea, setFocusArea] = createSignal("input") const [inputValue, setInputValue] = createSignal("") const [resultIndex, setResultIndex] = createSignal(0) const [historyIndex, setHistoryIndex] = createSignal(0) const handleSearch = async () => { const query = inputValue().trim() if (query) { await searchStore.search(query) if (searchStore.results().length > 0) { setFocusArea("results") setResultIndex(0) props.onInputFocusChange?.(false) } } if (props.focused && focusArea() === "input") { props.onInputFocusChange?.(true) } } const handleHistorySelect = async (query: string) => { setInputValue(query) await searchStore.search(query) if (searchStore.results().length > 0) { setFocusArea("results") setResultIndex(0) } } const handleResultSelect = (result: SearchResult) => { props.onSubscribe?.(result) searchStore.markSubscribed(result.podcast.id) } // Keyboard navigation useKeyboard((key) => { if (!props.focused) return const area = focusArea() // Enter to search from input if (key.name === "enter" && area === "input") { handleSearch() return } // Tab to cycle focus areas if (key.name === "tab" && !key.shift) { if (area === "input") { if (searchStore.results().length > 0) { setFocusArea("results") props.onInputFocusChange?.(false) } else if (searchStore.history().length > 0) { setFocusArea("history") props.onInputFocusChange?.(false) } } else if (area === "results") { if (searchStore.history().length > 0) { setFocusArea("history") } else { setFocusArea("input") props.onInputFocusChange?.(true) } } else { setFocusArea("input") props.onInputFocusChange?.(true) } return } if (key.name === "tab" && key.shift) { if (area === "input") { if (searchStore.history().length > 0) { setFocusArea("history") props.onInputFocusChange?.(false) } else if (searchStore.results().length > 0) { setFocusArea("results") props.onInputFocusChange?.(false) } } else if (area === "history") { if (searchStore.results().length > 0) { setFocusArea("results") } else { setFocusArea("input") props.onInputFocusChange?.(true) } } else { setFocusArea("input") props.onInputFocusChange?.(true) } return } // Up/Down for results and history if (area === "results") { const results = searchStore.results() if (key.name === "down" || key.name === "j") { setResultIndex((i) => Math.min(i + 1, results.length - 1)) return } if (key.name === "up" || key.name === "k") { setResultIndex((i) => Math.max(i - 1, 0)) return } if (key.name === "enter") { const result = results[resultIndex()] if (result) handleResultSelect(result) return } } if (area === "history") { const history = searchStore.history() if (key.name === "down" || key.name === "j") { setHistoryIndex((i) => Math.min(i + 1, history.length - 1)) return } if (key.name === "up" || key.name === "k") { setHistoryIndex((i) => Math.max(i - 1, 0)) return } if (key.name === "enter") { const query = history[historyIndex()] if (query) handleHistorySelect(query) return } } // Escape goes back to input or up one level if (key.name === "escape") { if (area === "input") { props.onExit?.() } else { setFocusArea("input") props.onInputFocusChange?.(true) } return } // "/" focuses search input if (key.name === "/" && area !== "input") { setFocusArea("input") props.onInputFocusChange?.(true) return } }) return ( {/* Search Header */} Search Podcasts {/* Search Input */} Search: { setInputValue(value) if (props.focused && focusArea() === "input") { props.onInputFocusChange?.(true) } }} placeholder="Enter podcast name, topic, or author..." focused={props.focused && focusArea() === "input"} width={50} /> [Enter] Search {/* Status */} Searching... {searchStore.error()} {/* Main Content - Results or History */} {/* Results Panel */} Results ({searchStore.results().length}) 0} fallback={ {searchStore.query() ? "No results found" : "Enter a search term to find podcasts"} } > {/* History Sidebar */} History {/* Footer Hints */} [Tab] Switch focus [/] Focus search [Enter] Select [Esc] Up ) }