From f77788588edfb46304291971410ec3eeb0b7b94c Mon Sep 17 00:00:00 2001 From: mkelvers Date: Tue, 7 Apr 2026 14:08:08 +0200 Subject: [PATCH] feat: add discover page for airing anime --- internal/features/anime/handler.go | 38 +++++++ internal/features/anime/service.go | 8 ++ internal/jikan/client.go | 6 ++ internal/jikan/seasons.go | 51 ++++++++++ internal/server/routes.go | 3 + internal/templates/anime.templ | 104 +++++++++---------- internal/templates/anime_templ.go | 14 +-- internal/templates/auth.templ | 4 +- internal/templates/catalog.templ | 4 +- internal/templates/discovery.templ | 53 ++++++++++ internal/templates/discovery_templ.go | 128 ++++++++++++++++++++++++ internal/templates/index.templ | 4 +- internal/templates/layout.templ | 7 +- internal/templates/layout_templ.go | 2 +- internal/templates/sort_filter.templ | 24 ++--- internal/templates/sort_filter_templ.go | 8 +- internal/templates/watchlist.templ | 31 +++--- internal/templates/watchlist_templ.go | 44 ++++---- static/css/style.css | 29 ++++++ 19 files changed, 437 insertions(+), 125 deletions(-) create mode 100644 internal/jikan/seasons.go create mode 100644 internal/templates/discovery.templ create mode 100644 internal/templates/discovery_templ.go diff --git a/internal/features/anime/handler.go b/internal/features/anime/handler.go index 953eb03..e056018 100644 --- a/internal/features/anime/handler.go +++ b/internal/features/anime/handler.go @@ -172,3 +172,41 @@ func (h *Handler) HandleQuickSearch(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(output) } + +func (h *Handler) HandleDiscover(w http.ResponseWriter, r *http.Request) { + templates.Discover().Render(r.Context(), w) +} + +func (h *Handler) HandleAPIDiscoverAiring(w http.ResponseWriter, r *http.Request) { + pageStr := r.URL.Query().Get("page") + page, _ := strconv.Atoi(pageStr) + if page < 1 { + page = 1 + } + + res, err := h.svc.GetAiringAnime(page) + if err != nil { + log.Printf("airing anime error: %v", err) + http.Error(w, "Failed to fetch airing anime", http.StatusInternalServerError) + return + } + + templates.DiscoverItems(res.Animes, "airing", page+1, res.HasNextPage).Render(r.Context(), w) +} + +func (h *Handler) HandleAPIDiscoverUpcoming(w http.ResponseWriter, r *http.Request) { + pageStr := r.URL.Query().Get("page") + page, _ := strconv.Atoi(pageStr) + if page < 1 { + page = 1 + } + + res, err := h.svc.GetUpcomingAnime(page) + if err != nil { + log.Printf("upcoming anime error: %v", err) + http.Error(w, "Failed to fetch upcoming anime", http.StatusInternalServerError) + return + } + + templates.DiscoverItems(res.Animes, "upcoming", page+1, res.HasNextPage).Render(r.Context(), w) +} diff --git a/internal/features/anime/service.go b/internal/features/anime/service.go index e53d997..7e60522 100644 --- a/internal/features/anime/service.go +++ b/internal/features/anime/service.go @@ -28,6 +28,14 @@ func (s *Service) GetTopAnime(page int) (jikan.TopAnimeResult, error) { return s.jikanClient.GetTopAnime(page) } +func (s *Service) GetAiringAnime(page int) (jikan.TopAnimeResult, error) { + return s.jikanClient.GetSeasonsNow(page) +} + +func (s *Service) GetUpcomingAnime(page int) (jikan.TopAnimeResult, error) { + return s.jikanClient.GetSeasonsUpcoming(page) +} + func (s *Service) GetAnimeDetails(ctx context.Context, id int, userID string) (jikan.Anime, string, error) { anime, err := s.jikanClient.GetAnimeByID(id) if err != nil { diff --git a/internal/jikan/client.go b/internal/jikan/client.go index 41f9b69..24cbdd0 100644 --- a/internal/jikan/client.go +++ b/internal/jikan/client.go @@ -14,6 +14,8 @@ type Client struct { baseURL string cache *expirable.LRU[string, SearchResult] topCache *expirable.LRU[int, TopAnimeResult] + airingCache *expirable.LRU[int, TopAnimeResult] + upcomingCache *expirable.LRU[int, TopAnimeResult] animeCache *expirable.LRU[int, Anime] relationsCache *expirable.LRU[int, JikanRelationsResponse] } @@ -21,6 +23,8 @@ type Client struct { func NewClient() *Client { cache := expirable.NewLRU[string, SearchResult](500, nil, time.Hour*1) topCache := expirable.NewLRU[int, TopAnimeResult](100, nil, time.Hour*1) + airingCache := expirable.NewLRU[int, TopAnimeResult](100, nil, time.Hour*1) + upcomingCache := expirable.NewLRU[int, TopAnimeResult](100, nil, time.Hour*1) animeCache := expirable.NewLRU[int, Anime](1000, nil, time.Hour*24) relationsCache := expirable.NewLRU[int, JikanRelationsResponse](1000, nil, time.Hour*24) @@ -29,6 +33,8 @@ func NewClient() *Client { baseURL: "https://api.jikan.moe/v4", cache: cache, topCache: topCache, + airingCache: airingCache, + upcomingCache: upcomingCache, animeCache: animeCache, relationsCache: relationsCache, } diff --git a/internal/jikan/seasons.go b/internal/jikan/seasons.go new file mode 100644 index 0000000..ffed9c6 --- /dev/null +++ b/internal/jikan/seasons.go @@ -0,0 +1,51 @@ +package jikan + +import "fmt" + +// GetSeasonsNow fetches currently airing anime +func (c *Client) GetSeasonsNow(page int) (TopAnimeResult, error) { + if page < 1 { + page = 1 + } + if cached, ok := c.airingCache.Get(page); ok { + return cached, nil + } + + var result TopAnimeResponse + reqURL := fmt.Sprintf("%s/seasons/now?page=%d", c.baseURL, page) + if err := c.fetchWithRetry(reqURL, &result); err != nil { + return TopAnimeResult{}, err + } + + res := TopAnimeResult{ + Animes: result.Data, + HasNextPage: result.Pagination.HasNextPage, + } + + c.airingCache.Add(page, res) + return res, nil +} + +// GetSeasonsUpcoming fetches upcoming anime +func (c *Client) GetSeasonsUpcoming(page int) (TopAnimeResult, error) { + if page < 1 { + page = 1 + } + if cached, ok := c.upcomingCache.Get(page); ok { + return cached, nil + } + + var result TopAnimeResponse + reqURL := fmt.Sprintf("%s/seasons/upcoming?page=%d", c.baseURL, page) + if err := c.fetchWithRetry(reqURL, &result); err != nil { + return TopAnimeResult{}, err + } + + res := TopAnimeResult{ + Animes: result.Data, + HasNextPage: result.Pagination.HasNextPage, + } + + c.upcomingCache.Add(page, res) + return res, nil +} diff --git a/internal/server/routes.go b/internal/server/routes.go index d7adfa1..6960da0 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -34,6 +34,9 @@ func NewRouter(cfg Config) http.Handler { // Anime / Search / Catalog mux.HandleFunc("/", animeHandler.HandleCatalog) + mux.HandleFunc("/discover", animeHandler.HandleDiscover) + mux.HandleFunc("/api/discover/airing", animeHandler.HandleAPIDiscoverAiring) + mux.HandleFunc("/api/discover/upcoming", animeHandler.HandleAPIDiscoverUpcoming) mux.HandleFunc("/search", animeHandler.HandleSearch) mux.HandleFunc("/api/search", animeHandler.HandleAPISearch) mux.HandleFunc("/api/search-quick", animeHandler.HandleQuickSearch) diff --git a/internal/templates/anime.templ b/internal/templates/anime.templ index 36c6057..9cc63d0 100644 --- a/internal/templates/anime.templ +++ b/internal/templates/anime.templ @@ -11,7 +11,7 @@ templ AnimeDetails(anime jikan.Anime, currentStatus string) {
if anime.ImageURL() != "" { - { + { } else {
no image
} @@ -116,54 +116,54 @@ templ AnimeDetails(anime jikan.Anime, currentStatus string) { { joinNames(anime.Studios) }
} - if len(anime.Producers) > 0 { - - } - if anime.Source != "" { - - } - if len(anime.Demographics) > 0 { -
-
all watching @@ -31,9 +30,7 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt dropped completed
- @SortFilter(SortFilterOptions{Sort: sortBy, Order: sortOrder, View: layout, Status: currentStatus}) - if len(entries) == 0 {
nothing here yet
@@ -54,16 +51,16 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt @@ -86,16 +83,16 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt { - - - { entry.DisplayTitle() } - - - - diff --git a/internal/templates/watchlist_templ.go b/internal/templates/watchlist_templ.go index c699c75..ae8d5af 100644 --- a/internal/templates/watchlist_templ.go +++ b/internal/templates/watchlist_templ.go @@ -132,7 +132,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var10 templ.SafeURL 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: 112} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 25, Col: 112} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -167,7 +167,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var13 templ.SafeURL 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: 117} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 26, Col: 117} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -202,7 +202,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var16 templ.SafeURL templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=continuing&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: 119} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 27, Col: 119} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { @@ -237,7 +237,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var19 templ.SafeURL templ_7745c5c3_Var19, 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: 29, Col: 116} + 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_Var19)) if templ_7745c5c3_Err != nil { @@ -272,7 +272,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var22 templ.SafeURL templ_7745c5c3_Var22, 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: 30, Col: 122} + 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_Var22)) if templ_7745c5c3_Err != nil { @@ -307,7 +307,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var25 templ.SafeURL templ_7745c5c3_Var25, 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: 31, Col: 116} + 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_Var25)) if templ_7745c5c3_Err != nil { @@ -342,7 +342,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var28 templ.SafeURL templ_7745c5c3_Var28, 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: 32, Col: 118} + 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_Var28)) if templ_7745c5c3_Err != nil { @@ -412,7 +412,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var30 string templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("watchlist-entry-%d", entry.AnimeID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 54, Col: 100} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 51, Col: 100} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { @@ -425,7 +425,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var31 templ.SafeURL templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/anime/%d", entry.AnimeID))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 55, Col: 67} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 52, Col: 67} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { @@ -443,7 +443,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var32 string templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(entry.ImageUrl) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 57, Col: 34} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 54, Col: 34} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) if templ_7745c5c3_Err != nil { @@ -456,7 +456,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var33 string templ_7745c5c3_Var33, 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: 63} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 54, Col: 63} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { @@ -479,7 +479,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var34 string 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: 62, 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_Var34)) if templ_7745c5c3_Err != nil { @@ -492,7 +492,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var35 string templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/api/watchlist/%d?from=watchlist", entry.AnimeID)))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 65, Col: 101} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 62, Col: 101} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { @@ -505,7 +505,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var36 string templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("#watchlist-entry-%d", entry.AnimeID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 66, Col: 69} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 63, Col: 69} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) if templ_7745c5c3_Err != nil { @@ -533,7 +533,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var37 string templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("watchlist-entry-%d", entry.AnimeID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 83, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 80, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) if templ_7745c5c3_Err != nil { @@ -546,7 +546,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var38 templ.SafeURL templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/anime/%d", entry.AnimeID))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 85, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 82, Col: 73} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) if templ_7745c5c3_Err != nil { @@ -559,7 +559,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var39 string templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(entry.ImageUrl) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 86, Col: 35} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 83, Col: 35} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) if templ_7745c5c3_Err != nil { @@ -572,7 +572,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var40 string templ_7745c5c3_Var40, 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: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 83, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) if templ_7745c5c3_Err != nil { @@ -585,7 +585,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var41 templ.SafeURL templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/anime/%d", entry.AnimeID))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 90, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 87, Col: 73} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) if templ_7745c5c3_Err != nil { @@ -598,7 +598,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta 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} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 88, Col: 32} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) if templ_7745c5c3_Err != nil { @@ -611,7 +611,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var43 string templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/api/watchlist/%d?from=watchlist", entry.AnimeID)))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 97, Col: 103} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 94, Col: 103} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { @@ -624,7 +624,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta var templ_7745c5c3_Var44 string templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("#watchlist-entry-%d", entry.AnimeID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 98, Col: 71} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 95, Col: 71} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) if templ_7745c5c3_Err != nil { diff --git a/static/css/style.css b/static/css/style.css index 2e8ace7..dcab42f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -973,3 +973,32 @@ a:visited { gap: 8px; } } + +/* Tabs */ +.tabs { + display: flex; + gap: 16px; + margin-bottom: 24px; + border-bottom: 1px solid var(--border); + padding-bottom: 8px; +} + +.tab { + background: none; + border: none; + color: var(--text-muted); + font-size: 16px; + cursor: pointer; + padding: 8px 16px; + transition: color 0.2s; + text-transform: lowercase; +} + +.tab:hover { + color: var(--text); +} + +.tab.active { + color: var(--link); + font-weight: bold; +}