# Book Picker Implementation Plan ## Overview Implement a client-side book picker modal for collections that allows users to search and select books to add to a collection. **Architecture:** Client-side only (Alpine.js + TypeScript + HTMX for submission) - No SSR for book grid - Uses existing `/api/media-items` and `/api/media-items/search` APIs - Alpine manages checkbox state and rendering ## Current State ### What's Done ✅ - Collections "Add Books" button enabled - Book picker modal HTML in template - Alpine state management in collections.ts - Icon picker fix (showAllIcons, setupHTMXModalInit) ### What's Broken ❌ - Book picker modal has HTMX-based search that calls non-existent endpoint - Checkboxes not rendered (API returns JSON, not HTML with checkboxes) ## Implementation ### Phase 1: Update collections.ts Add the following methods to `Alpine.data("collections", ...)`: ```typescript // State bookPickerPage: number = 0, bookPickerHasMore: boolean = true, bookPickerLoading: boolean = false, // Load books from API async loadBooksForPicker(reset: boolean = false): Promise { if (reset) { this.bookPickerPage = 0; this.bookPickerSelected = []; } this.bookPickerLoading = true; const token = localStorage.getItem("token"); if (!token) return; try { const offset = this.bookPickerPage * 50; const response = await fetch(`/api/media-items?limit=50&offset=${offset}`, { headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { const result = await response.json(); const items = result.data || []; this.renderBooksGrid(items, reset); this.bookPickerHasMore = items.length === 50; } } catch (error) { console.error("Failed to load books:", error); } finally { this.bookPickerLoading = false; } }, // Search books async searchBooks(query: string): Promise { if (query.length < 2) { if (query.length === 0) { this.loadBooksForPicker(true); } return; } this.bookPickerLoading = true; const token = localStorage.getItem("token"); if (!token) return; try { const response = await fetch(`/api/media-items/search?q=${encodeURIComponent(query)}&limit=50`, { headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { const items = await response.json(); this.renderBooksGrid(items, true); this.bookPickerHasMore = false; } } catch (error) { console.error("Failed to search books:", error); } finally { this.bookPickerLoading = false; } }, // Render books grid with checkboxes renderBooksGrid(items: any[], reset: boolean): void { const grid = document.getElementById("book-picker-grid"); if (!grid) return; const html = items.map((item: any) => `

${item.title}

${item.author || "Unknown"}

`).join(""); if (reset) { grid.innerHTML = html; } else { grid.innerHTML += html; } }, // Pagination loadMoreBooks(): void { if (this.bookPickerLoading || !this.bookPickerHasMore) return; this.bookPickerPage++; this.loadBooksForPicker(false); }, ``` ### Phase 2: Update collections.templ Update the modal to use Alpine methods: 1. **Search input** - Change from HTMX to Alpine: ```templ ``` 2. **Books grid** - Remove hx-get, add Alpine rendering: ```templ
``` 3. **Pagination button**: ```templ ``` 4. **Clear search button**: ```templ ``` ### Phase 3: Verify Submission Works Ensure the form submission works. The existing form should work: ```templ
``` Verify the endpoint accepts `book_ids` as form data. ## Files Modified | File | Changes | |------|---------| | `web/src/collections.ts` | Add loadBooksForPicker, searchBooks, renderBooksGrid, loadMoreBooks methods | | `templates/collections.templ` | Update modal to use Alpine methods instead of HTMX for search | ## Testing Checklist - [ ] Modal opens when clicking "Add Books" - [ ] Books load on modal open (initial load) - [ ] Search filters books correctly - [ ] Clear button resets to all books - [ ] Checkboxes can be selected/deselected - [ ] Selected count updates correctly - [ ] "Load More" loads additional books - [ ] Form submission adds books to collection - [ ] Modal closes after successful submission - [ ] Collection books list refreshes after add