feat(filters+nav): author navigation end-to-end — arrays on every media response, deep-linkable bookshelf filters, clickable authors
Completes the A2c acceptance scenario: click an author on a book detail page -> land on the bookshelf filtered to everything that author wrote (including their share of co-authored books). - Media detail + unscoped list resolved maps expose subtitle, explicit, authors, publishers (the library-scoped raw branch and the search endpoint already did — raw rows follow the SELECT). - SearchMediaItemsUnified SELECT carries the new columns; the search endpoint serializes the row directly, so clients get them there too. - book_detail.templ renders each author as a link to /bookshelf?author_filter=<name>. - Bookshelf honors filter params in the URL on load (author_filter, series_filter, genre_filter, publisher_filter, tags_filter, narrator_filter, language_filter): pre-fills the form, applies it to the first grid load. Comma-joined values are multi-select via the server-side string_to_array split. Filtered views are shareable. Pins widened: search rows (+7 keys), resolved keys (+4); changelog entries in docs/contract_pins.md. Android follow-up plan written to ~/.opencode/plan/bookhoard-app-v2-compat.md.
This commit is contained in:
1 parent
f94e7472b4
commit
7537063cb7
8 files changed
+851
-728
No files matched your search
+48
-10
@@ -456,28 +456,66 @@ Alpine.data("bookshelf", () => ({
|
||||
}));
|
||||
|
||||
// Standalone function - initialize the bookshelf (NOT async, like dashboard.ts)
|
||||
// A2c §5.3: deep-linkable filters. Reads known filter params from the URL
|
||||
// into the form inputs; returns true when at least one was applied. A
|
||||
// comma-joined value is a multi-select (the server splits it per element).
|
||||
function applyURLFilters(filterForm: HTMLFormElement | null): boolean {
|
||||
if (!filterForm) return false;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const filterNames = [
|
||||
"author_filter",
|
||||
"series_filter",
|
||||
"genre_filter",
|
||||
"publisher_filter",
|
||||
"tags_filter",
|
||||
"narrator_filter",
|
||||
"language_filter",
|
||||
];
|
||||
let applied = false;
|
||||
for (const name of filterNames) {
|
||||
const value = params.get(name);
|
||||
if (!value) continue;
|
||||
const input = filterForm.querySelector(
|
||||
`[name="${name}"]`,
|
||||
) as HTMLInputElement | null;
|
||||
if (input) {
|
||||
input.value = value;
|
||||
applied = true;
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
function initBookshelf(): void {
|
||||
const filterForm = document.getElementById(
|
||||
"filter-form",
|
||||
) as HTMLFormElement;
|
||||
|
||||
// A2c §5.3: deep-linkable filters — /bookshelf?author_filter=Sapkowski
|
||||
// pre-fills the form and applies it to the first grid load. Author
|
||||
// links on the detail page (and anything else) can share filtered
|
||||
// views; a comma-joined value is a multi-select (server splits it).
|
||||
const urlApplied = applyURLFilters(filterForm);
|
||||
|
||||
// Check if books were already rendered server-side
|
||||
const booksGrid = document.getElementById("books-grid");
|
||||
const hasServerBooks =
|
||||
booksGrid && booksGrid.querySelector("[data-book-id]") !== null;
|
||||
|
||||
if (!hasServerBooks) {
|
||||
// Only trigger HTMX if no SSR books rendered
|
||||
if (!hasServerBooks || urlApplied) {
|
||||
const librarySelect = document.getElementById(
|
||||
"library-select",
|
||||
) as HTMLSelectElement;
|
||||
librarySelect?.addEventListener("change", () => {
|
||||
localStorage.setItem("selectedLibrary", librarySelect.value);
|
||||
});
|
||||
if (librarySelect && librarySelect.value) {
|
||||
const filterForm = document.getElementById(
|
||||
"filter-form",
|
||||
) as HTMLFormElement;
|
||||
if (filterForm) {
|
||||
// Trigger initial HTMX load
|
||||
window.htmx.trigger(librarySelect, "change");
|
||||
}
|
||||
if (urlApplied && filterForm) {
|
||||
// A URL filter replaces the grid with the filtered set — the form
|
||||
// submit serializes the library select too (it lives in the form).
|
||||
window.htmx.trigger(filterForm, "submit");
|
||||
} else if (librarySelect && librarySelect.value && filterForm) {
|
||||
// Normal unfiltered initial HTMX load
|
||||
window.htmx.trigger(librarySelect, "change");
|
||||
}
|
||||
}
|
||||
// Setup listener for HTMX swap events on the filters list
|
||||
|
||||
Reference in new issue
Block a user