feat: add functional book picker modal to collections
Update CollectionDetail template to enable book picker: Enable Add Books button: - Remove disabled attribute and inline JavaScript handlers - Wire to $store.bookPicker.open() using Alpine store Remove old modal: - Delete non-functional inline-JavaScript modal (add-books-modal) - Remove inline event handlers (onchange, onclick) - Clean up unused DOM elements Add new book picker modal: - Full-screen modal with HTMX-powered filtering UI - Search by title, author, genre with live filtering - Multi-select checkboxes with Alpine.store state persistence - Selected count display and submit functionality - Clear filters resets search (preserves selections) - ESC key closes modal via Alpine event listener SSR-first implementation: - Alpine.store.bookPicker manages all state (no DOM state) - HTMX swaps book grid without losing selections - Checkboxes re-rendered from store state after DOM swap - Selection persists across pagination and filter changes - No class="hidden" for stateful UI (use x-show) - style="display: none;" prevents FOUC on x-show elements Replaces non-functional inline JavaScript approach. Matches bookshelf filtering UX for consistency. Changes to collections_templ.go are auto-generated from .templ file.
This commit is contained in:
+133
-29
@@ -154,7 +154,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
🗑️ Remove Selected
|
||||
</button>
|
||||
<button
|
||||
@click="openBookPicker()"
|
||||
@click="$store.bookPicker.open()"
|
||||
class="btn-primary px-4 py-2 rounded-lg"
|
||||
>
|
||||
➕ Add Books
|
||||
@@ -227,37 +227,141 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div id="add-books-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||
<div class="card rounded-lg p-6 w-full max-w-2xl mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add Books to Collection</h2>
|
||||
<button @click="hideAddBooksModal" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
<!-- Book Picker Modal -->
|
||||
<!-- CRITICAL: Selection state stored in Alpine.store to persist across HTMX swaps -->
|
||||
<div
|
||||
x-data="bookPicker"
|
||||
@keyup.escape.window="$store.bookPicker.close()"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
style="display: none;"
|
||||
>
|
||||
<!-- Modal content with same filtering UI as bookshelf + multi-select -->
|
||||
<!-- CRITICAL: x-show manages visibility, style="display: none;" prevents FOUC -->
|
||||
<!-- Following ALPINE_COMPLETION_GUIDE.md principles: Alpine state for UI, not class="hidden" -->
|
||||
<div
|
||||
@click.stop
|
||||
x-show="$store.bookPicker.isOpen"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="card rounded-lg w-full max-w-6xl mx-4 my-8"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); display: none;"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center p-6 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="$store.bookPicker.close()"
|
||||
class="p-2 hover:opacity-80 rounded"
|
||||
style="color: var(--text-primary)"
|
||||
>✕</button>
|
||||
</div>
|
||||
<p class="mb-4" style="color: var(--text-secondary)">Search and select books to add to this collection.</p>
|
||||
<!-- Library filter toggle - hidden by default, shown by JS when library_id present -->
|
||||
<div id="library-filter-container" class="mb-4 hidden">
|
||||
<label class="flex items-center gap-2 text-sm" style="color: var(--text-secondary);">
|
||||
<input type="checkbox" id="filter-by-library" class="w-4 h-4" onchange="searchBooksForCollection()"/>
|
||||
<span>Only show books from this library</span>
|
||||
</label>
|
||||
<!-- Filter Bar (same as bookshelf) -->
|
||||
<div
|
||||
class="p-4 border-b"
|
||||
style="border-color: var(--border);"
|
||||
>
|
||||
<div class="flex flex-wrap gap-4 items-center">
|
||||
<!-- Search -->
|
||||
<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="/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }"
|
||||
hx-target="#book-picker-grid"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-include="#book-picker-filters"
|
||||
/>
|
||||
</div>
|
||||
<!-- Author -->
|
||||
<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="/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }"
|
||||
hx-target="#book-picker-grid"
|
||||
hx-trigger="change"
|
||||
hx-include="#book-picker-filters"
|
||||
/>
|
||||
</div>
|
||||
<!-- Genre -->
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
<input
|
||||
type="text"
|
||||
name="genre_filter"
|
||||
placeholder="Genre"
|
||||
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="/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }"
|
||||
hx-target="#book-picker-grid"
|
||||
hx-trigger="change"
|
||||
hx-include="#book-picker-filters"
|
||||
/>
|
||||
</div>
|
||||
<!-- Clear -->
|
||||
<div>
|
||||
<button
|
||||
@click="$store.bookPicker.clearFilters()"
|
||||
class="px-4 py-2 rounded-lg border"
|
||||
style="border-color: var(--border); color: var(--text-primary);"
|
||||
>
|
||||
✕ Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Hidden form for HTMX -->
|
||||
<!-- NOTE: class="hidden" is acceptable here because form is never shown to user -->
|
||||
<!-- HTMX uses it for parameter inclusion only -->
|
||||
<form id="filter-form" class="hidden">
|
||||
<input type="hidden" name="limit" value="50"/>
|
||||
<input type="hidden" name="offset" value="0"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
id="book-search"
|
||||
placeholder="Search books..."
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
/>
|
||||
<!-- Books Grid with Multi-select -->
|
||||
<!-- CRITICAL: HTMX swaps this div's content, but Alpine.store preserves selection state -->
|
||||
<div
|
||||
id="book-picker-grid"
|
||||
class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 p-4 max-h-96 overflow-y-auto"
|
||||
>
|
||||
<!-- Books loaded via HTMX -->
|
||||
</div>
|
||||
<div id="book-results" class="max-h-64 overflow-y-auto mb-4"></div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="hideAddBooksModal" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" @click="addbooksToAdd" class="btn-primary px-4 py-2 rounded-lg">
|
||||
Add Selected Books
|
||||
</button>
|
||||
<!-- Selected Count & Submit -->
|
||||
<div
|
||||
class="p-4 border-t flex justify-between items-center"
|
||||
style="border-color: var(--border);"
|
||||
>
|
||||
<div class="text-sm" style="color: var(--text-secondary);">
|
||||
<span x-text="$store.bookPicker.selectedCount"></span> books selected
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="$store.bookPicker.close()"
|
||||
class="px-4 py-2 rounded-lg border"
|
||||
style="border-color: var(--border); color: var(--text-primary);"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="$store.bookPicker.submit()"
|
||||
class="px-4 py-2 rounded-lg font-medium"
|
||||
style="background-color: var(--accent); color: var(--bg-primary);"
|
||||
>
|
||||
Add Selected Books
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user