-
-
Loading your library...
+
+
+
+
+
+
+
+ if errorMessage != "" {
+
-
-
-
📚
-
No Books Yet
-
Select a library to view your collection
-
-
-
-
+ }
+
+
+
+
+
}
diff --git a/templates/bookshelf_templ.go b/templates/bookshelf_templ.go
index 88eabdf..e741411 100644
--- a/templates/bookshelf_templ.go
+++ b/templates/bookshelf_templ.go
@@ -8,7 +8,7 @@ package templates
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
-func BookShelf(user User, libraries []LibraryData) templ.Component {
+func BookShelf(user User, libraries []LibraryData, currentLibraryID string, errorMessage string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -29,7 +29,7 @@ func BookShelf(user User, libraries []LibraryData) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Library - Bookhoard")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Library - Bookhoard")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -37,7 +37,7 @@ func BookShelf(user User, libraries []LibraryData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if errorMessage != "" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 227, Col: 58}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/web/src/bookshelf.ts b/web/src/bookshelf.ts
index cd73744..1aa6044 100644
--- a/web/src/bookshelf.ts
+++ b/web/src/bookshelf.ts
@@ -1,200 +1,104 @@
import { Alpine } from "./alpine";
import { showToast } from "./toast";
-
-let currentLibraryId = "";
-let mediaItems: unknown[] = [];
-
-function initBookshelf(): void {
- const savedLibrary = localStorage.getItem("selectedLibrary");
- if (savedLibrary) {
- const select = document.getElementById(
- "library-select",
- ) as HTMLSelectElement;
- if (select && select.value) {
- currentLibraryId = savedLibrary;
- loadBookshelf(savedLibrary);
- }
- }
-}
-
-function selectLibrary(): void {
- const select = document.getElementById("library-select") as HTMLSelectElement;
- if (!select) return;
-
- const libraryId = select.value;
- if (!libraryId) return;
-
- currentLibraryId = libraryId;
- localStorage.setItem("selectedLibrary", libraryId);
- loadBookshelf(libraryId);
-}
-
-async function loadBookshelf(libraryId: string): Promise
{
- const token = localStorage.getItem("token");
- if (!token || !libraryId) return;
-
- this.isLoading = true;
-
- try {
- const response = await fetch(
- `/api/media-items?library_id=${libraryId}&limit=100&offset=0`,
- {
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- },
- );
-
- if (response.ok) {
- const data = await response.json();
- mediaItems = data;
- renderBookshelf();
- }
- } catch (error) {
- console.error("Error loading bookshelf:", error);
- showToast("Error loading books", "error");
- this.isLoading = false;
- }
-}
-
-function showEmptyState(): void {
- const booksGrid = document.getElementById("books-grid");
- const emptyState = document.getElementById("empty-state");
- const loading = document.getElementById("loading");
-
- if (loading) loading.style.display = "none";
- if (booksGrid) booksGrid.classList.add("hidden");
- if (emptyState) emptyState.classList.remove("hidden");
-}
-
-function renderBookshelf(): void {
- if (!mediaItems || mediaItems.length === 0) {
- this.isLoading = false;
- this.hasBooks = false;
- return;
- }
-
- const booksGrid = document.getElementById("books-grid");
- if (!booksGrid) return;
-
- const booksPerShelf = 6;
- const shelves: unknown[][] = [];
-
- for (let i = 0; i < mediaItems.length; i += booksPerShelf) {
- shelves.push(mediaItems.slice(i, i + booksPerShelf));
- }
-
- let html = "";
- shelves.forEach((shelfBooks) => {
- html += `
-
- ${shelfBooks.map((book: any) => renderBookCard(book)).join("")}
-
-
-
`;
- });
-
- booksGrid.innerHTML = html;
-}
-
-function renderBookCard(book: any): string {
- const coverUrl = book.cover_image_path || "/static/placeholder-book.svg";
- const authorHtml = book.author
- ? `${book.author}
`
- : "";
- return `
-
-
-

-
-
-
${book.title}
- ${authorHtml}
-
-
`;
-}
-
-function viewBook(_bookId: string): void {
- showToast("Book viewer coming soon!", "info");
-}
-
-function selectBook(bookId: string): void {
- localStorage.setItem("selectedBook", bookId);
- window.location.href = `/books/${bookId}`;
-}
-
-function changePage(page: number): void {
- if (!currentLibraryId) return;
- const offset = (page - 1) * 50;
- loadBookshelfPaginated(currentLibraryId, offset);
-}
-
-async function loadBookshelfPaginated(
- libraryId: string,
- offset: number,
-): Promise {
+async function loadSavedFilters(): Promise {
const token = localStorage.getItem("token");
if (!token) return;
-
try {
- const response = await fetch(
- `/api/media-items?library_id=${libraryId}&limit=50&offset=${offset}`,
- { headers: { Authorization: `Bearer ${token}` } },
- );
-
+ const response = await fetch("/api/bookshelf/filters", {
+ headers: { Authorization: `Bearer ${token}` },
+ });
if (response.ok) {
- const data = await response.json();
- mediaItems = data;
- renderBookshelf();
+ const filters = await response.json();
+ localStorage.setItem("bookshelfFilters", JSON.stringify(filters));
}
} catch (error) {
- console.error("Failed to load bookshelf:", error);
+ console.error("Failed to load saved filters:", error);
}
}
-
-function setupEventDelegation(): void {
- const container = document.getElementById("books-grid");
- if (!container) return;
-
- container.addEventListener("click", (e) => {
- const target = e.target as HTMLElement;
- const card = target.closest("[data-book-id]") as HTMLElement;
-
- if (card) {
- const bookId = card.dataset.bookId;
- if (bookId) {
- selectBook(bookId);
- }
+// Note: saveFilter and showSaveFilterModal are now methods on the Alpine component
+// They access state via 'this' instead of window.Alpine
+function clearFilters(): void {
+ const filterForm = document.getElementById("filter-form") as HTMLFormElement;
+ if (!filterForm) return;
+ const inputs = filterForm.querySelectorAll("input, select");
+ inputs.forEach((input) => {
+ if (input instanceof HTMLInputElement && input.type === "checkbox") {
+ input.checked = false;
+ } else {
+ (input as HTMLInputElement).value = "";
}
});
+ htmx.trigger(filterForm, "change");
}
-
-export {
- changePage,
- initBookshelf,
- loadBookshelf,
- selectBook,
- selectLibrary,
- setupEventDelegation,
- viewBook,
-};
-
+// Alpine.js component - all state managed locally, no window.Alpine at runtime
Alpine.data("bookshelf", () => ({
- // State Variables
- isLoading: true,
- hasBooks: false,
-
- // Methods
- changePage,
- initBookshelf,
- loadBookshelf,
- selectBook,
- selectLibrary,
- setupEventDelegation,
- viewBook,
+ showSaveModal: false,
+ filterName: "",
+ initBookshelf(): void {
+ loadSavedFilters();
+ const librarySelect = document.getElementById(
+ "library-select",
+ ) as HTMLSelectElement;
+ if (librarySelect && librarySelect.value) {
+ const filterForm = document.getElementById(
+ "filter-form",
+ ) as HTMLFormElement;
+ if (filterForm) {
+ htmx.trigger(librarySelect, "change");
+ }
+ }
+ },
+ clearFilters,
+ showSaveFilterModal(): void {
+ this.showSaveModal = true;
+ this.filterName = "";
+ },
+ hideSaveFilterModal(): void {
+ this.showSaveModal = false;
+ this.filterName = "";
+ },
+ async saveFilter(event: Event): Promise {
+ event.preventDefault();
+ const token = localStorage.getItem("token");
+ if (!token) {
+ showToast("Not authenticated", "error");
+ return;
+ }
+ if (!this.filterName) {
+ showToast("Please enter a filter name", "error");
+ return;
+ }
+ const filterForm = document.getElementById(
+ "filter-form",
+ ) as HTMLFormElement;
+ const formData = new FormData(filterForm);
+ const filterData: Record = {};
+ formData.forEach((value, key) => {
+ filterData[key] = value.toString();
+ });
+ try {
+ const response = await fetch("/api/bookshelf/filters", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify({
+ name: this.filterName,
+ filters: filterData,
+ }),
+ });
+ if (response.ok) {
+ showToast("Filter saved successfully", "success");
+ this.hideSaveFilterModal();
+ loadSavedFilters();
+ } else {
+ showToast("Failed to save filter", "error");
+ }
+ } catch (error) {
+ console.error("Failed to save filter:", error);
+ showToast("Error saving filter", "error");
+ }
+ },
}));
+export { clearFilters };