diff --git a/templates/custom_section.templ b/templates/custom_section.templ
index 45d3823f..6ef25626 100644
--- a/templates/custom_section.templ
+++ b/templates/custom_section.templ
@@ -136,7 +136,7 @@ templ CustomSectionBuilder(user User, libraries []LibraryData, errorMessage stri
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/web/src/custom-section-builder.ts b/web/src/custom-section-builder.ts
index 8b058e21..a96941f0 100644
--- a/web/src/custom-section-builder.ts
+++ b/web/src/custom-section-builder.ts
@@ -261,7 +261,7 @@ function initCustomSectionBuilder(): void {
// Delegated clicks for the generated picker/selection markup: the CSP
// build refuses inline onclick= handlers, so the rows carry data
// attributes and one listener per container survives innerHTML swaps.
- const resultsContainer = document.getElementById("search-results");
+ const resultsContainer = document.getElementById("cs-search-results");
resultsContainer?.addEventListener("click", (e) => {
const row = (e.target as HTMLElement).closest("[data-book-id]");
if (!row) return;
@@ -384,7 +384,11 @@ function onBookSearchInput(): void {
async function searchBooks(): Promise {
const searchInput = document.getElementById("book-search") as HTMLInputElement;
const librarySelect = document.getElementById("section-library") as HTMLSelectElement;
- const resultsContainer = document.getElementById("search-results") as HTMLElement;
+ // NOTE: cs-search-results, NOT search-results — the header search
+ // dropdown owns the latter on every page, and getElementById returns
+ // the first match in tree order (the picker's results were landing in
+ // the topbar dropdown, which stayed invisible).
+ const resultsContainer = document.getElementById("cs-search-results") as HTMLElement;
const query = searchInput?.value.trim();
const libraryId = librarySelect?.value;
@@ -395,22 +399,48 @@ async function searchBooks(): Promise {
}
try {
+ // The unified JSON search (same endpoint the bookshelf uses). The
+ // builder was wired to /api/books/search, a route that has never
+ // existed — every picker search 404'd. Rows come back with id/title/
+ // author/cover_image_path; map to the BookInfo shape the picker
+ // renders (conflict/badge metadata is attached server-side later).
const response = await fetch(
- `/api/books/search?q=${encodeURIComponent(query)}&library_id=${libraryId}`,
+ `/api/media-items/search?q=${encodeURIComponent(query)}&library_id=${libraryId}`,
{
headers: {
...authHeaders(),
- "Content-Type": "application/json",
},
},
);
+ if (response.status === 404) {
+ // Unified search signals "no results" as a 404 whose body still
+ // carries results: [].
+ displaySearchResults([]);
+ return;
+ }
+
if (!response.ok) {
throw new Error("Failed to search books");
}
const data = await response.json();
- displaySearchResults(data.books || []);
+ const rows: Array<{
+ id?: string;
+ title?: string;
+ author?: string | null;
+ cover_image_path?: string | null;
+ }> = Array.isArray(data) ? data : data.results || [];
+ const books: BookInfo[] = rows.map((r) => ({
+ media_item_id: r.id ?? "",
+ title: r.title ?? "",
+ author: r.author ?? "",
+ cover_image_path: r.cover_image_path ?? "",
+ has_conflict: false,
+ format_group: "",
+ badge: "",
+ }));
+ displaySearchResults(books);
} catch (error) {
console.error("Search books error:", error);
showToast("Failed to search books", "error");
@@ -418,7 +448,7 @@ async function searchBooks(): Promise {
}
function displaySearchResults(books: BookInfo[]): void {
- const resultsContainer = document.getElementById("search-results") as HTMLElement;
+ const resultsContainer = document.getElementById("cs-search-results") as HTMLElement;
if (!resultsContainer) return;
if (books.length === 0) {