Create templates/series.templ with: - Library selector dropdown (sticky, same pattern as dashboard) - Loading spinner overlay for AJAX library switching - Series grid with stacked-cascade multi-cover cards - Empty state when no series found - Pagination with Previous/Next links - SeriesCard sub-template linking to filtered bookshelf view Add 'Series' nav link in header between 'All Books' and 'Collections'. Make series badge on book detail page clickable, linking to /bookshelf?series_filter=<name>&sort=series. Add 'Continue Series' option to restore system collection modal.
125 lines
4.6 KiB
Templ
125 lines
4.6 KiB
Templ
package templates
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
)
|
|
|
|
templ Series(user User, seriesList []SeriesCardData, libData []LibraryData, currentLibraryID string, totalPages int, currentPage int, errorMessage string) {
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>Series - Bookhoard</title>
|
|
<link href="/static/style.css" rel="stylesheet"/>
|
|
</head>
|
|
<body x-data="seriesPage" x-init="initSeriesPage()" class="theme-{ user.Theme }">
|
|
@Header(user, "/series")
|
|
<!-- Sticky Library Selector -->
|
|
<div class="sticky top-0 z-40 bg-opacity-95 backdrop-blur border-b" style="background-color: var(--bg-primary);">
|
|
<div class="w-full px-4 py-3 flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<label class="text-sm font-medium" style="color: var(--text-secondary)">Library:</label>
|
|
<select
|
|
id="library-select"
|
|
name="library_id"
|
|
class="px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
|
style="background-color: var(--bg-secondary); color: var(--text-primary);"
|
|
>
|
|
for _, lib := range libData {
|
|
if lib.ID == currentLibraryID {
|
|
<option value={ lib.ID } selected>{ lib.Name }</option>
|
|
} else {
|
|
<option value={ lib.ID }>{ lib.Name }</option>
|
|
}
|
|
}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
id="loading-spinner"
|
|
class="hidden fixed inset-0 bg-opacity-50 flex items-center justify-center z-50"
|
|
style="background-color: var(--bg-primary);"
|
|
>
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2" style="border-color: var(--accent);"></div>
|
|
</div>
|
|
<!-- Series Grid -->
|
|
<div id="series-container">
|
|
<main class="w-full px-4 py-8">
|
|
<h1 class="text-3xl font-bold mb-6" style="color: var(--text-primary)">Series</h1>
|
|
if len(seriesList) == 0 {
|
|
<div id="series-empty" class="text-center py-16" 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 Series Found</h3>
|
|
<p>Books with series metadata will appear here</p>
|
|
</div>
|
|
}
|
|
<div id="series-grid" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
|
for _, series := range seriesList {
|
|
@SeriesCard(series, currentLibraryID)
|
|
}
|
|
</div>
|
|
<!-- Pagination -->
|
|
if totalPages > 1 {
|
|
<div id="series-pagination" class="flex justify-center items-center gap-4 mt-8">
|
|
if currentPage > 1 {
|
|
<a
|
|
href={ fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage-1) }
|
|
class="px-4 py-2 rounded-lg border"
|
|
style="border-color: var(--border); color: var(--text-primary);"
|
|
>
|
|
← Previous
|
|
</a>
|
|
}
|
|
<span class="text-sm" style="color: var(--text-secondary)">
|
|
Page { fmt.Sprintf("%d", currentPage) } of { fmt.Sprintf("%d", totalPages) }
|
|
</span>
|
|
if currentPage < totalPages {
|
|
<a
|
|
href={ fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage+1) }
|
|
class="px-4 py-2 rounded-lg border"
|
|
style="border-color: var(--border); color: var(--text-primary);"
|
|
>
|
|
Next →
|
|
</a>
|
|
}
|
|
</div>
|
|
}
|
|
</main>
|
|
</div>
|
|
@ErrorToast(errorMessage)
|
|
</body>
|
|
</html>
|
|
}
|
|
|
|
templ SeriesCard(series SeriesCardData, libraryID string) {
|
|
<a href={ "/bookshelf?series_filter=" + url.QueryEscape(series.Name) + "&sort=series&library_id=" + libraryID } class="block">
|
|
<div class="series-card rounded-lg overflow-hidden cursor-pointer hover:shadow-lg transition-shadow" style="background-color: var(--bg-secondary);">
|
|
<div class={ "stacked-covers cover-count-" + fmt.Sprintf("%d", len(series.CoverPaths)) }>
|
|
for i, cover := range series.CoverPaths {
|
|
<img
|
|
src={ cover }
|
|
class={ "cover-img cover-" + fmt.Sprintf("%d", i) }
|
|
alt={ series.Name }
|
|
loading="lazy"
|
|
onerror="this.src='/static/placeholder-book.svg'"
|
|
/>
|
|
}
|
|
if len(series.CoverPaths) == 0 {
|
|
<div class="cover-img cover-0 flex items-center justify-center" style="background: var(--bg-primary);">
|
|
<span class="text-3xl">📚</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
<div class="series-card-info px-3 py-2">
|
|
<h3 class="font-semibold text-sm line-clamp-2" style="color: var(--text-primary)">{ series.Name }</h3>
|
|
<p class="text-xs mt-1" style="color: var(--text-secondary)">
|
|
{ fmt.Sprintf("%d", series.BookCount) } of { fmt.Sprintf("%d", series.TotalInSeries) } books
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
}
|