feat: sort selected genres first in filter dropdown
This commit is contained in:
@@ -106,15 +106,14 @@
|
|||||||
{{if .OrderBy}}<input type="hidden" name="order_by" value="{{.OrderBy}}">{{end}}
|
{{if .OrderBy}}<input type="hidden" name="order_by" value="{{.OrderBy}}">{{end}}
|
||||||
{{if .Sort}}<input type="hidden" name="sort" value="{{.Sort}}">{{end}}
|
{{if .Sort}}<input type="hidden" name="sort" value="{{.Sort}}">{{end}}
|
||||||
<input type="hidden" name="sfw" value="{{.SFW}}">
|
<input type="hidden" name="sfw" value="{{.SFW}}">
|
||||||
{{range .GenresList}}
|
{{range orderedGenres .GenresList $selectedGenreIDs}}
|
||||||
{{$genreID := .MalID}}
|
{{$genre := .Genre}}
|
||||||
{{$isSelected := hasGenre $genreID $selectedGenreIDs}}
|
<label class="flex cursor-pointer items-center gap-3 px-2 py-1.5 text-sm text-foreground-muted hover:bg-surface-hover hover:text-foreground {{if .StartsInactive}}border-t border-white/15 mt-1 pt-2{{end}}">
|
||||||
<label class="flex cursor-pointer items-center gap-3 px-2 py-1.5 text-sm text-foreground-muted hover:bg-surface-hover hover:text-foreground">
|
<input id="genre-{{$genre.MalID}}" type="checkbox" class="sr-only" name="genres" value="{{$genre.MalID}}" data-checkbox-visual data-genre-visual {{if .Selected}}checked{{end}}>
|
||||||
<input id="genre-{{.MalID}}" type="checkbox" class="sr-only" name="genres" value="{{.MalID}}" data-checkbox-visual data-genre-visual {{if $isSelected}}checked{{end}}>
|
<div class="flex h-5 w-5 items-center justify-center border-2 transition-colors {{if .Selected}}border-accent bg-foreground-muted/12{{else}}border-white/45 bg-transparent{{end}}">
|
||||||
<div class="flex h-5 w-5 items-center justify-center border-2 transition-colors {{if $isSelected}}border-accent bg-foreground-muted/12{{else}}border-white/45 bg-transparent{{end}}">
|
<svg class="{{if not .Selected}}hidden {{end}}h-3.5 w-3.5 text-accent" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M20 6 9 17l-5-5" /></svg>
|
||||||
<svg class="{{if not $isSelected}}hidden {{end}}h-3.5 w-3.5 text-accent" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M20 6 9 17l-5-5" /></svg>
|
|
||||||
</div>
|
</div>
|
||||||
{{.Name}}
|
{{$genre.Name}}
|
||||||
</label>
|
</label>
|
||||||
{{end}}
|
{{end}}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"mal/internal/domain"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -12,6 +13,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type genreOption struct {
|
||||||
|
Genre domain.Genre
|
||||||
|
Selected bool
|
||||||
|
StartsInactive bool
|
||||||
|
}
|
||||||
|
|
||||||
func dict(values ...any) (map[string]any, error) {
|
func dict(values ...any) (map[string]any, error) {
|
||||||
if len(values)%2 != 0 {
|
if len(values)%2 != 0 {
|
||||||
return nil, fmt.Errorf("dict expects even number of values, got %d", len(values))
|
return nil, fmt.Errorf("dict expects even number of values, got %d", len(values))
|
||||||
@@ -160,6 +167,31 @@ func hasGenre(id int, genres []int) bool {
|
|||||||
return slices.Contains(genres, id)
|
return slices.Contains(genres, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func orderedGenreOptions(genres []domain.Genre, selectedGenres []int) []genreOption {
|
||||||
|
selected := make(map[int]struct{}, len(selectedGenres))
|
||||||
|
for _, genreID := range selectedGenres {
|
||||||
|
selected[genreID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]genreOption, 0, len(genres))
|
||||||
|
inactive := make([]genreOption, 0, len(genres))
|
||||||
|
for _, genre := range genres {
|
||||||
|
_, isSelected := selected[genre.MalID]
|
||||||
|
option := genreOption{Genre: genre, Selected: isSelected}
|
||||||
|
if isSelected {
|
||||||
|
out = append(out, option)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inactive = append(inactive, option)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(out) > 0 && len(inactive) > 0 {
|
||||||
|
inactive[0].StartsInactive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(out, inactive...)
|
||||||
|
}
|
||||||
|
|
||||||
func div(a, b float64) float64 {
|
func div(a, b float64) float64 {
|
||||||
if b == 0 {
|
if b == 0 {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package templates
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"mal/internal/domain"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -445,6 +446,63 @@ func TestGenresParamsEmpty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOrderedGenreOptionsSelectedFirst(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got := orderedGenreOptions(
|
||||||
|
[]domain.Genre{
|
||||||
|
{MalID: 1, Name: "Action"},
|
||||||
|
{MalID: 2, Name: "Comedy"},
|
||||||
|
{MalID: 3, Name: "Shounen"},
|
||||||
|
{MalID: 4, Name: "Drama"},
|
||||||
|
},
|
||||||
|
[]int{3, 1},
|
||||||
|
)
|
||||||
|
|
||||||
|
wantIDs := []int{1, 3, 2, 4}
|
||||||
|
if len(got) != len(wantIDs) {
|
||||||
|
t.Fatalf("expected %d genres, got %d", len(wantIDs), len(got))
|
||||||
|
}
|
||||||
|
for i, wantID := range wantIDs {
|
||||||
|
if got[i].Genre.MalID != wantID {
|
||||||
|
t.Fatalf("genre at index %d = %d, want %d", i, got[i].Genre.MalID, wantID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !got[0].Selected || !got[1].Selected {
|
||||||
|
t.Fatalf("expected first two genres to be selected, got %+v", got[:2])
|
||||||
|
}
|
||||||
|
if !got[2].StartsInactive {
|
||||||
|
t.Fatalf("expected first inactive genre to start divider, got %+v", got[2])
|
||||||
|
}
|
||||||
|
if got[3].StartsInactive {
|
||||||
|
t.Fatalf("expected divider only on first inactive genre, got %+v", got[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrderedGenreOptionsNoSelectedGenresPreservesOrder(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got := orderedGenreOptions(
|
||||||
|
[]domain.Genre{
|
||||||
|
{MalID: 1, Name: "Action"},
|
||||||
|
{MalID: 2, Name: "Comedy"},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, option := range got {
|
||||||
|
if option.Selected {
|
||||||
|
t.Fatalf("expected genre at index %d to be inactive", i)
|
||||||
|
}
|
||||||
|
if option.StartsInactive {
|
||||||
|
t.Fatalf("expected no inactive divider without active genres")
|
||||||
|
}
|
||||||
|
if option.Genre.MalID != i+1 {
|
||||||
|
t.Fatalf("genre at index %d = %d, want %d", i, option.Genre.MalID, i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPosterURLWebp(t *testing.T) {
|
func TestPosterURLWebp(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ func rendererFuncs() template.FuncMap {
|
|||||||
"browseURL": browseURL,
|
"browseURL": browseURL,
|
||||||
"genresParams": genresParams,
|
"genresParams": genresParams,
|
||||||
"hasGenre": hasGenre,
|
"hasGenre": hasGenre,
|
||||||
|
"orderedGenres": orderedGenreOptions,
|
||||||
"add": func(a, b int) int { return a + b },
|
"add": func(a, b int) int { return a + b },
|
||||||
"sub": func(a, b int) int { return a - b },
|
"sub": func(a, b int) int { return a - b },
|
||||||
"mul": func(a, b float64) float64 { return a * b },
|
"mul": func(a, b float64) float64 { return a * b },
|
||||||
|
|||||||
Reference in New Issue
Block a user