feat(series): sort and filter the series page

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.
This commit is contained in:
John O'Keefe committed 2026-10-27 14:53:45 -04:00
1 parent 79f4d668f1
commit d1f9ad3b25
10 files changed
+1118 -154

No files matched your search

+13
View File
@@ -142,6 +142,19 @@ type SeriesCardData struct {
BookCount int64
TotalInSeries int
CoverPaths []string
// Viewer's finished books in this series (drives the card's
// progress bar / "X of N read" line).
ReadCount int64
}
// SeriesPageState is the /series page's sort/filter state, echoed back
// into the toolbar controls and the pagination URLs (the URL is the
// single source of truth on this plain-GET page).
type SeriesPageState struct {
Sort string
Query string
Author string
Status string
}
type PendingRegistrationData struct {