package templates import ( "fmt" "net/url" "strconv" ) // seriesSortOptions is the (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.Dir != "" { vals.Set("dir", state.Dir) } if state.Query != "" { vals.Set("q", state.Query) } 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.Status != "" || (state.Sort != "" && state.Sort != "recent") } templ Series(user User, seriesList []SeriesCardData, libData []LibraryData, currentLibraryID string, totalPages int, currentPage int, state SeriesPageState, errorMessage string) { Series - Bookhoard @Header(user, "/series") @LibrarySwitcher(libData, currentLibraryID, "", nil)
@Icon("layers", "h-5 w-5")

Series

Grouped by series metadata

@Icon("search", "h-4 w-4")
if len(seriesList) == 0 { if hasActiveSeriesFilters(state) {
@Icon("search", "h-7 w-7")

No Series Match

Nothing matches the current search or filters

Clear filters
} else {
@Icon("layers", "h-7 w-7")

No Series Found

Books with series metadata will appear here

} }
for _, series := range seriesList { @SeriesCard(series) }
if totalPages > 1 {
if currentPage > 1 { @Icon("chevron-left", "h-4 w-4") Previous } Page { fmt.Sprintf("%d", currentPage) } of { fmt.Sprintf("%d", totalPages) } if currentPage < totalPages { Next @Icon("chevron-right", "h-4 w-4") }
}
@ErrorToast(errorMessage) } templ SeriesCard(series SeriesCardData) {
for i, cover := range series.CoverPaths { { } if len(series.CoverPaths) == 0 {
@Icon("book", "h-8 w-8 opacity-60")
}

{ series.Name }

{ fmt.Sprintf("%d", series.BookCount) } of { fmt.Sprintf("%d", series.TotalInSeries) } books

if series.ReadCount > 0 {

{ fmt.Sprintf("%d of %d read", series.ReadCount, series.BookCount) }

}
} // 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 }