Files
bookhoard/templates/collections.templ
T
john-okeefe af7533529c refactor(templates): remove duplicate main.js script tags, consolidate to header component
Remove redundant <script src="/static/main.js" defer></script> tags from 17+
templates that include the @Header component, eliminating duplicate script
loading that was causing Alpine.js to initialize twice per page load.

The header.templ component now serves as the single source of truth for
main.js inclusion, following the DRY principle and ensuring consistent
script loading across all pages that use the header navigation.

Additionally, add type="button" attribute to all buttons in header navigation
to prevent default form submission behavior when buttons are clicked.

Changes:
- Remove main.js script tag from templates using @Header component
- Keep main.js in header.templ (line 279) as universal inclusion point
- Preserve main.js in special pages: index.templ, login.templ, register.templ
  (these don't use @Header and are standalone entry points)
- Add type="button" to theme toggle, theme selection, wood paneling, and user menu buttons
  to prevent unwanted form submissions or page navigation

Benefits:
- Eliminates Alpine.js double-initialization bug
- Reduces HTTP requests (one script load instead of two)
- Improves maintainability (add header, get scripts automatically)
- Fixes broken @click handlers on collections, devices, and other pages
- Prevents buttons from triggering default form submission behavior

Technical notes:
- Templates affected: admin, analytics, bookshelf, collection_rules,
  collections, conflicts, custom_section, dashboard, devices, docs,
  library, profile, progress, queue, unlinked_books
- No changes to entry pages (index, login, register) which don't use @Header
- HTMX script remains in individual templates (stateless, no double-load issue)
- All interactive buttons in header now explicitly marked type="button" to
  prevent default browser form submission behavior

Related to: previous commit fixing Vite code-splitting
2026-03-13 22:25:12 -04:00

267 lines
9.9 KiB
Templ
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package templates
import "bookhoard/internal/handlers"
templ Collection(user User, collections []CollectionData, 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="initializeCollectionWebSocket" class="theme-{ user.Theme }">
@Header(user, "/collections")
<!-- 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) {
<!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="initializeCollectionWebSocket" class="theme-{ user.Theme }">
@Header(user, "/collections")
<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">
Back to Collections
</button>
<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 type="button" class="btn-primary px-4 py-2 rounded-lg" style="opacity: 0.5; cursor: not-allowed;" disabled>
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 {
<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">
<!-- Checkbox and Book Info -->
<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>
<!-- Book Cover -->
<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>
<!-- Remove Button -->
<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>
}
</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>
</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>
</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);"
/>
</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>
</div>
</div>
</div>
</body>
<div
id="collection-data"
data-id="{collection.ID}"
data-library-id="{ libraryID }"
style="display: none;"
></div>
</html>
}