diff --git a/BOOK_PICKER_IMPL.md b/BOOK_PICKER_IMPL.md new file mode 100644 index 0000000..3563e8e --- /dev/null +++ b/BOOK_PICKER_IMPL.md @@ -0,0 +1,211 @@ +# 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 diff --git a/web/src/types/htmx.d.ts b/web/src/types/htmx.d.ts new file mode 100644 index 0000000..58ac5de --- /dev/null +++ b/web/src/types/htmx.d.ts @@ -0,0 +1,4 @@ +declare var htmx: { + trigger(elt: Element | string, event: string, detail?: any): void; + find(selector: string): Element | null; +};