perf: optimize page load with async HTMX fragments
This commit is contained in:
@@ -1,3 +1,43 @@
|
||||
{{define "anime_characters"}}
|
||||
<div class="mt-12 w-full">
|
||||
<h2 class="mb-6 text-lg font-normal text-neutral-300">Characters & Cast</h2>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{{range (slice . 0 (min (len .) 10))}}
|
||||
<div class="flex gap-3 bg-white/[0.02] p-3 ring-1 ring-white/5">
|
||||
<div class="h-16 w-12 shrink-0 overflow-hidden bg-white/5">
|
||||
<img src="{{.Character.Images.Jpg.ImageURL}}" alt="{{.Character.Name}}" class="h-full w-full object-cover" loading="lazy" />
|
||||
</div>
|
||||
<div class="flex flex-col justify-center overflow-hidden">
|
||||
<span class="truncate text-sm font-medium text-neutral-200">{{.Character.Name}}</span>
|
||||
<span class="truncate text-xs text-neutral-500">{{.Role}}</span>
|
||||
{{if .VoiceActors}}
|
||||
<span class="mt-1 truncate text-[11px] text-neutral-400">{{(index .VoiceActors 0).Person.Name}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "anime_recommendations"}}
|
||||
{{if .}}
|
||||
<div class="w-full">
|
||||
<h2 class="mb-6 text-lg font-normal text-neutral-300">Recommendations</h2>
|
||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8">
|
||||
{{range (slice . 0 (min (len .) 8))}}
|
||||
<a href="/anime/{{.Entry.MalID}}" class="group flex flex-col gap-2">
|
||||
<div class="aspect-2/3 overflow-hidden bg-white/5 shadow-md">
|
||||
<img src="{{.Entry.Images.Webp.LargeImageURL}}" alt="{{.Entry.Title}}" class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" loading="lazy" />
|
||||
</div>
|
||||
<span class="truncate text-xs font-medium text-neutral-400 transition-colors group-hover:text-white">{{.Entry.Title}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "title"}}{{.Anime.DisplayTitle}}{{end}}
|
||||
{{define "content"}}
|
||||
{{if .WatchlistIDs}}<script>initWatchlist({{.WatchlistIDs}})</script>{{end}}
|
||||
@@ -201,27 +241,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .Characters}}
|
||||
<div hx-get="/anime/{{$anime.MalID}}?section=characters" hx-trigger="load" hx-swap="outerHTML">
|
||||
<div class="mt-12 w-full">
|
||||
<h2 class="mb-6 text-lg font-normal text-neutral-300">Characters & Cast</h2>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{{range (slice .Characters 0 (min (len .Characters) 10))}}
|
||||
<div class="flex gap-3 bg-white/[0.02] p-3 ring-1 ring-white/5">
|
||||
<div class="h-16 w-12 shrink-0 overflow-hidden bg-white/5">
|
||||
<img src="{{.Character.Images.Jpg.ImageURL}}" alt="{{.Character.Name}}" class="h-full w-full object-cover" loading="lazy" />
|
||||
</div>
|
||||
<div class="flex flex-col justify-center overflow-hidden">
|
||||
<span class="truncate text-sm font-medium text-neutral-200">{{.Character.Name}}</span>
|
||||
<span class="truncate text-xs text-neutral-500">{{.Role}}</span>
|
||||
{{if .VoiceActors}}
|
||||
<span class="mt-1 truncate text-[11px] text-neutral-400">{{(index .VoiceActors 0).Person.Name}}</span>
|
||||
{{end}}
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 animate-pulse">
|
||||
{{range (seq 5)}}
|
||||
<div class="flex gap-3 bg-white/[0.02] p-3 ring-1 ring-white/5 h-20">
|
||||
<div class="h-16 w-12 shrink-0 bg-white/5"></div>
|
||||
<div class="flex flex-col justify-center gap-2 grow">
|
||||
<div class="h-3 w-2/3 bg-white/5 rounded"></div>
|
||||
<div class="h-2 w-1/2 bg-white/5 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<div hx-get="/api/watch-order?animeId={{$anime.MalID}}" hx-trigger="load">
|
||||
@@ -232,20 +267,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .Recommendations}}
|
||||
<div class="mt-12 w-full">
|
||||
<div hx-get="/anime/{{$anime.MalID}}?section=recommendations" hx-trigger="load" hx-swap="outerHTML">
|
||||
<div class="w-full">
|
||||
<h2 class="mb-6 text-lg font-normal text-neutral-300">Recommendations</h2>
|
||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8">
|
||||
{{range (slice .Recommendations 0 (min (len .Recommendations) 8))}}
|
||||
<a href="/anime/{{.Entry.MalID}}" class="group flex flex-col gap-2">
|
||||
<div class="aspect-2/3 overflow-hidden bg-white/5 shadow-md">
|
||||
<img src="{{.Entry.Images.Webp.LargeImageURL}}" alt="{{.Entry.Title}}" class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" loading="lazy" />
|
||||
</div>
|
||||
<span class="truncate text-xs font-medium text-neutral-400 transition-colors group-hover:text-white">{{.Entry.Title}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 animate-pulse">
|
||||
{{range (seq 8)}}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="aspect-2/3 bg-white/5"></div>
|
||||
<div class="h-3 w-full bg-white/5 rounded"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{end}}
|
||||
|
||||
@@ -14,10 +14,7 @@
|
||||
{{$imageUrl = $anime.Images.Jpg.LargeImageURL}}
|
||||
{{end}}
|
||||
|
||||
{{$displayTitle := $anime.Title}}
|
||||
{{if $anime.TitleEnglish}}
|
||||
{{$displayTitle = $anime.TitleEnglish}}
|
||||
{{end}}
|
||||
{{$displayTitle := $anime.DisplayTitle}}
|
||||
|
||||
<div class="flex w-full flex-col gap-2" data-id="{{$dataId}}">
|
||||
<a href="/anime/{{$anime.MalID}}" class="group relative flex aspect-2/3 w-full flex-col overflow-hidden bg-white/5 after:absolute after:inset-0 {{if $withActions}}after:bg-black/80 after:opacity-0 hover:after:opacity-100{{else}}after:bg-linear-to-t after:from-black/80 after:via-black/20 after:to-transparent after:opacity-80 hover:after:opacity-100{{end}} after:transition-opacity">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{{define "title"}}Discover{{end}}
|
||||
{{define "content"}}
|
||||
{{if .WatchlistIDs}}<script>initWatchlist({{.WatchlistIDs}})</script>{{end}}
|
||||
<script>
|
||||
let isFetchingRandom = false;
|
||||
|
||||
@@ -51,45 +50,36 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{/* Trending Section */}}
|
||||
<section class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold text-white">Trending This Season</h2>
|
||||
<a href="/browse?order_by=popularity&sort=desc" class="text-accent text-sm hover:underline">View all</a>
|
||||
</div>
|
||||
<div class="discover-grid">
|
||||
{{range $i, $anime := .Trending}}
|
||||
<div>
|
||||
{{template "anime_card" dict "Anime" $anime "WithActions" true "IsWatchlist" (index $.WatchlistMap $anime.MalID)}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div hx-get="/api/discover/trending" hx-trigger="load" hx-swap="outerHTML">
|
||||
{{template "discover_skeleton"}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{/* Upcoming Section */}}
|
||||
<section class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold text-white">Highly Anticipated</h2>
|
||||
<a href="/browse?status=upcoming&order_by=members&sort=desc" class="text-accent text-sm hover:underline">View all</a>
|
||||
</div>
|
||||
<div class="discover-grid">
|
||||
{{range $i, $anime := .Upcoming}}
|
||||
<div>
|
||||
{{template "anime_card" dict "Anime" $anime "WithActions" true "IsWatchlist" (index $.WatchlistMap $anime.MalID)}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div hx-get="/api/discover/upcoming" hx-trigger="load" hx-swap="outerHTML">
|
||||
{{template "discover_skeleton"}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{/* Top Section */}}
|
||||
<section class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold text-white">All-Time Greats</h2>
|
||||
<a href="/browse?order_by=score&sort=desc" class="text-accent text-sm hover:underline">View all</a>
|
||||
</div>
|
||||
<div class="discover-grid">
|
||||
{{range $i, $anime := .Top}}
|
||||
<div>
|
||||
{{template "anime_card" dict "Anime" $anime "WithActions" true "IsWatchlist" (index $.WatchlistMap $anime.MalID)}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div hx-get="/api/discover/top" hx-trigger="load" hx-swap="outerHTML">
|
||||
{{template "discover_skeleton"}}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -100,6 +90,34 @@
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #1f1f1f 25%, #2a2a2a 50%, #1f1f1f 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
}
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
{{end}}
|
||||
|
||||
{{define "discover_section"}}
|
||||
<div class="discover-grid">
|
||||
{{range .Animes}}
|
||||
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "discover_skeleton"}}
|
||||
<div class="discover-grid">
|
||||
{{range (seq 8)}}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="skeleton aspect-[2/3] w-full rounded-xl"></div>
|
||||
<div class="skeleton h-4 w-3/4 rounded"></div>
|
||||
<div class="skeleton h-3 w-1/2 rounded opacity-50"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -1,9 +1,8 @@
|
||||
{{define "title"}}Home{{end}}
|
||||
{{define "content"}}
|
||||
{{if .WatchlistIDs}}<script>initWatchlist({{.WatchlistIDs}})</script>{{end}}
|
||||
<div class="flex flex-col gap-10">
|
||||
{{if .ContinueWatching}}
|
||||
{{template "continue_watching" .ContinueWatching}}
|
||||
{{if .User}}
|
||||
<div hx-get="/api/catalog/continue" hx-trigger="load" hx-swap="outerHTML"></div>
|
||||
{{end}}
|
||||
|
||||
<section class="w-full">
|
||||
@@ -14,11 +13,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="transition-transform group-hover:translate-x-0.5"><path d="m9 18 6-6-6-6"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6">
|
||||
{{range .CurrentlyAiring}}
|
||||
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
||||
{{end}}
|
||||
<div hx-get="/api/catalog/airing" hx-trigger="load" hx-swap="outerHTML">
|
||||
{{template "catalog_skeleton"}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -30,12 +26,46 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="transition-transform group-hover:translate-x-0.5"><path d="m9 18 6-6-6-6"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6">
|
||||
{{range .MostPopular}}
|
||||
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
||||
{{end}}
|
||||
<div hx-get="/api/catalog/popular" hx-trigger="load" hx-swap="outerHTML">
|
||||
{{template "catalog_skeleton"}}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #1f1f1f 25%, #2a2a2a 50%, #1f1f1f 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
}
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "catalog_section"}}
|
||||
{{if eq .Section "Continue"}}
|
||||
{{if .ContinueWatching}}
|
||||
{{template "continue_watching" .ContinueWatching}}
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6">
|
||||
{{range .Animes}}
|
||||
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "catalog_skeleton"}}
|
||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6">
|
||||
{{range (seq 6)}}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="skeleton aspect-[2/3] w-full rounded-xl"></div>
|
||||
<div class="skeleton h-4 w-3/4 rounded"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user