From b85b29aa13602dd4928576ebd3cf90be064be7f1 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sat, 6 Jun 2026 13:34:18 +0200 Subject: [PATCH] feat: add top picks for you page --- internal/anime/handler.go | 32 +++++++++++++++++++++++++++++++ internal/anime/recommendations.go | 1 + internal/anime/service.go | 14 +++++++++++++- internal/domain/anime.go | 1 + templates/discover.gohtml | 30 +++++++++++++++++++++++++++++ templates/index.gohtml | 2 +- 6 files changed, 78 insertions(+), 2 deletions(-) diff --git a/internal/anime/handler.go b/internal/anime/handler.go index 175ce07..1e9b39d 100644 --- a/internal/anime/handler.go +++ b/internal/anime/handler.go @@ -121,6 +121,7 @@ func (h *AnimeHandler) Register(r *gin.Engine) { r.GET("/api/catalog/continue", h.HandleCatalogContinue) r.GET("/api/catalog/top-pick", h.HandleCatalogTopPickForYou) r.GET("/discover", h.HandleDiscover) + r.GET("/discover/top-picks", h.HandleDiscoverTopPicksForYou) r.GET("/api/discover/trending", h.HandleDiscoverTrending) r.GET("/api/discover/upcoming", h.HandleDiscoverUpcoming) r.GET("/api/discover/top", h.HandleDiscoverTop) @@ -302,6 +303,37 @@ func (h *AnimeHandler) HandleDiscover(c *gin.Context) { }) } +func (h *AnimeHandler) HandleDiscoverTopPicksForYou(c *gin.Context) { + user := server.CurrentUser(c) + userID := server.CurrentUserID(c) + + data, err := h.svc.GetTopPicksForYou(c.Request.Context(), userID) + if err != nil { + observability.Warn( + "top_picks_for_you_fetch_failed", + "anime", + "", + map[string]any{ + "user_id": userID, + }, + err, + ) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + watchlistMap := h.watchlistMapForAnimes(c.Request.Context(), userID, data.Animes) + + c.HTML(http.StatusOK, "discover.gohtml", gin.H{ + "_fragment": "", + "CurrentPath": "/discover", + "User": user, + "Animes": data.Animes, + "WatchlistMap": watchlistMap, + "IsTopPicks": true, + }) +} + func (h *AnimeHandler) HandleDiscoverTrending(c *gin.Context) { h.renderDiscoverSection(c, "Trending") } diff --git a/internal/anime/recommendations.go b/internal/anime/recommendations.go index 4476e92..8c86543 100644 --- a/internal/anime/recommendations.go +++ b/internal/anime/recommendations.go @@ -16,6 +16,7 @@ const ( forYouMaxRecommendations = 10 forYouCandidateFetchLimit = 60 forYouResultLimit = 18 + forYouFullResultLimit = 60 forYouProfileSearchLimit = 8 forYouProfileGenreSearches = 2 forYouProfileThemeSearches = 2 diff --git a/internal/anime/service.go b/internal/anime/service.go index bc70afc..d8bac6b 100644 --- a/internal/anime/service.go +++ b/internal/anime/service.go @@ -109,6 +109,18 @@ func (s *animeService) GetDiscoverSection(ctx context.Context, userID string, se } func (s *animeService) GetTopPickForYou(ctx context.Context, userID string) (domain.CatalogSectionData, error) { + return s.getTopPicksForYou(ctx, userID, forYouResultLimit) +} + +func (s *animeService) GetTopPicksForYou(ctx context.Context, userID string) (domain.CatalogSectionData, error) { + return s.getTopPicksForYou(ctx, userID, forYouFullResultLimit) +} + +func (s *animeService) getTopPicksForYou( + ctx context.Context, + userID string, + resultLimit int, +) (domain.CatalogSectionData, error) { if strings.TrimSpace(userID) == "" { return domain.CatalogSectionData{Animes: []domain.Anime{}}, nil } @@ -342,7 +354,7 @@ func (s *animeService) GetTopPickForYou(ctx context.Context, userID string) (dom }) return domain.CatalogSectionData{ - Animes: rerankRecommendationCandidates(candidates, forYouResultLimit), + Animes: rerankRecommendationCandidates(candidates, resultLimit), }, nil } diff --git a/internal/domain/anime.go b/internal/domain/anime.go index 6f3300c..07d1b1c 100644 --- a/internal/domain/anime.go +++ b/internal/domain/anime.go @@ -134,6 +134,7 @@ type ReviewEntry struct { type AnimeCatalogService interface { GetCatalogSection(ctx context.Context, userID string, section string) (CatalogSectionData, error) GetTopPickForYou(ctx context.Context, userID string) (CatalogSectionData, error) + GetTopPicksForYou(ctx context.Context, userID string) (CatalogSectionData, error) } type AnimeDiscoverService interface { diff --git a/templates/discover.gohtml b/templates/discover.gohtml index 3dd1e37..8d2b5d2 100644 --- a/templates/discover.gohtml +++ b/templates/discover.gohtml @@ -1,5 +1,34 @@ {{define "title"}}Discover{{end}} {{define "content"}} +{{if .IsTopPicks}} +
+
+
+

Top Picks for You

+

The full ranked list from your current watchlist profile.

+
+ Back to Discover +
+ + {{if eq (len .Animes) 0}} +
+ +

No top picks yet

+

Add a few anime to your watchlist so the recommender has something to learn from.

+ +
+ {{else}} +
+ {{range .Animes}} + {{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}} + {{end}} +
+ {{end}} +
+{{else}}
@@ -65,6 +94,7 @@ } {{end}} +{{end}} {{define "discover_section"}}
diff --git a/templates/index.gohtml b/templates/index.gohtml index 1e04ecb..c78ee44 100644 --- a/templates/index.gohtml +++ b/templates/index.gohtml @@ -45,7 +45,7 @@

Top Pick for You

- + Explore more