From a63394f4294f789fa2725e036b03e77f3469af55 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 20 Mar 2026 23:10:28 -0400 Subject: [PATCH] docs: remove completed bookshelf and collections filter implementation plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove BOOKSHELF_COLLECTIONS_FILTER_PLAN.md - this plan has been fully implemented: - Bookshelf page with SSR-first architecture and Alpine.js integration ✅ - Collections book picker modal with Alpine.store for state persistence ✅ - All TypeScript files written and working ✅ - All templates updated with HTMX/Alpine patterns ✅ - Navigation link added ✅ The implementation is complete and tested. This planning document lives on in git history for reference. Keeping the repository clean of completed plans. Implementation details preserved in: - SAVED_FILTERS_IMPLEMENTATION.md (for future saved filters feature) - docs/developer/alpine-patterns.md (Alpine.js patterns used) - Git commit history (all implementation commits) --- BOOKSHELF_COLLECTIONS_FILTER_PLAN.md | 1154 -------------------------- 1 file changed, 1154 deletions(-) delete mode 100644 BOOKSHELF_COLLECTIONS_FILTER_PLAN.md diff --git a/BOOKSHELF_COLLECTIONS_FILTER_PLAN.md b/BOOKSHELF_COLLECTIONS_FILTER_PLAN.md deleted file mode 100644 index 9b72c23..0000000 --- a/BOOKSHELF_COLLECTIONS_FILTER_PLAN.md +++ /dev/null @@ -1,1154 +0,0 @@ -# Bookshelf & Collections Filter Implementation Plan - -## Executive Summary - -Restore bookshelf page as an advanced filtering interface and fix collections book picker modal. Both components will use the existing `/api/media-items/filtered` API with SSR-first architecture. - -**Status:** Orphaned code restoration + new feature implementation -**Priority:** High (core library browsing functionality) -**Estimated Time:** 8-10 hours -**Backend Changes:** None (API already exists) -**Frontend Changes:** New templates + TypeScript - ---- - -## Table of Contents - -1. [Current State](#current-state) -2. [Architecture](#architecture) -3. [Phase 1: Bookshelf Page](#phase-1-bookshelf-page) -4. [Phase 2: Collections Book Picker](#phase-2-collections-book-picker) -5. [Phase 3: Shared Components](#phase-3-shared-components) -6. [Phase 4: Integration & Testing](#phase-4-integration--testing) -7. [Success Criteria](#success-criteria) - ---- - -## Current State - -### What Exists (Backend) ✅ - -**API Endpoint:** `/api/media-items/filtered` -- Supports: author, series, genre, language, year range, cover images -- Sorting by any field -- Pagination (limit/offset) -- **Status:** Fully functional, unused - -### What's Broken (Frontend) ❌ - -**Bookshelf Page:** -- Template: `templates/bookshelf.templ` (exists, orphaned) -- TypeScript: `web/src/bookshelf.ts` (exists, imported but unused) -- Route: `/bookshelf` (removed in commit 2df2b2d) -- Status: Template/JS exist, no route registration - -**Collections Book Picker:** -- Button: Disabled in `templates/collections.templ:153-154` -- Functions: Deleted in commit 93710a1 -- Status: Feature completely non-functional - ---- - -## Architecture - -### SSR-First Principles (Per PROJECT_GUIDELINES.md & ALPINE_COMPLETION_GUIDE.md) - -**Page Type:** Type 2 (SSR + Interactive) - -**SSR (Server-Side Rendering):** -- ✅ Backend renders initial page with data -- ✅ Complete HTML sent to browser (no empty states) -- ✅ No data fetch on page load - -**Alpine.js (UI State):** -- ✅ Manages component state (modals, dropdowns, filters) -- ✅ `x-show` for visibility (NOT `class="hidden"`) -- ✅ `x-data` for state storage -- ✅ x-init ONLY for setup (event listeners) -- ❌ NEVER fetch data in x-init - -**HTMX (Dynamic Updates):** -- ✅ Swaps HTML fragments without page reload -- ✅ Server returns new HTML -- ✅ Client swaps only changed portion -- ✅ No Alpine state lost (uses Alpine.store) - -**TypeScript (Business Logic):** -- ✅ Pure functions -- ✅ API calls -- ✅ Data processing -- ❌ NO manual DOM manipulation -- ❌ NO `classList.add/remove("hidden")` -- ❌ NO `getElementById` for show/hide - -**When to Use What:** -- **`class="hidden"`**: Structural hiding only (forms, utilities) -- **`x-show`**: All stateful UI (modals, dropdowns, dynamic content) -- **`style="display: none;"`**: FOUC prevention with `x-show` (Alpine guide recommends this) - -### Component Design - -**Shared Filtering Component:** -- Reusable across bookshelf page and collections picker -- Alpine.js for state management -- Server-side initial render -- HTMX for filter updates (no page reload) - -**Data Flow (SSR → HTMX → Alpine → TypeScript):** - -``` -Initial Page Load (SSR): - Server fetches libraries & initial books (paginated) - → Renders complete HTML with data - → Sends to browser - → Alpine initialized for UI state - -Filter Change (HTMX → Alpine): - User types in filter field - → Alpine updates local state (if needed) - → HTMX fetches filtered books (HTML) - → HTMX swaps book grid innerHTML - → Alpine re-initializes (maintains store state) - → User sees filtered results - -Multi-Select (Alpine Store): - User clicks checkbox - → Alpine.store.bookPicker.toggleBook(id) - → Checkbox state persisted in store - → User changes filter (HTMX swap) - → Grid re-rendered with new books - → Alpine re-applies :checked from store - → Selection persists across filter changes - -Form Submission (HTMX → TypeScript): - User clicks "Add Books" - → TypeScript reads from Alpine.store - → API call with all selected books - → Success: modal closes, page updates -``` - ---- - -## Phase 1: Bookshelf Page - -### 1.1 Restore Route Registration - -**File:** `internal/router/frontend.go` - -**Location:** After line 204 (before dashboard route) - -**Add route handler:** - -```go -// Bookshelf page - advanced library filtering interface -frontendProtected.GET("/bookshelf", func(c *echo.Context) error { - user, err := getTemplateUserWithTheme(c, cfg) - if err != nil { - return renderErrorPage(c, "Error loading user", "user_load_error") - } - - var errorMsg string - - // Get library_id from query param or user's first library - libraryID := c.QueryParam("library_id") - if libraryID == "" { - userUUID, _ := uuid.Parse(user.ID) - libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID)) - if err == nil && len(libraries) > 0 { - libUUID, _ := uuid.FromBytes(libraries[0].ID.Bytes[0:16]) - libraryID = libUUID.String() - } else { - errorMsg = "No libraries available" - } - } - - // Get libraries for dropdown - userUUID, _ := uuid.Parse(user.ID) - libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID)) - if err != nil { - log.Printf("GetUserVisibleLibraries failed: %v", err) - libraries = []database.GetUserVisibleLibrariesRow{} - if errorMsg == "" { - errorMsg = "Error loading libraries" - } - } - - libData := make([]templates.LibraryData, len(libraries)) - for i, lib := range libraries { - libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16]) - libData[i] = templates.LibraryData{ - ID: libUUID.String(), - Name: lib.Name, - Description: getText(lib.Description), - TypeName: lib.TypeName, - } - } - - var buf bytes.Buffer - err = templates.BookShelf(user, libData, libraryID, errorMsg).Render(c.Request().Context(), &buf) - if err != nil { - return err - } - return c.HTML(http.StatusOK, buf.String()) -}) -``` - -**Why this approach:** -- SSR-first: Backend fetches libraries, not JavaScript -- Default library selection if none specified -- Error handling for no libraries -- Follows existing frontend.go patterns - ---- - -### 1.2 Update Bookshelf Template - -**File:** `templates/bookshelf.templ` - -**Complete rewrite (replace entire file):** - -```templ -package templates - -templ BookShelf(user User, libraries []LibraryData, currentLibraryID string, errorMessage string) { - - - - - - Library - Bookhoard - - - - - - @Header(user, "/bookshelf") - -
- -
-
- -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- -
- - -
-
- - -
- -
- - -
- - -
- - -
- -
- - -
- -
-
- - - -
- - -
- -
- - - - - - if errorMessage != "" { -
-

{ errorMessage }

-
- } -
- - - - - - - - -} -``` - -**Key Design Decisions:** - -1. **SSR-First:** - - Libraries fetched server-side - - Initial page render complete - - No data fetch in x-init - -2. **HTMX for Filtering:** - - Filter changes trigger HTMX requests - - Only book grid swaps (no full page reload) - - hx-include includes all filter values - -3. **Alpine.js for UI State:** - - Modal visibility (showSaveModal) - - Filter name input (filterName) - - No data fetching in Alpine - -4. **Progressive Enhancement:** - - Page works without JavaScript (server render) - - HTMX provides dynamic filtering - - Alpine adds modal interactions - ---- - -### 1.3 Create Bookshelf TypeScript - -**File:** `web/src/bookshelf.ts` - -**Replace entire file:** - -```typescript -// 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; - - try { - 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)); - } - } catch (error) { - console.error("Failed to load saved filters:", error); - } -} - -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 - 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") { - input.checked = false; - } else { - (input as HTMLInputElement).value = ""; - } - }); - - // Trigger HTMX reload with cleared filters - htmx.trigger(filterForm, "change"); -} - -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, - clearFilters, - showSaveFilterModal, - saveFilter, -})); - -export { initBookshelf, clearFilters, showSaveFilterModal, saveFilter }; -``` - -**Why this approach:** - -1. **Procedural style:** No classes, no this-capture -2. **Alpine-first:** UI state in Alpine.data -3. **TypeScript for business logic:** Saving/loading filters -4. **No DOM manipulation:** All via Alpine/HTMX -5. **x-init for setup only:** No data fetching - ---- - -### 1.4 Update TypeScript Build - -**File:** `web/src/main.ts` - -**Line 9:** Remove orphaned import - -```typescript -// DELETE this line: -// import "./bookshelf"; - -// Bookshelf is now loaded via