diff --git a/api/anime/handler.go b/api/anime/handler.go index 7a17d48..3ece93e 100644 --- a/api/anime/handler.go +++ b/api/anime/handler.go @@ -81,6 +81,21 @@ func (h *Handler) HandleCatalog(w http.ResponseWriter, r *http.Request) { templates.Catalog().Render(r.Context(), w) } +func (h *Handler) watchlistMap(ctx context.Context, userID string) map[int]string { + if userID == "" { + return nil + } + entries, err := h.db.GetUserWatchList(ctx, userID) + if err != nil { + return nil + } + m := make(map[int]string, len(entries)) + for _, e := range entries { + m[int(e.AnimeID)] = e.Status + } + return m +} + func (h *Handler) HandleSearch(w http.ResponseWriter, r *http.Request) { w.Header().Set("Vary", "HX-Request") @@ -101,7 +116,8 @@ func (h *Handler) HandleSearch(w http.ResponseWriter, r *http.Request) { http.Error(w, "Failed to search anime", http.StatusInternalServerError) return } - templates.SearchResultsWrapper(query, res.Animes, 2, res.HasNextPage).Render(r.Context(), w) + statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) + templates.SearchResultsWrapper(query, res.Animes, statuses, 2, res.HasNextPage).Render(r.Context(), w) return } @@ -125,7 +141,8 @@ func (h *Handler) HandleAPISearch(w http.ResponseWriter, r *http.Request) { res.Animes = deduplicateAnimes(res.Animes) - templates.SearchItems(query, res.Animes, page+1, res.HasNextPage).Render(r.Context(), w) + statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) + templates.SearchItems(query, res.Animes, statuses, page+1, res.HasNextPage).Render(r.Context(), w) } func (h *Handler) HandleAPICatalog(w http.ResponseWriter, r *http.Request) { @@ -134,7 +151,8 @@ func (h *Handler) HandleAPICatalog(w http.ResponseWriter, r *http.Request) { result, err := h.jikanClient.GetTopAnime(r.Context(), page) if err == nil { result.Animes = deduplicateAnimes(result.Animes) - templates.CatalogItems(result.Animes, page+1, result.HasNextPage).Render(r.Context(), w) + statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) + templates.CatalogItems(result.Animes, statuses, page+1, result.HasNextPage).Render(r.Context(), w) return } @@ -211,6 +229,7 @@ func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) { return } + statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) switch section { case "relations": relations, err := h.jikanClient.GetFullRelations(r.Context(), id) @@ -222,7 +241,7 @@ func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) { writeInlineLoadError(w, "Failed to load relations.") return } - animecomponents.RelationsList(relations).Render(r.Context(), w) + animecomponents.RelationsList(relations, statuses).Render(r.Context(), w) case "recommendations": recs, err := h.jikanClient.GetRecommendations(r.Context(), id, 12) if err != nil { @@ -230,7 +249,7 @@ func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) { writeInlineLoadError(w, "Failed to load recommendations.") return } - animecomponents.Recommendations(recs).Render(r.Context(), w) + animecomponents.Recommendations(recs, statuses).Render(r.Context(), w) case "episodes": currentEpisode := r.URL.Query().Get("current") episodes, err := h.getEpisodes(r.Context(), id) @@ -344,7 +363,8 @@ func (h *Handler) HandleAPIDiscoverAiring(w http.ResponseWriter, r *http.Request res.Animes = deduplicateAnimes(res.Animes) - templates.DiscoverItems(res.Animes, "airing", page+1, res.HasNextPage).Render(r.Context(), w) + statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) + templates.DiscoverItems(res.Animes, statuses, "airing", page+1, res.HasNextPage).Render(r.Context(), w) } func (h *Handler) HandleAPIDiscoverUpcoming(w http.ResponseWriter, r *http.Request) { @@ -359,7 +379,8 @@ func (h *Handler) HandleAPIDiscoverUpcoming(w http.ResponseWriter, r *http.Reque res.Animes = deduplicateAnimes(res.Animes) - templates.DiscoverItems(res.Animes, "upcoming", page+1, res.HasNextPage).Render(r.Context(), w) + statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) + templates.DiscoverItems(res.Animes, statuses, "upcoming", page+1, res.HasNextPage).Render(r.Context(), w) } func (h *Handler) HandleStudioDetails(w http.ResponseWriter, r *http.Request) { @@ -387,14 +408,15 @@ func (h *Handler) HandleStudioDetails(w http.ResponseWriter, r *http.Request) { log.Printf("studio anime fetch error for %d: %v", id, err) if jikan.IsRetryableError(err) || errors.Is(err, context.Canceled) { // Render page with empty anime list if API is rate limiting - templates.StudioDetails(producer, []jikan.Anime{}, false, 2).Render(r.Context(), w) + templates.StudioDetails(producer, []jikan.Anime{}, nil, false, 2).Render(r.Context(), w) return } http.Error(w, "Failed to fetch studio anime", http.StatusInternalServerError) return } - templates.StudioDetails(producer, result.Animes, result.HasNextPage, 2).Render(r.Context(), w) + statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) + templates.StudioDetails(producer, result.Animes, statuses, result.HasNextPage, 2).Render(r.Context(), w) } func (h *Handler) HandleAPIStudioAnime(w http.ResponseWriter, r *http.Request) { @@ -427,5 +449,6 @@ func (h *Handler) HandleAPIStudioAnime(w http.ResponseWriter, r *http.Request) { result.Animes = deduplicateAnimes(result.Animes) - templates.StudioAnimeItems(result.Animes, result.HasNextPage, id, page+1).Render(r.Context(), w) + statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) + templates.StudioAnimeItems(result.Animes, statuses, result.HasNextPage, id, page+1).Render(r.Context(), w) } diff --git a/web/components/anime/recommendations.templ b/web/components/anime/recommendations.templ index 98f70d2..53d0430 100644 --- a/web/components/anime/recommendations.templ +++ b/web/components/anime/recommendations.templ @@ -5,18 +5,19 @@ import ( ui "mal/web/components" ) -templ Recommendations(recs []jikan.Anime) { +templ Recommendations(recs []jikan.Anime, watchlistStatuses map[int]string) { if len(recs) > 0 {
for _, anime := range recs { @ui.AnimeCard(ui.AnimeCardProps{ - ID: anime.MalID, - Title: anime.DisplayTitle(), - ImageURL: anime.ImageURL(), - TitleEnglish: anime.TitleEnglish, - TitleJapanese: anime.TitleJapanese, - Airing: anime.Airing, - Synopsis: anime.Synopsis, + ID: anime.MalID, + Title: anime.DisplayTitle(), + ImageURL: anime.ImageURL(), + TitleEnglish: anime.TitleEnglish, + TitleJapanese: anime.TitleJapanese, + Airing: anime.Airing, + Synopsis: anime.Synopsis, + WatchlistStatus: watchlistStatuses[anime.MalID], }) }
diff --git a/web/components/anime/relations.templ b/web/components/anime/relations.templ index 63167a3..7a56cdf 100644 --- a/web/components/anime/relations.templ +++ b/web/components/anime/relations.templ @@ -5,18 +5,19 @@ import ( ui "mal/web/components" ) -templ RelationsList(relations []jikan.RelationEntry) { +templ RelationsList(relations []jikan.RelationEntry, watchlistStatuses map[int]string) { if len(relations) > 1 {
for _, rel := range relations { @ui.AnimeCard(ui.AnimeCardProps{ - ID: rel.Anime.MalID, - Title: rel.Anime.DisplayTitle(), - ImageURL: rel.Anime.ImageURL(), - TitleEnglish: rel.Anime.TitleEnglish, - TitleJapanese: rel.Anime.TitleJapanese, - Airing: rel.Anime.Airing, - CurrentNode: rel.IsCurrent, + ID: rel.Anime.MalID, + Title: rel.Anime.DisplayTitle(), + ImageURL: rel.Anime.ImageURL(), + TitleEnglish: rel.Anime.TitleEnglish, + TitleJapanese: rel.Anime.TitleJapanese, + Airing: rel.Anime.Airing, + CurrentNode: rel.IsCurrent, + WatchlistStatus: watchlistStatuses[rel.Anime.MalID], }) { if rel.IsCurrent {
@@ -31,5 +32,3 @@ templ RelationsList(relations []jikan.RelationEntry) {

No related anime found.

} } - - diff --git a/web/components/anime_card.templ b/web/components/anime_card.templ index b4e64ca..6522df6 100644 --- a/web/components/anime_card.templ +++ b/web/components/anime_card.templ @@ -87,7 +87,7 @@ templ animeCardPoster(props AnimeCardProps) { > Play - + } diff --git a/web/components/anime_list.templ b/web/components/anime_list.templ index 8c4569d..4b3e3b0 100644 --- a/web/components/anime_list.templ +++ b/web/components/anime_list.templ @@ -5,10 +5,10 @@ import ( "mal/integrations/jikan" ) -templ InfiniteAnimeList(animes []jikan.Anime, hasNext bool, nextURL string, containerID string) { +templ InfiniteAnimeList(animes []jikan.Anime, watchlistStatuses map[int]string, hasNext bool, nextURL string, containerID string) { for _, anime := range animes {
- @CatalogItem(anime) + @CatalogItem(anime, watchlistStatuses[anime.MalID])
} if hasNext { @@ -29,15 +29,16 @@ templ InfiniteAnimeList(animes []jikan.Anime, hasNext bool, nextURL string, cont } -templ CatalogItem(anime jikan.Anime) { +templ CatalogItem(anime jikan.Anime, watchlistStatus string) { @AnimeCard(AnimeCardProps{ - ID: anime.MalID, - Title: anime.DisplayTitle(), - ImageURL: anime.ImageURL(), - TitleEnglish: anime.TitleEnglish, - TitleJapanese: anime.TitleJapanese, - Airing: anime.Airing, - Synopsis: anime.Synopsis, - PlayHref: fmt.Sprintf("/watch/%d/1", anime.MalID), + ID: anime.MalID, + Title: anime.DisplayTitle(), + ImageURL: anime.ImageURL(), + TitleEnglish: anime.TitleEnglish, + TitleJapanese: anime.TitleJapanese, + Airing: anime.Airing, + Synopsis: anime.Synopsis, + PlayHref: fmt.Sprintf("/watch/%d/1", anime.MalID), + WatchlistStatus: watchlistStatus, }) } diff --git a/web/components/watchlist/card_button.templ b/web/components/watchlist/card_button.templ index 413f9a6..b96afd0 100644 --- a/web/components/watchlist/card_button.templ +++ b/web/components/watchlist/card_button.templ @@ -12,7 +12,7 @@ templ CardButton( inWatchlist bool, ) {
} } -templ SearchItems(query string, animes []jikan.Anime, nextPage int, hasNext bool) { - @ui.InfiniteAnimeList(animes, hasNext, string(templ.URL(fmt.Sprintf("/api/search?q=%s&page=%d", url.QueryEscape(query), nextPage))), "results") +templ SearchItems(query string, animes []jikan.Anime, watchlistStatuses map[int]string, nextPage int, hasNext bool) { + @ui.InfiniteAnimeList(animes, watchlistStatuses, hasNext, string(templ.URL(fmt.Sprintf("/api/search?q=%s&page=%d", url.QueryEscape(query), nextPage))), "results") } diff --git a/web/templates/studio.templ b/web/templates/studio.templ index 8849b1c..c2083f2 100644 --- a/web/templates/studio.templ +++ b/web/templates/studio.templ @@ -9,7 +9,7 @@ import ( "mal/web/shared/layout" ) -templ StudioDetails(producer jikan.ProducerResponse, animes []jikan.Anime, hasNext bool, nextPage int) { +templ StudioDetails(producer jikan.ProducerResponse, animes []jikan.Anime, watchlistStatuses map[int]string, hasNext bool, nextPage int) { @layout.Layout("mal - "+shared.GetProducerName(producer), true) {
@@ -45,12 +45,13 @@ templ StudioDetails(producer jikan.ProducerResponse, animes []jikan.Anime, hasNe for _, anime := range animes {
@components.AnimeCard(components.AnimeCardProps{ - ID: anime.MalID, - Title: anime.DisplayTitle(), - ImageURL: anime.ImageURL(), - TitleEnglish: anime.TitleEnglish, - TitleJapanese: anime.TitleJapanese, - Airing: anime.Airing, + ID: anime.MalID, + Title: anime.DisplayTitle(), + ImageURL: anime.ImageURL(), + TitleEnglish: anime.TitleEnglish, + TitleJapanese: anime.TitleJapanese, + Airing: anime.Airing, + WatchlistStatus: watchlistStatuses[anime.MalID], })
} @@ -72,16 +73,17 @@ templ StudioLoadMore(studioID int, nextPage int) { >
} -templ StudioAnimeItems(animes []jikan.Anime, hasNext bool, studioID int, nextPage int) { +templ StudioAnimeItems(animes []jikan.Anime, watchlistStatuses map[int]string, hasNext bool, studioID int, nextPage int) { for _, anime := range animes {
@components.AnimeCard(components.AnimeCardProps{ - ID: anime.MalID, - Title: anime.DisplayTitle(), - ImageURL: anime.ImageURL(), - TitleEnglish: anime.TitleEnglish, - TitleJapanese: anime.TitleJapanese, - Airing: anime.Airing, + ID: anime.MalID, + Title: anime.DisplayTitle(), + ImageURL: anime.ImageURL(), + TitleEnglish: anime.TitleEnglish, + TitleJapanese: anime.TitleJapanese, + Airing: anime.Airing, + WatchlistStatus: watchlistStatuses[anime.MalID], })
}