feat(templates): add library switcher to collections and dashboard pages

Replace inline library switcher HTML with shared LibrarySwitcher component
across all collection pages and the dashboard.

templates/collections.templ (Collection):
- Update signature to accept libData []LibraryData, currentLibraryID
- Add @LibrarySwitcher(libData, currentLibraryID) after header
- Change x-init to initCollectionsPage() for unified initialization

templates/collections.templ (CollectionDetail):
- Update signature to accept libData []LibraryData
- Add @LibrarySwitcher(libData, libraryID) after header
- Fix broken "Back to Collections" button: replace non-existent
  backToCollections Alpine method with a plain <a href="/collections"> link
- Change x-init to initCollectionsPage() for unified initialization

templates/dashboard.templ:
- Replace 46-line inline sticky library selector (lines 20-66) with
  @LibrarySwitcher(libData, currentLibraryID, DashboardActions())
- Dashboard-specific settings and refresh buttons extracted into the
  DashboardActions sub-component via the variadic actions parameter
This commit is contained in:
2026-05-17 21:12:26 -04:00
parent 2fa3764df5
commit 8e48aa4334
4 changed files with 253 additions and 373 deletions
+8 -25
View File
@@ -2,7 +2,7 @@ package templates
import "bookhoard/internal/handlers"
templ Collection(user User, collections []CollectionData, errorMessage string) {
templ Collection(user User, collections []CollectionData, libData []LibraryData, currentLibraryID string, errorMessage string) {
<!DOCTYPE html>
<html lang="en">
<head>
@@ -12,8 +12,9 @@ templ Collection(user User, collections []CollectionData, errorMessage string) {
<script src="/static/htmx.min.js"></script>
<link href="/static/style.css" rel="stylesheet"/>
</head>
<body x-data="collections" x-init="initializeCollectionWebSocket(); setupHTMXModalInit()" class="theme-{ user.Theme }">
<body x-data="collections" x-init="initCollectionsPage()" class="theme-{ user.Theme }">
@Header(user, "/collections")
@LibrarySwitcher(libData, currentLibraryID)
<!-- Modal Container -->
<div id="modal-container"></div>
<!-- Actual container page -->
@@ -100,7 +101,7 @@ templ Collection(user User, collections []CollectionData, errorMessage string) {
</html>
}
templ CollectionDetail(user User, collection CollectionData, books []handlers.BookInfo, libraryID string) {
templ CollectionDetail(user User, collection CollectionData, books []handlers.BookInfo, libraryID string, libData []LibraryData) {
<!DOCTYPE html>
<html lang="en">
<head>
@@ -110,13 +111,14 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
<script src="/static/htmx.min.js"></script>
<link href="/static/style.css" rel="stylesheet"/>
</head>
<body x-data="collections" x-init="initializeCollectionWebSocket()" class="theme-{ user.Theme }">
<body x-data="collections" x-init="initCollectionsPage()" class="theme-{ user.Theme }">
@Header(user, "/collections")
@LibrarySwitcher(libData, libraryID)
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-6">
<button @click="backToCollections" class="btn-secondary px-4 py-2 rounded-lg mb-4">
<a href="/collections" class="btn-secondary px-4 py-2 rounded-lg mb-4 inline-block">
Back to Collections
</button>
</a>
<div class="flex items-center gap-4">
<div class="text-4xl" style="color: { collection.Color }">{ collection.Icon }</div>
<div>
@@ -169,7 +171,6 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
style="background-color: var(--bg-secondary); border-color: var(--border);"
>
<div class="flex gap-4">
<!-- Checkbox and Book Info -->
<div class="flex-shrink-0 pt-1">
<input
type="checkbox"
@@ -193,7 +194,6 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
</p>
}
</div>
<!-- Book Cover -->
<div class="flex-shrink-0 w-16 sm:w-20">
if book.CoverImagePath != "" {
<img
@@ -211,7 +211,6 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
}
</div>
</div>
<!-- Remove Button -->
<div class="mt-3 pt-3 border-t" style="border-color: var(--border);">
<button
@click="removeBook('{ book.MediaItemID }')"
@@ -227,16 +226,12 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
</div>
</div>
<!-- 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"
@@ -262,13 +257,11 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
style="color: var(--text-primary)"
></button>
</div>
<!-- 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"
@@ -282,7 +275,6 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
hx-include="#book-picker-filters"
/>
</div>
<!-- Author -->
<div class="flex-1 min-w-[150px]">
<input
type="text"
@@ -296,7 +288,6 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
hx-include="#book-picker-filters"
/>
</div>
<!-- Genre -->
<div class="flex-1 min-w-[150px]">
<input
type="text"
@@ -310,7 +301,6 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
hx-include="#book-picker-filters"
/>
</div>
<!-- Clear -->
<div>
<button
@click="$store.bookPicker.clearFilters()"
@@ -321,23 +311,16 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
</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>
<!-- 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>
<!-- Selected Count & Submit -->
<div
class="p-4 border-t flex justify-between items-center"
style="border-color: var(--border);"