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
359 lines
12 KiB
Templ
359 lines
12 KiB
Templ
package templates
|
||
|
||
import "bookhoard/internal/handlers"
|
||
|
||
templ Collection(user User, collections []CollectionData, libData []LibraryData, currentLibraryID string, errorMessage string) {
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>Collections - Bookhoard</title>
|
||
<script src="/static/htmx.min.js"></script>
|
||
<link href="/static/style.css" rel="stylesheet"/>
|
||
</head>
|
||
<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 -->
|
||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||
<div class="mb-8 flex justify-between items-center">
|
||
<div>
|
||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">My Collections</h1>
|
||
<p style="color: var(--text-secondary)">Organize your books into custom collections</p>
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<button
|
||
hx-get="/collections/restore-modal"
|
||
hx-target="#modal-container"
|
||
hx-swap="innerHTML"
|
||
class="btn-secondary px-4 py-2 rounded-lg"
|
||
>
|
||
🔄 Restore System
|
||
</button>
|
||
<button
|
||
hx-get="/collections/create-modal"
|
||
hx-target="#modal-container"
|
||
hx-swap="innerHTML"
|
||
class="btn-primary px-4 py-2 rounded-lg"
|
||
>
|
||
➕ New Collection
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div id="collections-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
if len(collections) == 0 {
|
||
<div class="text-center py-16 col-span-full" style="color: var(--text-secondary)">
|
||
<div class="text-6xl mb-4">📚</div>
|
||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Collections Yet</h3>
|
||
<p class="mb-4">Create collections to organize your books</p>
|
||
<button
|
||
hx-get="/collections/create-modal"
|
||
hx-target="#modal-container"
|
||
hx-swap="innerHTML"
|
||
class="btn-primary px-4 py-2 rounded-lg"
|
||
>
|
||
Create Your First Collection
|
||
</button>
|
||
</div>
|
||
}
|
||
for _, col := range collections {
|
||
<div @click="navigateToCollection($el)" data-href={ "/collections/" + col.ID } class="block">
|
||
<div
|
||
class="card p-6 rounded-lg border-l-4 cursor-pointer hover:shadow-lg transition-shadow"
|
||
style="background-color: var(--bg-secondary);"
|
||
data-color={ col.Color }
|
||
>
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div class="text-3xl">{ col.Icon }</div>
|
||
<div class="flex space-x-2">
|
||
<button
|
||
hx-get={ "/collections/" + col.ID + "/edit-modal" }
|
||
hx-target="#modal-container"
|
||
hx-swap="innerHTML"
|
||
class="p-2 hover:opacity-80 rounded"
|
||
style="color: var(--text-secondary); background-color: var(--bg-primary);"
|
||
>
|
||
✏️
|
||
</button>
|
||
<button
|
||
hx-delete={ "/api/collections/" + col.ID + "" }
|
||
hx-redirect="/collections"
|
||
hx-confirm="Are you sure you want to delete this collection?"
|
||
class="p-2 hover:opacity-80 rounded"
|
||
style="color: var(--text-secondary); background-color: var(--bg-primary);"
|
||
>
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">{ col.Name }</h3>
|
||
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ col.Description }</p>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
@ErrorToast(errorMessage)
|
||
</body>
|
||
</html>
|
||
}
|
||
|
||
templ CollectionDetail(user User, collection CollectionData, books []handlers.BookInfo, libraryID string, libData []LibraryData) {
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>{ collection.Name } - Bookhoard</title>
|
||
<script src="/static/htmx.min.js"></script>
|
||
<link href="/static/style.css" rel="stylesheet"/>
|
||
</head>
|
||
<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">
|
||
<a href="/collections" class="btn-secondary px-4 py-2 rounded-lg mb-4 inline-block">
|
||
← Back to Collections
|
||
</a>
|
||
<div class="flex items-center gap-4">
|
||
<div class="text-4xl" style="color: { collection.Color }">{ collection.Icon }</div>
|
||
<div>
|
||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1>
|
||
<p style="color: var(--text-secondary)">{ collection.Description }</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="mb-6 flex justify-between items-center">
|
||
<div class="flex items-center gap-4">
|
||
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Books in this Collection</h2>
|
||
<span id="selected-count" class="hidden px-3 py-1 text-sm rounded" style="background-color: var(--accent); color: var(--bg-primary);">
|
||
0 selected
|
||
</span>
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<div class="flex-1 max-w-md">
|
||
<input
|
||
type="text"
|
||
id="collection-search"
|
||
placeholder="Search within collection..."
|
||
onkeyup="filterCollectionBooks()"
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
/>
|
||
</div>
|
||
<button
|
||
id="bulk-remove-btn"
|
||
disabled
|
||
class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
🗑️ Remove Selected
|
||
</button>
|
||
<button
|
||
@click="$store.bookPicker.open()"
|
||
class="btn-primary px-4 py-2 rounded-lg"
|
||
>
|
||
➕ Add Books
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div id="books-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||
if len(books) == 0 {
|
||
<div id="empty-state" class="col-span-full text-center py-16" style="color: var(--text-secondary)">No books in this collection yet.</div>
|
||
}
|
||
for _, book := range books {
|
||
<a href={ "/media/" + book.MediaItemID }>
|
||
<div
|
||
class="card p-4 rounded-lg border hover:shadow-lg transition-shadow"
|
||
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
||
>
|
||
<div class="flex gap-4">
|
||
<div class="flex-shrink-0 pt-1">
|
||
<input
|
||
type="checkbox"
|
||
onchange="toggleBookForRemoval('{ book.MediaItemID }')"
|
||
class="w-5 h-5"
|
||
/>
|
||
</div>
|
||
<div class="flex-1 min-w-0">
|
||
<h3
|
||
class="font-semibold text-lg mb-1 line-clamp-2"
|
||
style="color: var(--text-primary)"
|
||
>
|
||
{ book.Title }
|
||
</h3>
|
||
if book.Author != "" {
|
||
<p
|
||
class="text-sm line-clamp-1"
|
||
style="color: var(--text-secondary)"
|
||
>
|
||
by { book.Author }
|
||
</p>
|
||
}
|
||
</div>
|
||
<div class="flex-shrink-0 w-16 sm:w-20">
|
||
if book.CoverImagePath != "" {
|
||
<img
|
||
src={ book.CoverImagePath }
|
||
alt="Cover"
|
||
class="w-full aspect-[3/4] object-cover rounded shadow-md"
|
||
onerror="this.src='/static/placeholder-book.svg'"
|
||
/>
|
||
} else {
|
||
<img
|
||
src="/static/placeholder-book.svg"
|
||
alt="Cover"
|
||
class="w-full aspect-[3/4] object-cover rounded shadow-md"
|
||
/>
|
||
}
|
||
</div>
|
||
</div>
|
||
<div class="mt-3 pt-3 border-t" style="border-color: var(--border);">
|
||
<button
|
||
@click="removeBook('{ book.MediaItemID }')"
|
||
class="px-3 py-1 text-sm border rounded hover:opacity-80"
|
||
style="border-color: var(--border); color: var(--text-secondary);"
|
||
>
|
||
🗑️ Remove from Collection
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</a>
|
||
}
|
||
</div>
|
||
</div>
|
||
<!-- Book Picker Modal -->
|
||
<div
|
||
x-data="bookPicker"
|
||
@keyup.escape.window="$store.bookPicker.close()"
|
||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||
style="display: none;"
|
||
>
|
||
<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>
|
||
<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="/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>
|
||
<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>
|
||
<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>
|
||
<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>
|
||
<form id="filter-form" class="hidden">
|
||
<input type="hidden" name="limit" value="50"/>
|
||
<input type="hidden" name="offset" value="0"/>
|
||
</form>
|
||
</div>
|
||
<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"
|
||
>
|
||
</div>
|
||
<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>
|
||
</body>
|
||
<div
|
||
id="collection-data"
|
||
data-id="{collection.ID}"
|
||
data-library-id="{ libraryID }"
|
||
style="display: none;"
|
||
></div>
|
||
</html>
|
||
}
|