Files
hstream/resources/views/livewire/playlists.blade.php
T
w33b df6246e3c9 feat(playlist): enhance public playlist index with hero stats and loading optimizations
Add a redesigned hero section to the public playlist page displaying
community-generated totals for playlists and episodes, plus new string
translations (English, German, French). Eager-load user, limited episode
previews, and first gallery image to reduce N+1 queries. Cache aggregate
statistics for 10 minutes to improve performance on high-traffic pages.
2026-08-09 10:36:03 +02:00

211 lines
14 KiB
PHP

<div>
<!-- Hero -->
<section
class="relative overflow-hidden rounded-3xl border border-neutral-200/70 bg-white/40 px-6 py-10 shadow-xl backdrop-blur-lg sm:px-10 sm:py-12 dark:border-neutral-800 dark:bg-neutral-950/40">
<div class="pointer-events-none absolute -right-24 -top-24 h-72 w-72 rounded-full bg-rose-600/20 blur-3xl"></div>
<div class="pointer-events-none absolute -bottom-24 -left-24 h-72 w-72 rounded-full bg-neutral-500/20 blur-3xl dark:bg-rose-900/10"></div>
<div class="relative z-10">
<h1 class="text-3xl font-bold text-neutral-900 sm:text-4xl dark:text-white">
{{ __('nav.public-playlists') }}
</h1>
<p class="mt-2 text-neutral-600 dark:text-neutral-400">
{{ __('playlist.hero-subtitle') }}
</p>
<div class="mt-6 flex flex-wrap items-center gap-3">
<span
class="inline-flex items-center gap-2 rounded-full border border-neutral-200/70 bg-white/60 px-4 py-2 text-sm font-semibold text-neutral-800 shadow-sm backdrop-blur dark:border-neutral-700 dark:bg-neutral-900/60 dark:text-neutral-100">
<i class="fa-solid fa-rectangle-list text-rose-600"></i>
{{ number_format($totalPlaylists) }} {{ __('playlist.playlists-count') }}
</span>
<span
class="inline-flex items-center gap-2 rounded-full border border-neutral-200/70 bg-white/60 px-4 py-2 text-sm font-semibold text-neutral-800 shadow-sm backdrop-blur dark:border-neutral-700 dark:bg-neutral-900/60 dark:text-neutral-100">
<i class="fa-solid fa-film text-rose-600"></i>
{{ number_format($totalEpisodes) }} {{ __('home.episodes') }}
</span>
</div>
<div class="mt-8 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="relative">
<label for="playlist-search" class="sr-only">Search</label>
<div class="pointer-events-none absolute inset-y-0 left-4 flex items-center">
<i class="fa-solid fa-magnifying-glass text-neutral-400"></i>
</div>
<input wire:model.live.debounce.600ms="search" type="search" id="playlist-search" placeholder="Search Playlist..."
class="w-full rounded-full border border-neutral-200/70 bg-white/60 py-3 pl-11 pr-12 text-sm text-neutral-900 placeholder-neutral-400 shadow-sm backdrop-blur transition focus:border-rose-500 focus:outline-none focus:ring-2 focus:ring-rose-500/30 dark:border-neutral-700 dark:bg-neutral-900/60 dark:text-white dark:placeholder-neutral-500">
<div class="absolute inset-y-0 right-4 flex items-center" wire:loading>
<svg aria-hidden="true" class="h-5 w-5 animate-spin text-neutral-400" viewBox="0 0 100 101" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor" />
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill" />
</svg>
</div>
</div>
<div class="relative">
<div class="pointer-events-none absolute inset-y-0 left-4 flex items-center">
<i class="fa-solid fa-sort text-neutral-400"></i>
</div>
<select wire:model.live="order"
class="w-full appearance-none rounded-full border border-neutral-200/70 bg-white/60 py-3 pl-11 pr-10 text-sm text-neutral-900 shadow-sm backdrop-blur transition focus:border-rose-500 focus:outline-none focus:ring-2 focus:ring-rose-500/30 dark:border-neutral-700 dark:bg-neutral-900/60 dark:text-white">
<option value="az">A-Z</option>
<option value="za">Z-A</option>
<option value="episode-count">Episode count</option>
<option value="newest">Newest</option>
<option value="oldest">Oldest</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-4 flex items-center">
<i class="fa-solid fa-chevron-down text-neutral-400"></i>
</div>
</div>
</div>
</div>
</section>
<!-- Grid -->
<div class="mt-8" wire:keydown.right.window="nextPage" wire:keydown.left.window="previousPage">
<div wire:loading.grid
class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
@for ($i = 0; $i < 8; $i++)
<div
class="overflow-hidden rounded-2xl border border-neutral-200/70 bg-white/60 shadow-md backdrop-blur-lg dark:border-neutral-800 dark:bg-neutral-950/50">
<div class="relative aspect-video overflow-hidden">
<div class="h-full w-full animate-pulse bg-neutral-200 dark:bg-neutral-800">
<div class="shimmer-overlay h-full w-full"></div>
</div>
</div>
<div class="space-y-3 p-5">
<div class="h-4 w-3/4 animate-pulse rounded-full bg-neutral-200 dark:bg-neutral-800">
<div class="shimmer-overlay h-full w-full"></div>
</div>
<div class="h-3 w-1/2 animate-pulse rounded-full bg-neutral-200 dark:bg-neutral-800">
<div class="shimmer-overlay h-full w-full"></div>
</div>
</div>
</div>
@endfor
</div>
<div wire:loading.remove>
@if ($playlists->isEmpty())
<div
class="rounded-3xl border border-neutral-200/70 bg-white/40 px-6 py-16 text-center shadow-xl backdrop-blur-lg dark:border-neutral-800 dark:bg-neutral-950/40">
<div class="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-rose-600/10 text-rose-600">
<i class="fa-solid fa-rectangle-list text-2xl"></i>
</div>
<p class="mt-4 text-lg font-semibold text-neutral-900 dark:text-white">
{{ __('playlist.no-playlist-found') }}
</p>
@if ($search !== '')
<button type="button" wire:click="$set('search', '')"
class="mt-4 inline-flex items-center gap-2 rounded-full bg-rose-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-rose-700">
<i class="fa-solid fa-xmark"></i>
{{ __('playlist.clear-search') }}
</button>
@endif
</div>
@else
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
@foreach ($playlists as $playlist)
@php
$covers = $playlist->episodes
->take(4)
->map(fn ($pe) => $pe->episode?->gallery->first()?->thumbnail_url)
->filter()
->values();
@endphp
<div wire:key="playlist-{{ $playlist->id }}"
class="group flex flex-col overflow-hidden rounded-2xl border border-neutral-200/70 bg-white/60 shadow-md backdrop-blur-lg transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl hover:shadow-black/30 dark:border-neutral-800 dark:bg-neutral-950/50 dark:hover:border-neutral-700">
<a href="{{ route('playlist.show', $playlist->id) }}"
class="relative block aspect-video overflow-hidden bg-gradient-to-br from-neutral-800 via-neutral-900 to-neutral-950">
@if ($covers->isEmpty())
<div class="flex h-full w-full items-center justify-center">
<i class="fa-solid fa-rectangle-list text-4xl text-neutral-500"></i>
</div>
@elseif ($covers->count() === 1)
<img src="{{ $covers[0] }}" alt="{{ $playlist->name }}" loading="lazy"
class="h-full w-full object-cover">
@elseif ($covers->count() === 2)
<div class="grid h-full w-full grid-cols-2">
@foreach ($covers as $cover)
<img src="{{ $cover }}" alt="" loading="lazy"
class="h-full w-full object-cover">
@endforeach
</div>
@elseif ($covers->count() === 3)
<div class="grid h-full w-full grid-cols-2 grid-rows-2">
<img src="{{ $covers[0] }}" alt="" loading="lazy"
class="col-span-2 h-full w-full object-cover">
<img src="{{ $covers[1] }}" alt="" loading="lazy"
class="h-full w-full object-cover">
<img src="{{ $covers[2] }}" alt="" loading="lazy"
class="h-full w-full object-cover">
</div>
@else
<div class="grid h-full w-full grid-cols-2 grid-rows-2">
@foreach ($covers->take(4) as $cover)
<img src="{{ $cover }}" alt="" loading="lazy"
class="h-full w-full object-cover">
@endforeach
</div>
@endif
<span
class="absolute left-3 top-3 inline-flex items-center gap-1.5 rounded-full bg-black/60 px-2.5 py-1 text-xs font-semibold text-white backdrop-blur">
<i class="fa-solid fa-clapperboard"></i>
{{ $playlist->episodes_count }}
</span>
<div
class="absolute inset-0 flex items-center justify-center bg-black/60 opacity-0 transition duration-300 group-hover:opacity-100">
<span
class="flex h-14 w-14 items-center justify-center rounded-full bg-rose-600 shadow-lg transition-transform duration-300 group-hover:scale-110">
<i class="fa-solid fa-play pl-1 text-lg text-white"></i>
</span>
</div>
</a>
<div class="flex flex-1 flex-col p-5">
<h3 class="text-base font-semibold text-neutral-900 line-clamp-2 dark:text-neutral-50">
<a href="{{ route('playlist.show', $playlist->id) }}">{{ $playlist->name }}</a>
</h3>
<div class="mt-2 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
<img src="{{ $playlist->user?->getAvatar() ?? asset('images/default-avatar.webp') }}"
alt="" class="h-5 w-5 rounded-full ring-1 ring-neutral-200 dark:ring-neutral-700">
<span class="truncate">{{ $playlist->user?->name }}</span>
<span>·</span>
<span class="shrink-0">{{ $playlist->created_at->diffForHumans() }}</span>
</div>
<div class="mt-4 flex items-center justify-between">
<span class="text-sm text-neutral-500 dark:text-neutral-400">
{{ $playlist->episodes_count }} {{ __('home.episodes') }}
</span>
<a href="{{ route('hentai.index', ['title' => $playlist->episodes->first()->episode->slug, 'playlist' => $playlist->id]) }}"
class="inline-flex items-center gap-2 rounded-full bg-gradient-to-r from-rose-600 to-rose-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:from-rose-700 hover:to-rose-800">
<i class="fa-solid fa-play"></i>
{{ __('playlist.play') }}
</a>
</div>
</div>
</div>
@endforeach
</div>
@endif
</div>
</div>
@if ($playlists->hasPages())
<div class="mt-10 mb-10">
{{ $playlists->links('pagination::tailwind') }}
</div>
@endif
</div>