feat: add reviews subpage with infinite scroll

This commit is contained in:
2026-05-15 19:36:35 +02:00
parent 59fb0ed7f8
commit 0e5416aab3
2 changed files with 156 additions and 0 deletions

View File

@@ -413,3 +413,44 @@ func (h *AnimeHandler) HandleRandomAnime(c *gin.Context) {
"in_watchlist": inWatchlist,
})
}
func (h *AnimeHandler) HandleAnimeReviews(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
c.Status(http.StatusNotFound)
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
reviews, hasNextPage, err := h.svc.GetReviews(c.Request.Context(), id, page)
if err != nil {
c.Status(http.StatusInternalServerError)
return
}
user, _ := c.Get("User")
if c.GetHeader("HX-Request") == "true" && page > 1 {
c.HTML(http.StatusOK, "reviews.gohtml", gin.H{
"_fragment": "review_cards",
"Reviews": reviews,
"NextPage": page + 1,
"HasNextPage": hasNextPage,
"AnimeID": id,
})
return
}
c.HTML(http.StatusOK, "reviews.gohtml", gin.H{
"CurrentPath": fmt.Sprintf("/anime/%d/reviews", id),
"Reviews": reviews,
"NextPage": page + 1,
"HasNextPage": hasNextPage,
"AnimeID": id,
"User": user,
})
}

View File

@@ -0,0 +1,115 @@
{{define "title"}}Reviews{{end}}
{{define "content"}}
{{template "reviews_content" .}}
{{end}}
{{define "reviews_content"}}
<div class="flex w-full flex-col gap-6">
<a href="/anime/{{.AnimeID}}" class="text-sm text-foreground-muted transition-colors hover:text-foreground">&larr; Back to anime</a>
<h1 class="text-xl font-normal text-foreground">Reviews</h1>
{{if eq (len .Reviews) 0}}
<div class="flex h-64 flex-col items-center justify-center gap-4 text-foreground-muted">
<p class="text-foreground">No reviews yet</p>
</div>
{{else}}
<div id="reviews-list" class="flex flex-col gap-6">
{{template "review_cards" .}}
</div>
{{end}}
</div>
{{end}}
{{define "review_cards"}}
{{range .Reviews}}
<div class="flex flex-col gap-4 bg-background-surface p-6 ring-1 ring-border">
<div class="flex items-start justify-between">
<div class="flex items-center gap-3">
<div class="h-10 w-10 shrink-0 overflow-hidden rounded-full bg-background-surface">
<img src="{{.User.Images.Jpg.ImageURL}}" alt="{{.User.Username}}" class="h-full w-full object-cover" loading="lazy" />
</div>
<div class="flex flex-col">
<span class="text-sm font-medium text-foreground">{{.User.Username}}</span>
<span class="text-xs text-foreground-muted">{{formatDate .Date}}</span>
</div>
</div>
<div class="flex items-center gap-2">
{{if .IsPreliminary}}
<span class="rounded bg-yellow-500/10 px-2 py-0.5 text-xs text-yellow-500">Preliminary</span>
{{end}}
{{if .IsSpoiler}}
<span class="rounded bg-red-500/10 px-2 py-0.5 text-xs text-red-500">Spoiler</span>
{{end}}
<div class="flex items-center gap-1">
<svg class="h-4 w-4 fill-foreground" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>
<span class="text-sm font-medium text-foreground">{{.Score}}</span>
</div>
</div>
</div>
{{if .Tags}}
<div class="flex flex-wrap gap-2">
{{range .Tags}}
<span class="rounded bg-accent/10 px-2 py-0.5 text-xs text-accent">{{.}}</span>
{{end}}
</div>
{{end}}
<div class="whitespace-pre-line text-sm leading-relaxed text-foreground-muted">
{{.Review}}
</div>
<div class="flex flex-wrap items-center gap-4 border-t border-border pt-4 text-xs text-foreground-muted">
{{if .EpisodesSeen}}<span>{{.EpisodesSeen}} episodes seen</span>{{end}}
<div class="flex items-center gap-3">
{{if .Reactions.Nice}}
<span class="flex items-center gap-1" title="Nice">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2h0a3.13 3.13 0 0 1 3 3.88Z"/></svg>
{{.Reactions.Nice}}
</span>
{{end}}
{{if .Reactions.LoveIt}}
<span class="flex items-center gap-1" title="Love it">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>
{{.Reactions.LoveIt}}
</span>
{{end}}
{{if .Reactions.Funny}}
<span class="flex items-center gap-1" title="Funny">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>
{{.Reactions.Funny}}
</span>
{{end}}
{{if .Reactions.Confusing}}
<span class="flex items-center gap-1" title="Confusing">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
{{.Reactions.Confusing}}
</span>
{{end}}
{{if .Reactions.Informative}}
<span class="flex items-center gap-1" title="Informative">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
{{.Reactions.Informative}}
</span>
{{end}}
{{if .Reactions.WellWritten}}
<span class="flex items-center gap-1" title="Well written">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
{{.Reactions.WellWritten}}
</span>
{{end}}
{{if .Reactions.Creative}}
<span class="flex items-center gap-1" title="Creative">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14s.5-1 2-1 2 1 2 1"/><path d="M9 14s.5-1 2-1 2 1 2 1"/><circle cx="12" cy="12" r="10"/><path d="M9 16c.67.67 1.79 1 3 1s2.33-.33 3-1"/></svg>
{{.Reactions.Creative}}
</span>
{{end}}
</div>
</div>
</div>
{{end}}
{{if .HasNextPage}}
<div hx-get="/anime/{{.AnimeID}}/reviews?page={{.NextPage}}"
hx-trigger="intersect once"
hx-swap="outerHTML"
hx-target="this"
class="h-px"></div>
{{end}}
{{end}}