diff --git a/BOOKSHELF_COLLECTIONS_FILTER_PLAN.md b/BOOKSHELF_COLLECTIONS_FILTER_PLAN.md new file mode 100644 index 0000000..9b72c23 --- /dev/null +++ b/BOOKSHELF_COLLECTIONS_FILTER_PLAN.md @@ -0,0 +1,1154 @@ +# 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) { + + +
+ + +{ errorMessage }
+