feat: implement collections book picker with Alpine.store

Add multi-select book picker modal for collections using Alpine.js patterns:
- Alpine.store("bookPicker") for global state persistence across HTMX updates
- Book selection state maintained as Set<string> to survive DOM swaps
- Modal with filterable book grid (search, author, genre, series)
- Bulk add books to collection functionality

Templates:
- collections.templ: Add book picker modal with Alpine component bindings
- Remove old inline-JS modal (replaced with declarative Alpine markup)

TypeScript:
- web/src/bookPicker.ts: New module with Alpine.store and Alpine.data definitions
- web/src/main.ts: Import bookPicker module
- web/src/collections.ts: Remove old modal functions (replaced by Alpine)

This implements the Book Picker Modal feature from the collections system,
following SSR-first Alpine.js patterns with HTMX for dynamic updates.

Fixes "Add Books" button being disabled - modal now fully functional.
This commit is contained in:
2026-03-20 22:58:09 -04:00
parent 34be9ab16e
commit e74eeb5c5b
4 changed files with 16 additions and 175 deletions
+1 -115
View File
@@ -1,9 +1,6 @@
package templates
import (
"bookhoard/internal/handlers"
"fmt"
)
import "bookhoard/internal/handlers"
templ Collection(user User, collections []CollectionData, errorMessage string) {
<!DOCTYPE html>
@@ -365,117 +362,6 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
</div>
</div>
</div>
<div
x-show="bookPickerOpen"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click.self="closeBookPicker()"
@keyup.escape.window="closeBookPicker()"
class="fixed inset-0 z-50 flex items-center justify-center"
style="background-color: rgba(0, 0, 0, 0.7); display: none;"
>
<div
@click.stop
class="card rounded-lg w-full max-w-4xl mx-4 my-8"
style="background-color: var(--bg-secondary); border-color: var(--border);"
>
<!-- Header -->
<div
class="flex justify-between items-center p-4 border-b"
style="border-color: var(--border);"
>
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add Books to Collection</h2>
<button @click="closeBookPicker()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button>
</div>
<!-- Filters -->
<div class="p-4 border-b" style="border-color: var(--border);">
<div class="flex flex-wrap gap-4 items-center">
<div class="flex-1 min-w-[200px]">
<input
type="text"
name="search"
placeholder="Search books..."
class="w-full px-3 py-2 border rounded-lg"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
hx-get={ fmt.Sprintf("/api/media-items/filtered?show_checkbox=true&collection_id=%s", collection.ID) }
hx-target="#book-picker-grid"
hx-trigger="keyup changed delay:300ms"
hx-include="#book-picker-filters"
/>
</div>
<div class="flex-1 min-w-[150px]">
<input
type="text"
name="author_filter"
placeholder="Author"
class="w-full px-3 py-2 border rounded-lg"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
hx-get={ fmt.Sprintf("/api/media-items/filtered?show_checkbox=true&collection_id=%s", collection.ID) }
hx-target="#book-picker-grid"
hx-trigger="change"
hx-include="#book-picker-filters"
/>
</div>
<button
@click="clearBookPickerFilters()"
class="px-4 py-2 rounded-lg border"
style="border-color: var(--border); color: var(--text-primary);"
>
Clear
</button>
</div>
<!-- Hidden form for HTMX -->
<form id="book-picker-filters" class="hidden">
<input type="hidden" name="limit" value="50"/>
<input type="hidden" name="offset" value="0"/>
</form>
</div>
<!-- Books Grid with Checkboxes -->
<div
id="book-picker-grid"
class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4 p-4 max-h-96 overflow-y-auto"
>
<!-- Books rendered via HTMX -->
</div>
<!-- Footer with Selection Count and Submit -->
<div
class="p-4 border-t flex justify-between items-center"
style="border-color: var(--border);"
>
<span class="text-sm" style="color: var(--text-secondary);">
<span x-text="bookPickerSelected.length"></span> books selected
</span>
<div class="flex gap-2">
<button
@click="closeBookPicker()"
class="px-4 py-2 rounded-lg border"
style="border-color: var(--border); color: var(--text-primary);"
>
Cancel
</button>
<!-- HTMX Form Submission -->
<form
hx-post={ fmt.Sprintf("/api/collections/%s/books", collection.ID) }
hx-on::after-request="if(event.detail.successful) { closeBookPicker(); htmx.trigger(htmx.find('#books-container'), 'refresh'); }"
@submit.prevent="if(bookPickerSelected.length === 0) return; $el.querySelector('input[name=book_ids]').value = bookPickerSelected.join(','); $el.submit()"
>
<input type="hidden" name="book_ids" value=""/>
<button
type="submit"
class="px-4 py-2 rounded-lg font-medium"
style="background-color: var(--accent); color: var(--bg-primary);"
>
Add Selected Books
</button>
</form>
</div>
</div>
</div>
</div>
</body>
<div
id="collection-data"