feat: add top picks for you page

This commit is contained in:
2026-06-06 13:34:18 +02:00
committed by Milas Holsting
parent 7d3aea8625
commit 77971d611c
6 changed files with 78 additions and 2 deletions

View File

@@ -121,6 +121,7 @@ func (h *AnimeHandler) Register(r *gin.Engine) {
r.GET("/api/catalog/continue", h.HandleCatalogContinue) r.GET("/api/catalog/continue", h.HandleCatalogContinue)
r.GET("/api/catalog/top-pick", h.HandleCatalogTopPickForYou) r.GET("/api/catalog/top-pick", h.HandleCatalogTopPickForYou)
r.GET("/discover", h.HandleDiscover) r.GET("/discover", h.HandleDiscover)
r.GET("/discover/top-picks", h.HandleDiscoverTopPicksForYou)
r.GET("/api/discover/trending", h.HandleDiscoverTrending) r.GET("/api/discover/trending", h.HandleDiscoverTrending)
r.GET("/api/discover/upcoming", h.HandleDiscoverUpcoming) r.GET("/api/discover/upcoming", h.HandleDiscoverUpcoming)
r.GET("/api/discover/top", h.HandleDiscoverTop) 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) { func (h *AnimeHandler) HandleDiscoverTrending(c *gin.Context) {
h.renderDiscoverSection(c, "Trending") h.renderDiscoverSection(c, "Trending")
} }

View File

@@ -16,6 +16,7 @@ const (
forYouMaxRecommendations = 10 forYouMaxRecommendations = 10
forYouCandidateFetchLimit = 60 forYouCandidateFetchLimit = 60
forYouResultLimit = 18 forYouResultLimit = 18
forYouFullResultLimit = 60
forYouProfileSearchLimit = 8 forYouProfileSearchLimit = 8
forYouProfileGenreSearches = 2 forYouProfileGenreSearches = 2
forYouProfileThemeSearches = 2 forYouProfileThemeSearches = 2

View File

@@ -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) { 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) == "" { if strings.TrimSpace(userID) == "" {
return domain.CatalogSectionData{Animes: []domain.Anime{}}, nil return domain.CatalogSectionData{Animes: []domain.Anime{}}, nil
} }
@@ -342,7 +354,7 @@ func (s *animeService) GetTopPickForYou(ctx context.Context, userID string) (dom
}) })
return domain.CatalogSectionData{ return domain.CatalogSectionData{
Animes: rerankRecommendationCandidates(candidates, forYouResultLimit), Animes: rerankRecommendationCandidates(candidates, resultLimit),
}, nil }, nil
} }

View File

@@ -134,6 +134,7 @@ type ReviewEntry struct {
type AnimeCatalogService interface { type AnimeCatalogService interface {
GetCatalogSection(ctx context.Context, userID string, section string) (CatalogSectionData, error) GetCatalogSection(ctx context.Context, userID string, section string) (CatalogSectionData, error)
GetTopPickForYou(ctx context.Context, userID string) (CatalogSectionData, error) GetTopPickForYou(ctx context.Context, userID string) (CatalogSectionData, error)
GetTopPicksForYou(ctx context.Context, userID string) (CatalogSectionData, error)
} }
type AnimeDiscoverService interface { type AnimeDiscoverService interface {

View File

@@ -1,5 +1,34 @@
{{define "title"}}Discover{{end}} {{define "title"}}Discover{{end}}
{{define "content"}} {{define "content"}}
{{if .IsTopPicks}}
<div class="flex flex-col gap-6 pb-12">
<div class="flex flex-wrap items-end justify-between gap-3">
<div class="min-w-0">
<h1 class="text-xl font-normal tracking-[-0.02em] text-foreground">Top Picks for You</h1>
<p class="mt-1 text-sm text-foreground-muted">The full ranked list from your current watchlist profile.</p>
</div>
<a href="/discover" class="text-sm text-foreground-muted transition-colors hover:text-foreground">Back to Discover</a>
</div>
{{if eq (len .Animes) 0}}
<div class="flex h-64 flex-col items-center justify-center gap-3 text-foreground-muted">
<svg class="h-10 w-10 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" /></svg>
<p class="text-sm text-foreground">No top picks yet</p>
<p class="text-sm">Add a few anime to your watchlist so the recommender has something to learn from.</p>
<div class="mt-2 flex gap-2">
<a href="/watchlist" class="!rounded-none inline-flex h-10 items-center justify-center bg-background-button px-4 text-sm font-normal text-foreground transition-colors hover:bg-background-button-hover focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent">Open watchlist</a>
<a href="/discover" class="!rounded-none inline-flex h-10 items-center justify-center bg-background-button px-4 text-sm font-normal text-foreground transition-colors hover:bg-background-button-hover focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent">Discover anime</a>
</div>
</div>
{{else}}
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7">
{{range .Animes}}
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
{{end}}
</div>
{{end}}
</div>
{{else}}
<div class="flex flex-col gap-12 pb-12"> <div class="flex flex-col gap-12 pb-12">
<section class="flex flex-col items-center justify-center bg-background-surface px-6 py-16 text-center"> <section class="flex flex-col items-center justify-center bg-background-surface px-6 py-16 text-center">
<div class="flex max-w-2xl flex-col items-center gap-5"> <div class="flex max-w-2xl flex-col items-center gap-5">
@@ -65,6 +94,7 @@
} }
</style> </style>
{{end}} {{end}}
{{end}}
{{define "discover_section"}} {{define "discover_section"}}
<div class="discover-grid"> <div class="discover-grid">

View File

@@ -45,7 +45,7 @@
<section id="top-pick-for-you-section" class="w-full" data-top-pick-carousel> <section id="top-pick-for-you-section" class="w-full" data-top-pick-carousel>
<div class="mb-4 flex items-end justify-between gap-3"> <div class="mb-4 flex items-end justify-between gap-3">
<h2 class="min-w-0 text-base font-normal text-foreground">Top Pick for You</h2> <h2 class="min-w-0 text-base font-normal text-foreground">Top Pick for You</h2>
<a href="/discover" class="group flex items-center gap-1 text-sm text-foreground-muted transition-colors hover:text-foreground"> <a href="/discover/top-picks" class="group flex items-center gap-1 text-sm text-foreground-muted transition-colors hover:text-foreground">
Explore more Explore more
<svg xmlns="http://www.w3.org/2000/svg" class="size-4 transition-transform group-hover:translate-x-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg> <svg xmlns="http://www.w3.org/2000/svg" class="size-4 transition-transform group-hover:translate-x-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>
</a> </a>