diff --git a/scraper/internal/server/ui.go b/scraper/internal/server/ui.go index 34cd58d..7c127e3 100644 --- a/scraper/internal/server/ui.go +++ b/scraper/internal/server/ui.go @@ -548,18 +548,58 @@ const rankingTmpl = `
-
+
-
- +
+ + +
+ {{if .AllStatuses}} +
+ Status + {{range .AllStatuses}} + + {{end}} +
+ {{end}} + {{if .AllGenres}} +
+ Genre + {{range .AllGenres}} + + {{end}} +
+ {{end}} +
+ Library + + +
+
{{if gt .TotalPages 1}} -
+
Page: {{$cur := .CurrentPage}} @@ -627,7 +667,10 @@ const rankingTmpl = ` hx-target="#main-content" hx-push-url="true" hx-swap="innerHTML" - data-filter="{{.Title}} {{.Author}} {{.Status}} {{range .Genres}}{{.}} {{end}}" + data-filter="{{.Title}} {{.Author}}" + data-status="{{.Status}}" + data-genres="{{range .Genres}}{{.}}|{{end}}" + data-local="1" class="group flex gap-3 rounded-xl border border-teal-700 bg-teal-950 p-3 hover:border-teal-400 transition-colors min-w-0"> {{if .Cover}} cover @@ -657,7 +700,10 @@ const rankingTmpl = `
{{else}} -
{{if .Cover}} cover @@ -701,27 +747,112 @@ const rankingTmpl = ` ` @@ -856,6 +987,21 @@ func (s *Server) handleRanking(w http.ResponseWriter, r *http.Request) { t := template.Must(template.New("ranking").Parse(rankingTmpl)) var buf bytes.Buffer + + // Collect distinct genres and statuses across ALL items for facet filters. + genreSet := map[string]bool{} + statusSet := map[string]bool{} + for _, it := range rankingItems { + if it.Status != "" { + statusSet[it.Status] = true + } + for _, g := range it.Genres { + genreSet[g] = true + } + } + allGenres := sortedKeys(genreSet) + allStatuses := sortedKeys(statusSet) + _ = t.Execute(&buf, struct { Books interface{} CachedAt string @@ -864,6 +1010,8 @@ func (s *Server) handleRanking(w http.ResponseWriter, r *http.Request) { CurrentPage int TotalPages int TotalItems int + AllGenres []string + AllStatuses []string }{ Books: toRankingViewItems(pageItems, s.writer.LocalSlugs()), CachedAt: cachedAt, @@ -872,6 +1020,8 @@ func (s *Server) handleRanking(w http.ResponseWriter, r *http.Request) { CurrentPage: currentPage, TotalPages: totalPages, TotalItems: totalItems, + AllGenres: allGenres, + AllStatuses: allStatuses, }) s.respond(w, r, "Rankings", buf.String()) } @@ -2195,6 +2345,21 @@ func (s *Server) handleChapter(w http.ResponseWriter, r *http.Request) { // ─── helpers ────────────────────────────────────────────────────────────────── +// sortedKeys returns the keys of a string-bool map in sorted order. +func sortedKeys(m map[string]bool) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + // Simple insertion sort — sets are small (< 100 items). + for i := 1; i < len(out); i++ { + for j := i; j > 0 && out[j] < out[j-1]; j-- { + out[j], out[j-1] = out[j-1], out[j] + } + } + return out +} + // stripMarkdown removes Markdown syntax and returns clean plain text. func stripMarkdown(src string) string { src = regexp.MustCompile(`(?m)^#{1,6}\s+`).ReplaceAllString(src, "")