let searchInputTimeout: ReturnType | null = null; const SEARCH_DEBOUNCE_MS = 300; const SEARCH_MIN_CHARS = 2; function initializeSearch(): void { const searchInput = document.getElementById( "header-search", ) as HTMLInputElement | null; if (!searchInput) { console.warn("Search input not found"); return; } searchInput.addEventListener("input", handleSearchInput); searchInput.addEventListener("keydown", handleSearchKeydown); searchInput.addEventListener("focus", () => { if (searchInput.value.length >= SEARCH_MIN_CHARS) { performSearch(searchInput.value); } }); document.addEventListener("click", (e: MouseEvent) => { const searchResults = document.getElementById("search-results"); const searchInputEl = document.getElementById("header-search"); if ( searchResults && !searchResults.contains(e.target as Node) && e.target !== searchInputEl ) { hideSearchResults(); } }); } function handleSearchInput(e: Event): void { const target = e.target as HTMLInputElement; const query = target.value.trim(); if (searchInputTimeout) { clearTimeout(searchInputTimeout); } if (query.length < SEARCH_MIN_CHARS) { hideSearchResults(); return; } searchInputTimeout = setTimeout(() => { performSearch(query); }, SEARCH_DEBOUNCE_MS); } function handleSearchKeydown(e: KeyboardEvent): void { const searchResults = document.getElementById("search-results"); if (!searchResults || searchResults.classList.contains("hidden")) { return; } const items = searchResults.querySelectorAll(".search-result-item"); const currentIndex = parseInt(searchResults.dataset.selectedIndex || "-1"); if (e.key === "ArrowDown") { e.preventDefault(); const nextIndex = Math.min(currentIndex + 1, items.length - 1); selectSearchResult(items, nextIndex); } else if (e.key === "ArrowUp") { e.preventDefault(); const prevIndex = Math.max(currentIndex - 1, -1); selectSearchResult(items, prevIndex); } else if (e.key === "Enter") { e.preventDefault(); if (currentIndex >= 0 && items[currentIndex]) { const link = items[currentIndex].querySelector("a"); if (link) link.click(); } } else if (e.key === "Escape") { hideSearchResults(); } } function selectSearchResult(items: NodeListOf, index: number): void { items.forEach((item, i) => { if (i === index) { item.classList.add("bg-opacity-80"); } else { item.classList.remove("bg-opacity-80"); } }); const searchResults = document.getElementById("search-results"); if (searchResults) { searchResults.dataset.selectedIndex = index.toString(); } } function performSearch(query: string): void { const token = localStorage.getItem("token"); if (!token) { console.warn("No authentication token found"); return; } showSearchLoading(); fetch(`/api/media-items/search?q=${encodeURIComponent(query)}`, { headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }) .then((response) => { if (response.status === 404) { return { error: "no results found", results: [] }; } return response.json(); }) .then( ( data: | { error?: string; results?: MediaItemSummary[] } | MediaItemSummary[], ) => { hideSearchLoading(); if (data && "error" in data && data.error === "no results found") { showNoResults(query); } else if (Array.isArray(data) && data.length > 0) { showSearchResults(data, query); } else if (Array.isArray(data)) { showNoResults(query); } else { showNoResults(query); } }, ) .catch((error) => { hideSearchLoading(); console.error("Search error:", error); showSearchError(); }); } function showSearchLoading(): void { createSearchResultsContainer(); const searchResults = document.getElementById("search-results"); if (!searchResults) return; searchResults.innerHTML = `

Searching...

`; searchResults.classList.remove("hidden"); } function hideSearchLoading(): void {} function showSearchResults(results: MediaItemSummary[], query: string): void { createSearchResultsContainer(); const searchResults = document.getElementById("search-results"); if (!searchResults) return; searchResults.dataset.selectedIndex = "-1"; const libraryIconMap: Record = { ebooks: "📚", comics: "📖", manga: "🗾", }; let html = `

${results.length} result${results.length !== 1 ? "s" : ""} for "${searchEscapeHtml(query)}"

`; results.forEach((item, index) => { const icon = libraryIconMap[item.library_type_name] || "📁"; const titleHtml = highlightMatch(item.title, query); const authorHtml = item.author ? highlightMatch(item.author, query) : ""; html += ` `; }); html += `

Press ↑↓ to navigate, Enter to select

`; searchResults.innerHTML = html; searchResults.classList.remove("hidden"); } function showNoResults(query: string): void { createSearchResultsContainer(); const searchResults = document.getElementById("search-results"); if (!searchResults) return; searchResults.innerHTML = `
🔍

No results found for "${searchEscapeHtml(query)}"

Try different keywords

`; searchResults.classList.remove("hidden"); } function showSearchError(): void { createSearchResultsContainer(); const searchResults = document.getElementById("search-results"); if (!searchResults) return; searchResults.innerHTML = `
⚠️

Search error

Please try again

`; searchResults.classList.remove("hidden"); } function hideSearchResults(): void { const searchResults = document.getElementById("search-results"); if (searchResults) { searchResults.classList.add("hidden"); } } function createSearchResultsContainer(): void { let searchResults = document.getElementById("search-results"); if (!searchResults) { searchResults = document.createElement("div"); searchResults.id = "search-results"; searchResults.className = "hidden absolute z-50 w-full max-w-2xl mt-2 rounded-lg shadow-lg border"; searchResults.style.cssText = "background-color: var(--bg-secondary); border-color: var(--border)"; const searchInput = document.getElementById("header-search"); if (searchInput) { const searchContainer = searchInput.closest(".relative"); if (searchContainer) { searchContainer.appendChild(searchResults); } } } } function highlightMatch(text: string, query: string): string { if (!text) return ""; const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const regex = new RegExp(`(${escapedQuery})`, "gi"); return searchEscapeHtml(text).replace( regex, '$1', ); } function searchEscapeHtml(text: string): string { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } function selectLibraryAndBook(libraryId: string, bookId: string): void { localStorage.setItem("selectedLibrary", libraryId); localStorage.setItem("selectedBook", bookId); hideSearchResults(); } document.addEventListener("DOMContentLoaded", initializeSearch); (window as any).selectLibraryAndBook = selectLibraryAndBook;