Update playlist design

This commit is contained in:
2026-08-04 15:29:39 +02:00
parent 7553b9f895
commit 7f9573fe0f
13 changed files with 632 additions and 132 deletions
@@ -0,0 +1,73 @@
@props([
'playlistEpisode',
'isOwner' => false,
])
@php
$episode = $playlistEpisode->episode;
@endphp
<div wire:key="playlist-episode-{{ $playlistEpisode->id }}"
class="group flex items-center gap-3 sm:gap-4 rounded-xl border border-neutral-200/70 bg-white p-2.5 sm:p-3 shadow-sm transition hover:border-rose-400/40 hover:bg-rose-50 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-900">
<div
class="h-8 w-8 shrink-0 rounded-full bg-neutral-100 text-sm font-bold text-neutral-500 flex items-center justify-center dark:bg-neutral-800 dark:text-neutral-400">
{{ $playlistEpisode->position }}
</div>
<a href="{{ route('hentai.index', ['title' => $episode->slug]) }}" class="shrink-0">
<img src="{{ $episode->gallery->first()?->thumbnail_url }}"
alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="160"
class="aspect-video w-24 sm:w-36 rounded-lg object-cover transition-transform duration-300 group-hover:scale-105">
</a>
<div class="min-w-0 flex-1">
<a href="{{ route('hentai.index', ['title' => $episode->slug]) }}"
class="block truncate font-bold text-neutral-900 transition hover:text-rose-500 dark:text-white dark:hover:text-rose-400">
{{ $episode->title }} - {{ $episode->episode }}
</a>
@if ($episode->title_jpn)
<p class="truncate text-sm text-neutral-500 dark:text-neutral-400">{{ $episode->title_jpn }}</p>
@endif
<div class="mt-1.5 flex flex-wrap items-center gap-1.5 text-[11px] font-semibold">
<span class="rounded-full bg-black/70 px-2 py-0.5 text-white ring-1 ring-white/10 dark:bg-neutral-700">
{{ $episode->getResolution() }}
</span>
<span class="flex items-center gap-1 text-neutral-500 dark:text-neutral-400">
<i class="fa-regular fa-eye"></i> {{ $episode->viewCountFormatted() }}
</span>
<span class="flex items-center gap-1 text-neutral-500 dark:text-neutral-400">
<i class="fa-regular fa-heart"></i> {{ $episode->likeCount() }}
</span>
<span class="flex items-center gap-1 text-neutral-500 dark:text-neutral-400">
<i class="fa-regular fa-comment"></i> {{ $episode->commentCount() }}
</span>
@auth
@if ($episode->userWatched(auth()->id()))
<span
class="flex items-center gap-1 rounded-full bg-emerald-800/40 px-2 py-0.5 text-emerald-300 ring-1 ring-emerald-500/30">
<i class="fa-solid fa-eye"></i> {{ __('playlist.watched') }}
</span>
@endif
@endauth
</div>
</div>
@if ($isOwner)
<div class="flex shrink-0 items-center gap-1">
<button type="button" wire:click="moveUp({{ $playlistEpisode->id }})"
class="h-8 w-8 rounded-lg text-neutral-500 transition hover:bg-neutral-100 hover:text-rose-500 dark:text-neutral-400 dark:hover:bg-neutral-800">
<i class="fa-solid fa-arrow-up"></i>
</button>
<button type="button" wire:click="moveDown({{ $playlistEpisode->id }})"
class="h-8 w-8 rounded-lg text-neutral-500 transition hover:bg-neutral-100 hover:text-rose-500 dark:text-neutral-400 dark:hover:bg-neutral-800">
<i class="fa-solid fa-arrow-down"></i>
</button>
<button type="button" wire:click="remove({{ $playlistEpisode->id }})"
class="h-8 w-8 rounded-lg text-red-500 transition hover:bg-red-50 dark:hover:bg-red-950/40">
<i class="fa-solid fa-trash-can"></i>
</button>
</div>
@endif
</div>
@@ -1,57 +1,58 @@
<div>
<div wire:keydown.right.window="nextPage" wire:keydown.left.window="previousPage"
class="text-gray-900 dark:text-white">
<div
class="relative pt-5 mx-auto sm:px-6 lg:px-8 space-y-6 text-gray-900 dark:text-white xl:max-w-[95%] 2xl:max-w-[90%]">
<!-- Header -->
<div class="flex text-sm font-light bg-neutral-950/50 backdrop-blur-lg rounded-lg p-10 gap-2">
<div>
<img class="relative w-24 h-24 flex-none rounded-full shadow-lg"
src="{{ $playlist->user->getAvatar() }}">
</div>
<div class="flex flex-col justify-center flex-1 pl-4">
<div
class="flex flex-col items-center gap-6 rounded-2xl border border-neutral-200/70 bg-white p-6 shadow-sm sm:flex-row sm:items-center sm:gap-8 sm:p-8 dark:border-neutral-800 dark:bg-neutral-950">
<img class="h-24 w-24 shrink-0 rounded-full shadow-lg ring-2 ring-rose-500/20"
src="{{ $playlist->user->getAvatar() }}">
<div class="flex min-w-0 flex-1 flex-col text-center sm:text-left">
@if ($editingName)
<div class="flex items-center gap-2 mb-1">
<div class="flex flex-wrap items-center justify-center gap-2 mb-1 sm:justify-start">
<input
type="text"
wire:model="editingPlaylistName"
maxlength="30"
class="rounded-lg border border-neutral-400 bg-neutral-800 px-3 py-1.5 text-xl font-bold text-white focus:border-rose-500 focus:outline-none focus:ring-1 focus:ring-rose-500"
class="rounded-lg border border-neutral-300 bg-white px-3 py-1.5 text-xl font-bold text-neutral-900 focus:border-rose-500 focus:outline-none focus:ring-1 focus:ring-rose-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white"
/>
<button wire:click="updateName" class="rounded-lg bg-rose-600 px-3 py-1.5 text-sm font-semibold text-white transition hover:bg-rose-700">
Save
</button>
<button wire:click="cancelEditName" class="rounded-lg border border-neutral-500 px-3 py-1.5 text-sm text-neutral-300 transition hover:bg-neutral-700">
<button wire:click="cancelEditName" class="rounded-lg border border-neutral-300 px-3 py-1.5 text-sm text-neutral-600 transition hover:bg-neutral-100 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800">
Cancel
</button>
</div>
@error('editingPlaylistName')
<p class="text-rose-400 text-sm mb-1">{{ $message }}</p>
<p class="text-rose-500 text-sm mb-1">{{ $message }}</p>
@enderror
@else
<h1 class="font-bold text-3xl">
{{ $playlist->name }}
<h1 class="flex items-center justify-center gap-1 text-3xl font-bold text-neutral-900 sm:justify-start dark:text-white">
<span class="truncate">{{ $playlist->name }}</span>
@auth
@if (Auth::id() === $playlist->user->id)
<button wire:click="editName" class="ml-2 text-xl text-neutral-400 transition hover:text-white" title="Edit playlist name">
<button wire:click="editName" class="ml-1 text-xl text-neutral-400 transition hover:text-rose-500" title="Edit playlist name">
<i class="fa-solid fa-pen-to-square"></i>
</button>
@endif
@endauth
</h1>
@endif
<p class="font-light text-lg text-neutral-200">Episodes: {{ count($playlistEpisodes) }}</p>
<p class="font-light text-lg text-neutral-200">
<p class="mt-1 text-lg font-light text-neutral-500 dark:text-neutral-300">{{ __('playlist.episodes') }}: {{ $playlist->episodes_count }}</p>
<p class="mt-0.5 text-lg font-light text-neutral-500 dark:text-neutral-300">
Creator: {{ $playlist->user->name }}
@auth
@if (Auth::id() === $playlist->user->id)
<span class="ml-3">
<button wire:click="toggleVisibility" class="cursor-pointer rounded-full px-3 py-0.5 text-xs font-semibold transition {{ $playlist->is_private ? 'bg-neutral-600 text-neutral-300 hover:bg-neutral-500' : 'bg-green-600 text-white hover:bg-green-500' }}">
<button wire:click="toggleVisibility" class="cursor-pointer rounded-full px-3 py-0.5 text-xs font-semibold transition {{ $playlist->is_private ? 'bg-neutral-200 text-neutral-700 hover:bg-neutral-300 dark:bg-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-600' : 'bg-green-600 text-white hover:bg-green-500' }}">
<i class="fa-solid {{ $playlist->is_private ? 'fa-lock' : 'fa-earth-americas' }} mr-1"></i>
{{ $playlist->is_private ? 'Private' : 'Public' }}
</button>
</span>
@else
<span class="ml-3 rounded-full px-3 py-0.5 text-xs font-semibold {{ $playlist->is_private ? 'bg-neutral-600 text-neutral-300' : 'bg-green-600 text-white' }}">
<span class="ml-3 rounded-full px-3 py-0.5 text-xs font-semibold {{ $playlist->is_private ? 'bg-neutral-200 text-neutral-700 dark:bg-neutral-700 dark:text-neutral-200' : 'bg-green-600 text-white' }}">
<i class="fa-solid {{ $playlist->is_private ? 'fa-lock' : 'fa-earth-americas' }} mr-1"></i>
{{ $playlist->is_private ? 'Private' : 'Public' }}
</span>
@@ -59,88 +60,86 @@
@endauth
</p>
</div>
<div class="flex flex-col justify-center pl-4">
<div class="flex justify-end">
@php $episode = $playlistEpisodes->first()?->episode; @endphp
@if(isset($episode))
<a href="{{ route('hentai.index', ['title' => $episode->slug, 'playlist' => $playlist->id]) }}"
class="cursor-pointer float-right text-white bg-rose-700 hover:bg-rose-800 focus:ring-4 focus:outline-none focus:ring-rose-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-rose-600 dark:hover:bg-rose-700 dark:focus:ring-rose-800">{{ __('playlist.play') }}</a>
@else
<a class="cursor-not-allowed float-right text-white bg-neutral-700 focus:ring-4 focus:outline-none focus:ring-neutral-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-neutral-600 dark:focus:ring-neutral-800">{{ __('playlist.play') }}</a>
@endif
</div>
<div class="shrink-0">
@if ($firstEpisode)
<a href="{{ route('hentai.index', ['title' => $firstEpisode->slug, 'playlist' => $playlist->id]) }}"
class="inline-flex items-center gap-2 text-white bg-rose-700 hover:bg-rose-800 focus:ring-4 focus:outline-none focus:ring-rose-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-rose-600 dark:hover:bg-rose-700 dark:focus:ring-rose-800">
<i class="fa-solid fa-play text-xs"></i> {{ __('playlist.play') }}
</a>
@else
<a class="inline-flex cursor-not-allowed items-center gap-2 text-neutral-500 bg-neutral-200 font-medium rounded-lg text-sm px-4 py-2 dark:text-neutral-300 dark:bg-neutral-700">
<i class="fa-solid fa-play text-xs"></i> {{ __('playlist.play') }}
</a>
@endif
</div>
</div>
@forelse($playlistEpisodes as $playlistEpisode)
@php $episode = $playlistEpisode->episode; @endphp
<div wire:key="playlist-episode-{{ $playlistEpisode->id }}"
class="flex justify-between items-center rounded-lg hover:bg-black border border-neutral-950 bg-neutral-950/50 backdrop-blur-lg transition !mt-1 gap-2">
<div class="flex pl-5 pr-5 w-10">
{{ $playlistEpisode->position }}
<!-- Toolbar -->
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
<div class="relative flex-1">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-4">
<svg class="h-5 w-5 text-neutral-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
</svg>
</div>
<div class="flex-[2] hidden md:block">
<div class="relative p-1 w-full md:p-2">
<a href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000"
class="block object-cover object-center relative z-20 rounded-lg aspect-video"
src="{{ $episode->gallery->first()->thumbnail_url }}">
@guest
<p
class="absolute left-2 bottom-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30 collapse md:visible">
<i class="fa-regular fa-eye"></i> {{ $episode->viewCountFormatted() }} <i
class="fa-regular fa-heart"></i>
{{ $episode->likeCount() }} <i class="fa-regular fa-comment"></i>
{{ $episode->commentCount() }}
</p>
@endguest
<input
wire:model.live.debounce.500ms="search"
type="search"
placeholder="{{ __('playlist.search-episodes') }}"
class="w-full rounded-xl border border-neutral-300 bg-white py-2.5 pl-12 pr-12 text-sm text-neutral-900 shadow-sm transition focus:border-rose-500 focus:outline-none focus:ring-4 focus:ring-rose-500/20 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white dark:placeholder-neutral-500"
/>
@auth
@if ($episode->userWatched(auth()->user()->id))
<p
class="absolute left-2 bottom-2 bg-green-600/80 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
<i class="fa-regular fa-eye"></i> {{ $episode->viewCountFormatted() }} <i
class="fa-regular fa-heart"></i> {{ $episode->likeCount() }} <i
class="fa-regular fa-comment"></i>
{{ $episode->commentCount() }}
</p>
@else
<p
class="absolute left-2 bottom-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
<i class="fa-regular fa-eye"></i> {{ $episode->viewCountFormatted() }} <i
class="fa-regular fa-heart"></i> {{ $episode->likeCount() }} <i
class="fa-regular fa-comment"></i>
{{ $episode->commentCount() }}
</p>
@endif
@endauth
</a>
<div wire:loading.class="opacity-100" class="opacity-0">
<div class="absolute inset-y-0 right-3 flex items-center">
<svg class="h-5 w-5 animate-spin text-rose-500" viewBox="0 0 24 24" fill="none">
<circle class="opacity-20" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-90" fill="currentColor"
d="M22 12a10 10 0 0 1-10 10V18a6 6 0 0 0 6-6h4Z">
</path>
</svg>
</div>
</div>
<div class="flex-[5]">
<a href="{{ route('hentai.index', ['title' => $episode->slug]) }}"
class="font-bold">{{ $episode->title }} - {{ $episode->episode }}</a>
<br>
<a href="{{ route('hentai.index', ['title' => $episode->slug]) }}">{{ $episode->title_jpn }}</a>
</div>
<div class="pr-5">
@auth
@if (Auth::user()->id === $playlist->user->id)
<button class="pr-2" wire:click="moveUp({{ $playlistEpisode->id }})"><i
class="fa-solid fa-arrow-up"></i></button>
<button class="pr-2" wire:click="moveDown({{ $playlistEpisode->id }})"><i
class="fa-solid fa-arrow-down"></i></button>
<button wire:click="remove({{ $playlistEpisode->id }})" class="text-red-500"><i
class="fa-solid fa-trash"></i></button>
@endif
@endauth
</div>
</div>
@empty
<div class="pt-6 text-2xl text-center">
No results (╥﹏╥)
<div class="flex shrink-0 items-center gap-2">
<label for="playlistPerPage" class="text-sm text-neutral-500 dark:text-neutral-400">
{{ __('playlist.per-page') }}
</label>
<select
wire:model.live="perPage"
id="playlistPerPage"
class="rounded-xl border border-neutral-300 bg-white py-2.5 pl-3 pr-8 text-sm text-neutral-900 shadow-sm transition focus:border-rose-500 focus:outline-none focus:ring-4 focus:ring-rose-500/20 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white"
>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
@endforelse
</div>
<!-- List -->
<div class="space-y-2">
@forelse($episodes as $playlistEpisode)
@include('livewire.partials.playlist-episode-row', ['playlistEpisode' => $playlistEpisode, 'isOwner' => $isOwner])
@empty
<div
class="rounded-2xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 p-12 text-center">
<div class="inline-flex h-20 w-20 items-center justify-center rounded-full bg-gray-100 dark:bg-neutral-800 mb-4">
<i class="fa-solid fa-clapperboard text-3xl text-gray-400 dark:text-gray-500"></i>
</div>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300">
{{ $search !== '' ? __('playlist.no-matches') : __('playlist.empty-playlist') }}
</h3>
</div>
@endforelse
</div>
<!-- Pagination -->
@if ($episodes->hasPages())
<div class="mt-6">{{ $episodes->links('pagination::tailwind') }}</div>
@endif
</div>
</div>