feat(series): multi-field search, sort-direction toggle, apply placement
Toolbar feedback round: the series search box now matches whatever the main search matches for a group — series NAME, any book's AUTHOR, and any book's TITLE inside the series (WHERE EXISTS per group-row, so counts stay true); the separate author input is gone from the toolbar (the author= param stays live for the JSON API and deep links). The sort control becomes a base-key select (Date added/Name/Author/Books/ Progress/Last read) plus a chevron direction toggle that flips and resubmits; sort/dir compose through ResolveSeriesSort (full registry keys keep working for back-compat, unknown base keys fall to recent). The search/apply button moved next to the search box as requested, and default URLs stay clean (sort/dir only emitted once the user chooses).
This commit is contained in:
1 parent
14285ff314
commit
96c1228805
11 files changed
+504
-149
No files matched your search
+33
-31
@@ -8,23 +8,19 @@ import (
|
||||
|
||||
// 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.
|
||||
// every value here must have a matching base sort in
|
||||
// services.seriesBaseSorts (the direction toggle composes the full
|
||||
// registry key).
|
||||
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"},
|
||||
{"recent", "Date added"},
|
||||
{"name", "Name"},
|
||||
{"author", "Author"},
|
||||
{"books", "Books"},
|
||||
{"progress", "Progress"},
|
||||
{"last_read", "Last read"},
|
||||
}
|
||||
|
||||
// seriesStatusOptions is the progress-filter <select> (mirror of the
|
||||
@@ -49,12 +45,12 @@ func seriesPageURL(state SeriesPageState, libraryID string, page int) string {
|
||||
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.Author != "" {
|
||||
vals.Set("author", state.Author)
|
||||
}
|
||||
if state.Status != "" {
|
||||
vals.Set("status", state.Status)
|
||||
}
|
||||
@@ -66,7 +62,7 @@ func seriesPageURL(state SeriesPageState, libraryID string, page int) string {
|
||||
// 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")
|
||||
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) {
|
||||
@@ -108,19 +104,15 @@ templ Series(user User, seriesList []SeriesCardData, libData []LibraryData, curr
|
||||
type="text"
|
||||
name="q"
|
||||
value={ state.Query }
|
||||
placeholder="Search series…"
|
||||
placeholder="Search series, authors, titles…"
|
||||
class="input w-full pl-9"
|
||||
aria-label="Search series by name"
|
||||
aria-label="Search series by name, author, or title"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="author"
|
||||
value={ state.Author }
|
||||
placeholder="Author"
|
||||
class="input w-40"
|
||||
aria-label="Filter series by author"
|
||||
/>
|
||||
<button type="submit" class="btn btn-secondary" aria-label="Search">
|
||||
@Icon("search", "h-4 w-4")
|
||||
<span>Search</span>
|
||||
</button>
|
||||
<select
|
||||
name="status"
|
||||
class="input w-auto cursor-pointer"
|
||||
@@ -134,16 +126,26 @@ templ Series(user User, seriesList []SeriesCardData, libData []LibraryData, curr
|
||||
<select
|
||||
name="sort"
|
||||
class="input w-auto cursor-pointer"
|
||||
aria-label="Sort series"
|
||||
aria-label="Sort series by"
|
||||
@change="$el.form.requestSubmit()"
|
||||
>
|
||||
for _, opt := range seriesSortOptions {
|
||||
<option value={ opt.Value } selected?={ (state.Sort == "" && opt.Value == "recent") || state.Sort == opt.Value }>{ opt.Label }</option>
|
||||
}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-secondary" aria-label="Apply filters">
|
||||
@Icon("filter", "h-4 w-4")
|
||||
<span>Apply</span>
|
||||
<input type="hidden" name="dir" value={ state.Dir } x-ref="sortDir"/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary px-3"
|
||||
aria-label="Toggle sort direction"
|
||||
title="Toggle sort direction"
|
||||
@click="flipSortDir()"
|
||||
>
|
||||
if state.Dir == "asc" {
|
||||
@Icon("chevron-up", "h-4 w-4")
|
||||
} else {
|
||||
@Icon("chevron-down", "h-4 w-4")
|
||||
}
|
||||
</button>
|
||||
</form>
|
||||
if len(seriesList) == 0 {
|
||||
|
||||
Reference in new issue
Block a user