From 9223176344c2f5453b5be5ef6676846b8951a513 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Mon, 6 Apr 2026 23:38:30 +0200 Subject: [PATCH] add continuing tab to watchlist --- internal/database/models.go | 1 + internal/database/queries.sql | 10 +- internal/database/queries.sql.go | 20 +- internal/features/watchlist/handler.go | 20 +- internal/features/watchlist/service.go | 2 + internal/jikan/types.go | 1 + internal/templates/anime.templ | 18 +- internal/templates/anime_templ.go | 20 +- internal/templates/watchlist.templ | 3 + internal/templates/watchlist_templ.go | 336 ++++++++++++++----------- migrations/003_add_anime_airing.sql | 2 + 11 files changed, 253 insertions(+), 180 deletions(-) create mode 100644 migrations/003_add_anime_airing.sql diff --git a/internal/database/models.go b/internal/database/models.go index 9bd84f1..ef4861d 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -24,6 +24,7 @@ type Anime struct { CreatedAt time.Time `json:"created_at"` TitleEnglish sql.NullString `json:"title_english"` TitleJapanese sql.NullString `json:"title_japanese"` + Airing sql.NullBool `json:"airing"` } type Session struct { diff --git a/internal/database/queries.sql b/internal/database/queries.sql index c7e1339..e35f9f9 100644 --- a/internal/database/queries.sql +++ b/internal/database/queries.sql @@ -24,13 +24,14 @@ DELETE FROM session WHERE id = ?; DELETE FROM session WHERE user_id = ?; -- name: UpsertAnime :one -INSERT INTO anime (id, title_original, title_english, title_japanese, image_url) -VALUES (?, ?, ?, ?, ?) +INSERT INTO anime (id, title_original, title_english, title_japanese, image_url, airing) +VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET title_original = excluded.title_original, title_english = excluded.title_english, title_japanese = excluded.title_japanese, - image_url = excluded.image_url + image_url = excluded.image_url, + airing = excluded.airing RETURNING *; -- name: GetAnime :one @@ -54,7 +55,8 @@ SELECT a.title_original, a.title_english, a.title_japanese, - a.image_url + a.image_url, + a.airing FROM watch_list_entry e JOIN anime a ON e.anime_id = a.id WHERE e.user_id = ? diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index d17618f..735d106 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -93,7 +93,7 @@ func (q *Queries) DeleteWatchListEntry(ctx context.Context, arg DeleteWatchListE } const getAnime = `-- name: GetAnime :one -SELECT id, title_original, image_url, created_at, title_english, title_japanese FROM anime WHERE id = ? LIMIT 1 +SELECT id, title_original, image_url, created_at, title_english, title_japanese, airing FROM anime WHERE id = ? LIMIT 1 ` func (q *Queries) GetAnime(ctx context.Context, id int64) (Anime, error) { @@ -106,6 +106,7 @@ func (q *Queries) GetAnime(ctx context.Context, id int64) (Anime, error) { &i.CreatedAt, &i.TitleEnglish, &i.TitleJapanese, + &i.Airing, ) return i, err } @@ -164,7 +165,8 @@ SELECT a.title_original, a.title_english, a.title_japanese, - a.image_url + a.image_url, + a.airing FROM watch_list_entry e JOIN anime a ON e.anime_id = a.id WHERE e.user_id = ? @@ -182,6 +184,7 @@ type GetUserWatchListRow struct { TitleEnglish sql.NullString `json:"title_english"` TitleJapanese sql.NullString `json:"title_japanese"` ImageUrl string `json:"image_url"` + Airing sql.NullBool `json:"airing"` } func (q *Queries) GetUserWatchList(ctx context.Context, userID string) ([]GetUserWatchListRow, error) { @@ -204,6 +207,7 @@ func (q *Queries) GetUserWatchList(ctx context.Context, userID string) ([]GetUse &i.TitleEnglish, &i.TitleJapanese, &i.ImageUrl, + &i.Airing, ); err != nil { return nil, err } @@ -243,14 +247,15 @@ func (q *Queries) GetWatchListEntry(ctx context.Context, arg GetWatchListEntryPa } const upsertAnime = `-- name: UpsertAnime :one -INSERT INTO anime (id, title_original, title_english, title_japanese, image_url) -VALUES (?, ?, ?, ?, ?) +INSERT INTO anime (id, title_original, title_english, title_japanese, image_url, airing) +VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET title_original = excluded.title_original, title_english = excluded.title_english, title_japanese = excluded.title_japanese, - image_url = excluded.image_url -RETURNING id, title_original, image_url, created_at, title_english, title_japanese + image_url = excluded.image_url, + airing = excluded.airing +RETURNING id, title_original, image_url, created_at, title_english, title_japanese, airing ` type UpsertAnimeParams struct { @@ -259,6 +264,7 @@ type UpsertAnimeParams struct { TitleEnglish sql.NullString `json:"title_english"` TitleJapanese sql.NullString `json:"title_japanese"` ImageUrl string `json:"image_url"` + Airing sql.NullBool `json:"airing"` } func (q *Queries) UpsertAnime(ctx context.Context, arg UpsertAnimeParams) (Anime, error) { @@ -268,6 +274,7 @@ func (q *Queries) UpsertAnime(ctx context.Context, arg UpsertAnimeParams) (Anime arg.TitleEnglish, arg.TitleJapanese, arg.ImageUrl, + arg.Airing, ) var i Anime err := row.Scan( @@ -277,6 +284,7 @@ func (q *Queries) UpsertAnime(ctx context.Context, arg UpsertAnimeParams) (Anime &i.CreatedAt, &i.TitleEnglish, &i.TitleJapanese, + &i.Airing, ) return i, err } diff --git a/internal/features/watchlist/handler.go b/internal/features/watchlist/handler.go index a9d895c..4da97ce 100644 --- a/internal/features/watchlist/handler.go +++ b/internal/features/watchlist/handler.go @@ -44,6 +44,8 @@ func (h *Handler) HandleUpdateWatchlist(w http.ResponseWriter, r *http.Request) animeTitleJapanese := r.FormValue("anime_title_japanese") animeImage := r.FormValue("anime_image") status := r.FormValue("status") + airingStr := r.FormValue("airing") + airing := airingStr == "true" log.Printf("watchlist add: id=%s, title=%s", animeIDStr, animeTitle) @@ -60,6 +62,7 @@ func (h *Handler) HandleUpdateWatchlist(w http.ResponseWriter, r *http.Request) TitleJapanese: animeTitleJapanese, ImageURL: animeImage, Status: status, + Airing: airing, } if err := h.svc.AddEntry(r.Context(), user.ID, req); err != nil { @@ -67,7 +70,7 @@ func (h *Handler) HandleUpdateWatchlist(w http.ResponseWriter, r *http.Request) return } - templates.WatchlistDropdown(int(animeID), animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, status).Render(r.Context(), w) + templates.WatchlistDropdown(int(animeID), animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, status, airing).Render(r.Context(), w) } func (h *Handler) HandleDeleteWatchlist(w http.ResponseWriter, r *http.Request) { @@ -109,8 +112,12 @@ func (h *Handler) HandleDeleteWatchlist(w http.ResponseWriter, r *http.Request) if anime.TitleJapanese.Valid { titleJapanese = anime.TitleJapanese.String } + airing := false + if anime.Airing.Valid { + airing = anime.Airing.Bool + } - templates.WatchlistDropdown(int(animeID), anime.TitleOriginal, titleEnglish, titleJapanese, anime.ImageUrl, "").Render(r.Context(), w) + templates.WatchlistDropdown(int(animeID), anime.TitleOriginal, titleEnglish, titleJapanese, anime.ImageUrl, "", airing).Render(r.Context(), w) } func (h *Handler) HandleGetWatchlist(w http.ResponseWriter, r *http.Request) { @@ -148,7 +155,14 @@ func (h *Handler) HandleGetWatchlist(w http.ResponseWriter, r *http.Request) { } var filteredEntries []database.GetUserWatchListRow - if statusFilter != "" && statusFilter != "all" { + if statusFilter == "continuing" { + // Show airing anime with watching or plan_to_watch status + for _, entry := range entries { + if entry.Airing.Bool && (entry.Status == "watching" || entry.Status == "plan_to_watch") { + filteredEntries = append(filteredEntries, entry) + } + } + } else if statusFilter != "" && statusFilter != "all" { for _, entry := range entries { if entry.Status == statusFilter { filteredEntries = append(filteredEntries, entry) diff --git a/internal/features/watchlist/service.go b/internal/features/watchlist/service.go index d887489..428d25a 100644 --- a/internal/features/watchlist/service.go +++ b/internal/features/watchlist/service.go @@ -26,6 +26,7 @@ type AddRequest struct { TitleJapanese string ImageURL string Status string + Airing bool } func (s *Service) AddEntry(ctx context.Context, userID string, req AddRequest) error { @@ -39,6 +40,7 @@ func (s *Service) AddEntry(ctx context.Context, userID string, req AddRequest) e TitleEnglish: sql.NullString{String: req.TitleEnglish, Valid: req.TitleEnglish != ""}, TitleJapanese: sql.NullString{String: req.TitleJapanese, Valid: req.TitleJapanese != ""}, ImageUrl: req.ImageURL, + Airing: sql.NullBool{Bool: req.Airing, Valid: true}, }) if err != nil { return fmt.Errorf("failed to save anime reference: %w", err) diff --git a/internal/jikan/types.go b/internal/jikan/types.go index af6abab..627ddf4 100644 --- a/internal/jikan/types.go +++ b/internal/jikan/types.go @@ -46,6 +46,7 @@ type Anime struct { Rank int `json:"rank"` Popularity int `json:"popularity"` Status string `json:"status"` + Airing bool `json:"airing"` Episodes int `json:"episodes"` Season string `json:"season"` Year int `json:"year"` diff --git a/internal/templates/anime.templ b/internal/templates/anime.templ index c822256..1165756 100644 --- a/internal/templates/anime.templ +++ b/internal/templates/anime.templ @@ -36,7 +36,7 @@ templ AnimeDetails(anime jikan.Anime, currentStatus string) { }
- @WatchlistDropdown(anime.MalID, anime.Title, anime.TitleEnglish, anime.TitleJapanese, anime.ImageURL(), currentStatus) + @WatchlistDropdown(anime.MalID, anime.Title, anime.TitleEnglish, anime.TitleJapanese, anime.ImageURL(), currentStatus, anime.Airing)
@@ -188,7 +188,7 @@ func joinNames(entities []jikan.NamedEntity) string { return strings.Join(names, ", ") } -templ WatchlistDropdown(animeID int, animeTitle string, animeTitleEnglish string, animeTitleJapanese string, animeImage string, currentStatus string) { +templ WatchlistDropdown(animeID int, animeTitle string, animeTitleEnglish string, animeTitleJapanese string, animeImage string, currentStatus string, airing bool) { ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, entry := range entries { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var34 string - templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("watchlist-entry-%d", entry.AnimeID)) + templ_7745c5c3_Var34, 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: 80, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 62, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">
title
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, entry := range entries { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var42 string + templ_7745c5c3_Var42, 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: 91, Col: 31} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
title
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\">remove
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/migrations/003_add_anime_airing.sql b/migrations/003_add_anime_airing.sql new file mode 100644 index 0000000..8f74ee6 --- /dev/null +++ b/migrations/003_add_anime_airing.sql @@ -0,0 +1,2 @@ +-- Add airing status column to anime table +ALTER TABLE anime ADD COLUMN airing BOOLEAN DEFAULT 0;