diff --git a/internal/features/watchlist/handler.go b/internal/features/watchlist/handler.go
index 3464bf3..a9d895c 100644
--- a/internal/features/watchlist/handler.go
+++ b/internal/features/watchlist/handler.go
@@ -125,6 +125,15 @@ func (h *Handler) HandleGetWatchlist(w http.ResponseWriter, r *http.Request) {
}
statusFilter := r.URL.Query().Get("status")
+ sortBy := r.URL.Query().Get("sort")
+ sortOrder := r.URL.Query().Get("order")
+
+ if sortBy != "title" && sortBy != "score" {
+ sortBy = "date"
+ }
+ if sortOrder != "asc" {
+ sortOrder = "desc"
+ }
user, ok := r.Context().Value(middleware.UserContextKey).(*database.User)
if !ok || user == nil {
@@ -150,7 +159,10 @@ func (h *Handler) HandleGetWatchlist(w http.ResponseWriter, r *http.Request) {
filteredEntries = entries
}
- templates.Watchlist(filteredEntries, layout, statusFilter).Render(r.Context(), w)
+ // Sort entries
+ h.sortEntries(filteredEntries, sortBy, sortOrder)
+
+ templates.Watchlist(filteredEntries, layout, statusFilter, sortBy, sortOrder).Render(r.Context(), w)
}
func (h *Handler) HandleExportWatchlist(w http.ResponseWriter, r *http.Request) {
@@ -214,3 +226,40 @@ func (h *Handler) HandleImportWatchlist(w http.ResponseWriter, r *http.Request)
w.Header().Set("HX-Redirect", "/watchlist")
w.WriteHeader(http.StatusOK)
}
+
+func (h *Handler) sortEntries(entries []database.GetUserWatchListRow, sortBy, sortOrder string) {
+ var less func(int, int) bool
+
+ switch sortBy {
+ case "title":
+ less = func(i, j int) bool {
+ cmp := entries[i].TitleOriginal < entries[j].TitleOriginal
+ if sortOrder == "asc" {
+ return cmp
+ }
+ return !cmp
+ }
+ case "score":
+ less = func(i, j int) bool {
+ // Score is stored as JSON in the DB, for now just keep default order
+ return false
+ }
+ default: // "date"
+ less = func(i, j int) bool {
+ cmp := entries[i].UpdatedAt.After(entries[j].UpdatedAt)
+ if sortOrder == "asc" {
+ return !cmp
+ }
+ return cmp
+ }
+ }
+
+ // Simple bubble sort for small lists
+ for i := 0; i < len(entries); i++ {
+ for j := i + 1; j < len(entries); j++ {
+ if less(j, i) {
+ entries[i], entries[j] = entries[j], entries[i]
+ }
+ }
+ }
+}
diff --git a/internal/jikan/search.go b/internal/jikan/search.go
index 400904a..94999e8 100644
--- a/internal/jikan/search.go
+++ b/internal/jikan/search.go
@@ -34,7 +34,7 @@ func (c *Client) Search(query string, page int) (SearchResult, error) {
return res, nil
}
-// GetTopAnime fetches the top anime by popularity
+// GetTopAnime fetches the top anime by popularity (default) or other filters
func (c *Client) GetTopAnime(page int) (TopAnimeResult, error) {
if page < 1 {
page = 1
diff --git a/internal/templates/sort_filter.templ b/internal/templates/sort_filter.templ
new file mode 100644
index 0000000..76a84de
--- /dev/null
+++ b/internal/templates/sort_filter.templ
@@ -0,0 +1,46 @@
+package templates
+
+type SortFilterOptions struct {
+ Sort string // "title", "date", "score"
+ Order string // "asc", "desc"
+ View string // for watchlist: "grid", "table"
+ Status string // for watchlist: "all", "watching", etc
+}
+
+templ SortFilter(opts SortFilterOptions) {
+
nothing here yet
diff --git a/internal/templates/watchlist_templ.go b/internal/templates/watchlist_templ.go
index a22e27a..c37dc66 100644
--- a/internal/templates/watchlist_templ.go
+++ b/internal/templates/watchlist_templ.go
@@ -13,7 +13,7 @@ import (
"malago/internal/database"
)
-func Watchlist(entries []database.GetUserWatchListRow, layout string, currentStatus string) templ.Component {
+func Watchlist(entries []database.GetUserWatchListRow, layout string, currentStatus string, sortBy string, sortOrder string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -60,9 +60,9 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL
- templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=grid&status=%s", currentStatus)))
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=grid&status=%s&sort=%s&order=%s", currentStatus, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 19, Col: 86}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 19, Col: 122}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -95,9 +95,9 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 templ.SafeURL
- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=table&status=%s", currentStatus)))
+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=table&status=%s&sort=%s&order=%s", currentStatus, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 20, Col: 87}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 20, Col: 123}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -130,9 +130,9 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 templ.SafeURL
- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=all", layout)))
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=all&sort=%s&order=%s", layout, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 26, Col: 76}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 26, Col: 112}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -165,9 +165,9 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 templ.SafeURL
- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=watching", layout)))
+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=watching&sort=%s&order=%s", layout, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 27, Col: 81}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 27, Col: 117}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -200,9 +200,9 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 templ.SafeURL
- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=on_hold", layout)))
+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=on_hold&sort=%s&order=%s", layout, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 28, Col: 80}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 28, Col: 116}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@@ -235,9 +235,9 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 templ.SafeURL
- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=plan_to_watch", layout)))
+ templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=plan_to_watch&sort=%s&order=%s", layout, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 29, Col: 86}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 29, Col: 122}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
@@ -270,9 +270,9 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 templ.SafeURL
- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=dropped", layout)))
+ templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=dropped&sort=%s&order=%s", layout, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 30, Col: 80}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 30, Col: 116}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
@@ -305,9 +305,9 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 templ.SafeURL
- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=completed", layout)))
+ templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=completed&sort=%s&order=%s", layout, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 31, Col: 82}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 31, Col: 118}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
@@ -330,264 +330,272 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+ templ_7745c5c3_Err = SortFilter(SortFilterOptions{Sort: sortBy, Order: sortOrder, View: layout, Status: currentStatus}).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
if len(entries) == 0 {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
nothing here yet
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
nothing here yet
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if currentStatus == "all" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "your watchlist is empty.
search for anime to get started.")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "your watchlist is empty.
search for anime to get started.")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "no anime in this category.")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "no anime in this category.")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
if layout == "grid" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, entry := range entries {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if entry.ImageUrl != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" class=\"catalog-thumb\" loading=\"lazy\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "no image
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "no image
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(entry.DisplayTitle())
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 57, Col: 56}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 59, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
× ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\" hx-swap=\"delete\">×
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
title ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "title ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, entry := range entries {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(entry.DisplayTitle())
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 86, Col: 31}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 88, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " remove ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" hx-swap=\"delete\" style=\"background: none; border: none; cursor: pointer;\">remove")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/static/css/style.css b/static/css/style.css
index 1391b53..8b11c38 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -857,6 +857,43 @@ a:visited {
margin: 0;
}
+/* Sort Filter */
+.sort-filter {
+ display: flex;
+ gap: 16px;
+ margin-bottom: 16px;
+ padding: 12px;
+ background: var(--surface);
+ border: 1px solid var(--border);
+}
+
+.sort-filter-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.sort-filter-group label {
+ font-size: 12px;
+ color: var(--text-muted);
+ font-weight: 500;
+}
+
+.sort-filter-select {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ color: var(--text);
+ padding: 6px 8px;
+ font-size: 12px;
+ font-family: inherit;
+ cursor: pointer;
+}
+
+.sort-filter-select:focus {
+ outline: none;
+ border-color: var(--link);
+}
+
/* Responsive */
@media (max-width: 900px) {
.anime-page {