feat: add search/fetch.ts
This commit is contained in:
192
static/search/fetch.ts
Normal file
192
static/search/fetch.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
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,
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user