The /series page rendered its grid in one fixed order (newest first)
with no way to find a series beyond eyeballing cards. The page now has
a toolbar — series-name search, author filter, viewer-progress filter
and a 12-key sort select — wired as a plain GET form (the page's
existing model: the URL is the single source of truth, and the library
switcher now carries q/author/status/sort along on switch).
Extensibility is deliberate (user request): sorts live in a registry
(services.ResolveSeriesSort) mirrored by a CASE cascade in
GetDistinctSeries and a <select> in series.templ — a new sort is one
entry in each. The sort value is a BOUND PARAM compared inside the
cascade, never interpolated; unknown keys resolve to the default.
Filters: series-name matches in WHERE (the group key, so counts stay
true); author (BOOL_OR any-book match) and status (finished/reading/
unstarted vs the viewer's progress, same 0.999 threshold as the
filter_query status arms) in HAVING so COUNT(*) keeps reporting the
whole series. GetDistinctSeriesCount shares the JOIN + HAVING so
pagination totals match. The reading_progress join can't multiply rows
(the table is UNIQUE per media_item+user).
Cards gain the viewer's finished-count ("X of N read" + progress bar),
and the empty state distinguishes "no series at all" from "no matches —
clear filters". The JSON /api/series endpoint takes the same params and
gains read_count/last_read_at additively.
Integration test pins every sort key against a three-series fixture
with distinct created/author/count/progress answers, the filter matrix,
composition, unknown-sort fallback, card read-counts and the JSON
contract — all as the fresh regular user so the live reader's data is
untouched.
684 lines
30 KiB
Go
684 lines
30 KiB
Go
// Code generated by templ - DO NOT EDIT.
|
||
|
||
// templ: version: v0.3.1020
|
||
package templates
|
||
|
||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||
|
||
import "github.com/a-h/templ"
|
||
import templruntime "github.com/a-h/templ/runtime"
|
||
|
||
import (
|
||
"fmt"
|
||
"net/url"
|
||
"strconv"
|
||
)
|
||
|
||
// seriesSortOptions is the <select> mirror of the SQL CASE cascade in
|
||
// GetDistinctSeries and the registry in services.ResolveSeriesSort —
|
||
// every value here must have a matching SQL arm and registry entry.
|
||
var seriesSortOptions = []struct {
|
||
Value string
|
||
Label string
|
||
}{
|
||
{"recent", "Recently added"},
|
||
{"recent_asc", "Oldest added"},
|
||
{"name_asc", "Name (A–Z)"},
|
||
{"name_desc", "Name (Z–A)"},
|
||
{"author_asc", "Author (A–Z)"},
|
||
{"author_desc", "Author (Z–A)"},
|
||
{"books_desc", "Most books"},
|
||
{"books_asc", "Fewest books"},
|
||
{"progress_desc", "Most complete"},
|
||
{"progress_asc", "Least complete"},
|
||
{"last_read_desc", "Recently read"},
|
||
{"last_read_asc", "Least recently read"},
|
||
}
|
||
|
||
// seriesStatusOptions is the progress-filter <select> (mirror of the
|
||
// status registry in services.ResolveSeriesSort's sibling map).
|
||
var seriesStatusOptions = []struct {
|
||
Value string
|
||
Label string
|
||
}{
|
||
{"", "Any progress"},
|
||
{"unstarted", "Unstarted"},
|
||
{"reading", "In progress"},
|
||
{"finished", "Finished"},
|
||
}
|
||
|
||
// seriesPageURL builds a /series URL carrying the full page state so
|
||
// pagination and clear-filters links never drop sort/filter state.
|
||
func seriesPageURL(state SeriesPageState, libraryID string, page int) string {
|
||
vals := url.Values{}
|
||
if libraryID != "" {
|
||
vals.Set("library_id", libraryID)
|
||
}
|
||
if state.Sort != "" {
|
||
vals.Set("sort", state.Sort)
|
||
}
|
||
if state.Query != "" {
|
||
vals.Set("q", state.Query)
|
||
}
|
||
if state.Author != "" {
|
||
vals.Set("author", state.Author)
|
||
}
|
||
if state.Status != "" {
|
||
vals.Set("status", state.Status)
|
||
}
|
||
vals.Set("page", strconv.Itoa(page))
|
||
return "/series?" + vals.Encode()
|
||
}
|
||
|
||
// hasActiveSeriesFilters reports whether the state deviates from the
|
||
// page's defaults — used to swap the empty state for a
|
||
// "no matches" + clear-filters variant.
|
||
func hasActiveSeriesFilters(state SeriesPageState) bool {
|
||
return state.Query != "" || state.Author != "" || state.Status != "" || (state.Sort != "" && state.Sort != "recent")
|
||
}
|
||
|
||
func Series(user User, seriesList []SeriesCardData, libData []LibraryData, currentLibraryID string, totalPages int, currentPage int, state SeriesPageState, errorMessage string) templ.Component {
|
||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||
return templ_7745c5c3_CtxErr
|
||
}
|
||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||
if !templ_7745c5c3_IsBuffer {
|
||
defer func() {
|
||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err == nil {
|
||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||
}
|
||
}()
|
||
}
|
||
ctx = templ.InitializeContext(ctx)
|
||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||
if templ_7745c5c3_Var1 == nil {
|
||
templ_7745c5c3_Var1 = templ.NopComponent
|
||
}
|
||
ctx = templ.ClearChildren(ctx)
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!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\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var2 = []any{"theme-" + user.Theme}
|
||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body x-data=\"seriesPage\" x-init=\"initSeriesPage()\" class=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var3 string
|
||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 1, Col: 0}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = Header(user, "/series").Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = LibrarySwitcher(libData, currentLibraryID, "", nil).Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div id=\"series-container\"><main class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-6 flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = Icon("layers", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span><div><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Series</h1><p class=\"text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Grouped by series metadata</p></div></div><form id=\"series-filter-form\" method=\"get\" action=\"/series\" class=\"mb-6 flex flex-wrap items-center gap-3\"><input type=\"hidden\" name=\"library_id\" value=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var4 string
|
||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(currentLibraryID)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 102, Col: 69}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"><div class=\"relative flex-1 min-w-48\"><span class=\"absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none\" style=\"color: var(--text-secondary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = Icon("search", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span> <input type=\"text\" name=\"q\" value=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var5 string
|
||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(state.Query)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 110, Col: 27}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" placeholder=\"Search series…\" class=\"input w-full pl-9\" aria-label=\"Search series by name\"></div><input type=\"text\" name=\"author\" value=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var6 string
|
||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(state.Author)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 119, Col: 27}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" placeholder=\"Author\" class=\"input w-40\" aria-label=\"Filter series by author\"> <select name=\"status\" class=\"input w-auto cursor-pointer\" aria-label=\"Filter series by your progress\" @change=\"$el.form.requestSubmit()\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
for _, opt := range seriesStatusOptions {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<option value=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var7 string
|
||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(opt.Value)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 131, Col: 33}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if state.Status == opt.Value {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " selected")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var8 string
|
||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 131, Col: 87}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</option>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</select> <select name=\"sort\" class=\"input w-auto cursor-pointer\" aria-label=\"Sort series\" @change=\"$el.form.requestSubmit()\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
for _, opt := range seriesSortOptions {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<option value=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var9 string
|
||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(opt.Value)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 141, Col: 33}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if (state.Sort == "" && opt.Value == "recent") || state.Sort == opt.Value {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " selected")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var10 string
|
||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 141, Col: 132}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</option>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</select> <button type=\"submit\" class=\"btn btn-secondary\" aria-label=\"Apply filters\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = Icon("filter", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span>Apply</span></button></form>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if len(seriesList) == 0 {
|
||
if hasActiveSeriesFilters(state) {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div id=\"series-empty\" class=\"card text-center py-16 px-6\"><div class=\"grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = Icon("search", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Series Match</h3><p class=\"mb-4\" style=\"color: var(--text-secondary)\">Nothing matches the current search or filters</p><a href=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var11 templ.SafeURL
|
||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(seriesPageURL(SeriesPageState{}, currentLibraryID, 1))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 157, Col: 71}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" class=\"btn btn-secondary\">Clear filters</a></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
} else {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div id=\"series-empty\" class=\"card text-center py-16 px-6\"><div class=\"grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = Icon("layers", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Series Found</h3><p style=\"color: var(--text-secondary)\">Books with series metadata will appear here</p></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div id=\"series-grid\" class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
for _, series := range seriesList {
|
||
templ_7745c5c3_Err = SeriesCard(series).Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if totalPages > 1 {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<div id=\"series-pagination\" class=\"flex justify-center items-center gap-3 mt-10\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if currentPage > 1 {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<a href=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var12 templ.SafeURL
|
||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(seriesPageURL(state, currentLibraryID, currentPage-1))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 180, Col: 69}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" class=\"btn btn-secondary\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = Icon("chevron-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<span>Previous</span></a> ")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<span class=\"text-sm\" style=\"color: var(--text-secondary)\">Page ")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var13 string
|
||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", currentPage))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 188, Col: 45}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " of ")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var14 string
|
||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", totalPages))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 188, Col: 82}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</span> ")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if currentPage < totalPages {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<a href=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var15 templ.SafeURL
|
||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(seriesPageURL(state, currentLibraryID, currentPage+1))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 192, Col: 69}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" class=\"btn btn-secondary\"><span>Next</span>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = Icon("chevron-right", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</a>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</main></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = ErrorToast(errorMessage).Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</body></html>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func SeriesCard(series SeriesCardData) templ.Component {
|
||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||
return templ_7745c5c3_CtxErr
|
||
}
|
||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||
if !templ_7745c5c3_IsBuffer {
|
||
defer func() {
|
||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err == nil {
|
||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||
}
|
||
}()
|
||
}
|
||
ctx = templ.InitializeContext(ctx)
|
||
templ_7745c5c3_Var16 := templ.GetChildren(ctx)
|
||
if templ_7745c5c3_Var16 == nil {
|
||
templ_7745c5c3_Var16 = templ.NopComponent
|
||
}
|
||
ctx = templ.ClearChildren(ctx)
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<a href=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var17 templ.SafeURL
|
||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs("/series/detail?name=" + url.QueryEscape(series.Name) + "&library_id=" + url.QueryEscape(series.LibraryID))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 209, Col: 117}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" class=\"block\"><div class=\"series-card card rounded-2xl overflow-hidden cursor-pointer\" style=\"box-shadow: var(--shadow-card);\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var18 = []any{"stacked-covers cover-count-" + fmt.Sprintf("%d", len(series.CoverPaths))}
|
||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<div class=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var19 string
|
||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var18).String())
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 1, Col: 0}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
for i, cover := range series.CoverPaths {
|
||
var templ_7745c5c3_Var20 = []any{"cover-img cover-" + fmt.Sprintf("%d", i)}
|
||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<img src=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var21 string
|
||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(cover)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 214, Col: 17}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" class=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var22 string
|
||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var20).String())
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 1, Col: 0}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\" alt=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var23 string
|
||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(series.Name)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 216, Col: 23}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\" loading=\"lazy\"> ")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
if len(series.CoverPaths) == 0 {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<div class=\"cover-img cover-0 flex items-center justify-center\" style=\"background: var(--bg-primary); color: var(--text-secondary);\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = Icon("book", "h-8 w-8 opacity-60").Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</div><div class=\"series-card-info px-3 py-2\"><h3 class=\"font-semibold text-sm line-clamp-2\" style=\"color: var(--text-primary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var24 string
|
||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(series.Name)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 228, Col: 99}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</h3><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var25 string
|
||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", series.BookCount))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 230, Col: 42}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, " of ")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var26 string
|
||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", series.TotalInSeries))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 230, Col: 89}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, " books</p>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if series.ReadCount > 0 {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<div class=\"mt-1.5 h-1 rounded-full overflow-hidden\" style=\"background: var(--bg-primary);\"><div class=\"h-full rounded-full\" style=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var27 string
|
||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %d%%; background-color: var(--accent);", readPercent(series.ReadCount, series.BookCount)))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 236, Col: 124}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\"></div></div><p class=\"text-[10px] mt-0.5\" style=\"color: var(--text-secondary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var28 string
|
||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d of %d read", series.ReadCount, series.BookCount))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 240, Col: 72}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</p>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</div></div></a>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// readPercent converts (finished, total) into a clamped 0..100 int for
|
||
// the card's progress bar.
|
||
func readPercent(finished, total int64) int {
|
||
if total <= 0 {
|
||
return 0
|
||
}
|
||
pct := int(finished * 100 / total)
|
||
if pct < 0 {
|
||
return 0
|
||
}
|
||
if pct > 100 {
|
||
return 100
|
||
}
|
||
return pct
|
||
}
|
||
|
||
var _ = templruntime.GeneratedTemplate
|