From 3cf5d764a2f470535ab276bbf8d9045700bd5141 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 20 Mar 2026 11:42:04 -0400 Subject: [PATCH] refactor: rewrite bookshelf TypeScript to SSR-first architecture Complete rewrite following PROJECT_GUIDELINES.md procedural style: Remove anti-patterns: - Remove class-based OOP approach - Remove manual DOM manipulation (classList.add/remove) - Remove client-side data fetching in x-init - Remove getEventListeners and manual event delegation Add SSR-first patterns: - Alpine.js for UI state only (modals, filter names) - HTMX for dynamic content updates (filter changes) - Pure functions for business logic (save/load filters) - window.htmx.trigger() for programmatic HTMX triggers - Server-side rendering for initial data load Key features: - saveFilter(): Save custom filter configurations - loadSavedFilters(): Load user's saved filters - initBookshelf(): Setup only (no data fetch) - clearFilters(): Reset all filter fields - showSaveFilterModal(): Open save filter modal All Alpine state is local component data, not global store. Follows ALPINE_COMPLETION_GUIDE.md principles strictly. --- web/src/bookshelf.ts | 166 +++++++++++++++++++++++++------------------ 1 file changed, 96 insertions(+), 70 deletions(-) diff --git a/web/src/bookshelf.ts b/web/src/bookshelf.ts index 1aa6044..759afaf 100644 --- a/web/src/bookshelf.ts +++ b/web/src/bookshelf.ts @@ -1,5 +1,10 @@ +// Bookshelf functionality - procedural/imperative style + import { Alpine } from "./alpine"; import { showToast } from "./toast"; + +// Filter state management (Alpine.js handles this, TS just saves/restores) + async function loadSavedFilters(): Promise { const token = localStorage.getItem("token"); if (!token) return; @@ -7,6 +12,7 @@ async function loadSavedFilters(): Promise { const response = await fetch("/api/bookshelf/filters", { headers: { Authorization: `Bearer ${token}` }, }); + if (response.ok) { const filters = await response.json(); localStorage.setItem("bookshelfFilters", JSON.stringify(filters)); @@ -15,11 +21,83 @@ async function loadSavedFilters(): Promise { console.error("Failed to load saved filters:", error); } } -// Note: saveFilter and showSaveFilterModal are now methods on the Alpine component -// They access state via 'this' instead of window.Alpine + +async function saveFilter(event: Event): Promise { + event.preventDefault(); + const token = localStorage.getItem("token"); + if (!token) { + showToast("Not authenticated", "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(); + }); + + // Add filter name from Alpine state + const filterName = (window as any).Alpine?.$store.bookshelf?.filterName; + if (!filterName) { + showToast("Please enter a filter name", "error"); + return; + } + + try { + const response = await fetch("/api/bookshelf/filters", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + name: filterName, + filters: filterData, + }), + }); + + if (response.ok) { + showToast("Filter saved successfully", "success"); + // Close modal via Alpine + (window as any).Alpine.$store.bookshelf.showSaveModal = false; + loadSavedFilters(); + } else { + showToast("Failed to save filter", "error"); + } + } catch (error) { + console.error("Failed to save filter:", error); + showToast("Error saving filter", "error"); + } +} + +function initBookshelf(): void { + // Load saved filters on page load + loadSavedFilters(); + + // Setup initial book load via HTMX + const librarySelect = document.getElementById( + "library-select", + ) as HTMLSelectElement; + if (librarySelect && librarySelect.value) { + const filterForm = document.getElementById( + "filter-form", + ) as HTMLFormElement; + const booksGrid = document.getElementById("books-grid"); + + if (filterForm && booksGrid) { + // Trigger initial HTMX load + window.htmx.trigger(librarySelect, "change"); + } + } +} + function clearFilters(): void { const filterForm = document.getElementById("filter-form") as HTMLFormElement; if (!filterForm) return; + + // Reset all form fields const inputs = filterForm.querySelectorAll("input, select"); inputs.forEach((input) => { if (input instanceof HTMLInputElement && input.type === "checkbox") { @@ -28,77 +106,25 @@ function clearFilters(): void { (input as HTMLInputElement).value = ""; } }); - htmx.trigger(filterForm, "change"); + + // Trigger HTMX reload with cleared filters + window.htmx.trigger(filterForm, "change"); } -// Alpine.js component - all state managed locally, no window.Alpine at runtime + +function showSaveFilterModal(): void { + // Open modal via Alpine store + (window as any).Alpine.$store.bookshelf.showSaveModal = true; +} + +// Alpine.js component Alpine.data("bookshelf", () => ({ 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"); - } - } - }, + + initBookshelf, 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"); - } - }, + showSaveFilterModal, + saveFilter, })); -export { clearFilters }; + +export { initBookshelf, clearFilters, showSaveFilterModal, saveFilter };