import type { CommandPaletteItem, CommandPaletteResponse } from "./state"; import { state, searchInput, searchResults, responseCache } from "./state"; import { setClearButtonState, clearResults, renderEmptyState, renderItems, appendItems, } from "./render"; const parseCommandPaletteResponse = (payload: unknown): CommandPaletteResponse => { if (Array.isArray(payload)) { return { items: payload as CommandPaletteItem[], hasNextPage: false }; } if ( payload && typeof payload === "object" && Array.isArray((payload as CommandPaletteResponse).items) ) { const response = payload as CommandPaletteResponse; return { items: response.items, hasNextPage: response.hasNextPage, nextPage: response.nextPage, }; } return { items: [], hasNextPage: false }; }; const visibleSearchItems = (items: CommandPaletteItem[], query: string): CommandPaletteItem[] => { if (query === "") { return []; } return items.filter((item) => item.type === "anime"); }; const renderPendingQuery = (query: string): void => { if (!query) { return; } clearResults(); }; export const cancelScheduledFetch = (): void => { if (!state.fetchTimeout) { return; } window.clearTimeout(state.fetchTimeout); state.fetchTimeout = undefined; }; export const fetchSearchItems = (query: string): void => { state.lastQuery = query; setClearButtonState(query !== ""); if (state.activeRequestController) { state.activeRequestController.abort(); state.activeRequestController = undefined; } if (query === "") { clearResults(); renderEmptyState(""); return; } const cached = responseCache.get(query); if (cached) { state.nextSearchPage = cached.nextPage; state.hasNextSearchPage = cached.hasNextPage; renderItems(visibleSearchItems(cached.items, query)); return; } renderPendingQuery(query); const controller = new AbortController(); state.activeRequestController = controller; fetch("/api/command-palette?q=" + encodeURIComponent(query), { signal: controller.signal }) .then((res: Response) => { if (!res.ok) { return { items: [], hasNextPage: false }; } return res.json(); }) .then((payload: unknown) => { if (controller.signal.aborted || query !== state.lastQuery) { return; } const response = parseCommandPaletteResponse(payload); const visibleItems = visibleSearchItems(response.items, query); state.activeRequestController = undefined; state.nextSearchPage = response.nextPage; state.hasNextSearchPage = response.hasNextPage; responseCache.set(query, response); renderItems(visibleItems); }) .catch((err: unknown) => { if (controller.signal.aborted) { return; } state.activeRequestController = undefined; console.error("Search overlay error:", err); renderItems([]); }); }; export const fetchNextSearchPage = (): void => { if ( !state.lastQuery || !state.hasNextSearchPage || !state.nextSearchPage || state.isFetchingNextPage ) { return; } state.isFetchingNextPage = true; const query = state.lastQuery; const page = state.nextSearchPage; fetch( "/api/command-palette?q=" + encodeURIComponent(query) + "&page=" + encodeURIComponent(String(page)), ) .then((res: Response) => { if (!res.ok) { return { items: [], hasNextPage: false }; } return res.json(); }) .then((payload: unknown) => { if (query !== state.lastQuery) { return; } const response = parseCommandPaletteResponse(payload); const visibleItems = visibleSearchItems(response.items, query); const cached = responseCache.get(query); if (cached) { responseCache.set(query, { items: [...cached.items, ...response.items], hasNextPage: response.hasNextPage, nextPage: response.nextPage, }); } state.nextSearchPage = response.nextPage; state.hasNextSearchPage = response.hasNextPage; appendItems(visibleItems); }) .catch((err: unknown) => { console.error("Search overlay pagination error:", err); }) .finally(() => { state.isFetchingNextPage = false; }); }; export const onResultsScroll = (): void => { if (!searchResults) { return; } const remainingScroll = searchResults.scrollHeight - searchResults.scrollTop - searchResults.clientHeight; if (remainingScroll < 480) { fetchNextSearchPage(); } }; export const scheduleFetch = (): void => { if (state.fetchTimeout) { window.clearTimeout(state.fetchTimeout); } const query = searchInput?.value.trim() || ""; setClearButtonState(query !== ""); state.fetchTimeout = window.setTimeout( () => fetchSearchItems(query), query.length >= 2 ? 240 : 80, ); };