Fix: lightbox, profile stats, toast, rating UX

This commit is contained in:
Mark
2026-05-30 01:57:53 +03:00
parent 58e587f955
commit 93d869cf8c
4 changed files with 192 additions and 36 deletions
+16 -6
View File
@@ -16,14 +16,24 @@ class UserController extends Controller
'total' => $user->library()->count(), 'total' => $user->library()->count(),
]; ];
$recentlyPlayed = $user->library() $genreStats = \DB::table('genres')
->with('game') ->select('genres.id', 'genres.name', \DB::raw('COUNT(*) as count'))
->where('status', 'played') ->join('game_genre', 'genres.id', '=', 'game_genre.genre_id')
->latest('updated_at') ->join('user_games', 'game_genre.game_id', '=', 'user_games.game_id')
->limit(6) ->where('user_games.user_id', $user->id)
->whereIn('user_games.status', ['played', 'playing'])
->groupBy('genres.id', 'genres.name')
->orderByDesc('count')
->limit(5)
->get(); ->get();
return view('users.show', compact('user', 'stats', 'recentlyPlayed')); $allGames = $user->library()
->with('game')
->latest('updated_at')
->limit(12)
->get();
return view('users.show', compact('user', 'stats', 'genreStats', 'allGames'));
} }
public function library(User $user) public function library(User $user)
+114 -24
View File
@@ -119,25 +119,49 @@
</div> </div>
@else @else
@auth @auth
<div class="flex flex-wrap gap-3"> <div class="space-y-3">
<div class="relative" x-data="{ open: false }"> {{-- Step 1: pick a status --}}
<button @click="open = !open" <div x-show="!pendingStatus">
class="flex items-center gap-2 px-5 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-semibold rounded-lg transition-colors"> <div class="relative" x-data="{ open: false }">
+ Add to Library <button @click="open = !open"
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> class="flex items-center gap-2 px-5 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-semibold rounded-lg transition-colors">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/> + Add to Library
</svg> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</button> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
<div x-show="open" @click.outside="open = false" x-transition </svg>
class="absolute left-0 mt-2 w-44 bg-[#16161d] border border-white/10 rounded-lg shadow-xl overflow-hidden z-10">
@foreach(['playing' => 'Playing', 'played' => 'Played', 'want_to_play' => 'Want to Play', 'on_hold' => 'On Hold', 'dropped' => 'Dropped'] as $s => $label)
<button @click="addToLibrary('{{ $s }}'); open = false"
class="w-full text-left px-4 py-2.5 text-sm text-slate-300 hover:bg-white/5 hover:text-white transition-colors">
{{ $label }}
</button> </button>
@endforeach <div x-show="open" @click.outside="open = false" x-transition
class="absolute left-0 mt-2 w-44 bg-[#16161d] border border-white/10 rounded-lg shadow-xl overflow-hidden z-10">
@foreach(['playing' => 'Playing', 'played' => 'Played', 'want_to_play' => 'Want to Play', 'on_hold' => 'On Hold', 'dropped' => 'Dropped'] as $s => $label)
<button @click="pendingStatus = '{{ $s }}'; open = false"
class="w-full text-left px-4 py-2.5 text-sm text-slate-300 hover:bg-white/5 hover:text-white transition-colors">
{{ $label }}
</button>
@endforeach
</div>
</div> </div>
</div> </div>
{{-- Step 2: optional rating + save --}}
<div x-show="pendingStatus" x-transition
class="flex flex-wrap items-center gap-3 bg-[#16161d] border border-white/10 rounded-lg px-4 py-3">
<span class="text-sm text-slate-300">Adding as <strong x-text="statusLabel(pendingStatus)" class="text-purple-400 font-semibold"></strong></span>
<div class="flex items-center gap-2">
<label class="text-sm text-slate-400">Rate it (optional):</label>
<input type="number" min="1" max="10" x-model="pendingRating"
class="w-16 bg-[#0f0f13] border border-white/10 rounded px-2 py-1 text-sm text-center text-slate-200 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500"
placeholder="—">
<span class="text-slate-500 text-sm">/10</span>
</div>
<button @click="addToLibraryWithRating()"
class="px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white text-sm font-semibold rounded-lg transition-colors">
Save
</button>
<button @click="pendingStatus = null; pendingRating = null"
class="px-3 py-1.5 text-slate-400 hover:text-slate-200 text-sm transition-colors">
Cancel
</button>
</div>
</div> </div>
@else @else
<a href="{{ route('login') }}" <a href="{{ route('login') }}"
@@ -146,6 +170,13 @@
</a> </a>
@endauth @endauth
@endif @endif
{{-- Toast notification --}}
<div x-show="toast" x-transition
class="fixed bottom-4 right-4 z-50 bg-purple-600 text-white px-4 py-3 rounded-lg shadow-lg text-sm font-medium"
style="display:none;">
<span x-text="toast"></span>
</div>
</div> </div>
{{-- Companies --}} {{-- Companies --}}
@@ -177,15 +208,66 @@
{{-- Screenshots --}} {{-- Screenshots --}}
@if($game->screenshots->isNotEmpty()) @if($game->screenshots->isNotEmpty())
<div class="mt-10 pt-8 border-t border-white/5"> <div class="mt-10 pt-8 border-t border-white/5"
x-data="{
open: false,
current: 0,
screenshots: {{ $game->screenshots->map(fn($s) => ['url' => $s->url, 'thumb' => $s->thumb_url ?? $s->url])->toJson() }},
openAt(i) { this.current = i; this.open = true; },
prev() { this.current = (this.current - 1 + this.screenshots.length) % this.screenshots.length; },
next() { this.current = (this.current + 1) % this.screenshots.length; },
}"
@keydown.escape.window="open = false"
@keydown.arrow-left.window="if (open) prev()"
@keydown.arrow-right.window="if (open) next()">
<h2 class="text-lg font-semibold text-white mb-4">Screenshots</h2> <h2 class="text-lg font-semibold text-white mb-4">Screenshots</h2>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3"> <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
@foreach($game->screenshots as $shot) @foreach($game->screenshots as $i => $shot)
<a href="{{ $shot->url }}" target="_blank" class="block rounded-lg overflow-hidden bg-[#16161d] hover:ring-1 hover:ring-purple-500 transition-all"> <button @click="openAt({{ $i }})" class="block w-full rounded-lg overflow-hidden bg-[#16161d] hover:ring-1 hover:ring-purple-500 transition-all">
<img src="{{ $shot->thumb_url ?? $shot->url }}" alt="Screenshot" class="w-full aspect-video object-cover"> <img src="{{ $shot->thumb_url ?? $shot->url }}" alt="Screenshot" class="w-full aspect-video object-cover">
</a> </button>
@endforeach @endforeach
</div> </div>
{{-- Lightbox overlay --}}
<div x-show="open"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/90"
@click.self="open = false"
style="display:none;">
{{-- Close --}}
<button @click="open = false" class="absolute top-4 right-4 text-white/70 hover:text-white transition-colors">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
{{-- Counter --}}
<div class="absolute top-4 left-1/2 -translate-x-1/2 text-sm text-white/60">
<span x-text="current + 1"></span> / <span x-text="screenshots.length"></span>
</div>
{{-- Prev --}}
<button @click="prev()" class="absolute left-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white transition-colors p-2">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
</button>
{{-- Full-size image --}}
<img :src="screenshots[current]?.url" alt="Screenshot" class="max-h-[85vh] max-w-[90vw] rounded-lg shadow-2xl object-contain">
{{-- Next --}}
<button @click="next()" class="absolute right-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white transition-colors p-2">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</button>
</div>
</div> </div>
@endif @endif
@@ -233,6 +315,9 @@ function libraryWidget(gameId, initialStatus, initialRating) {
gameId, gameId,
status: initialStatus, status: initialStatus,
rating: initialRating, rating: initialRating,
toast: null,
pendingStatus: null,
pendingRating: null,
statusLabel(s) { statusLabel(s) {
const labels = { const labels = {
@@ -245,7 +330,12 @@ function libraryWidget(gameId, initialStatus, initialRating) {
return labels[s] || 'In Library'; return labels[s] || 'In Library';
}, },
async addToLibrary(status) { showToast(message) {
this.toast = message;
setTimeout(() => { this.toast = null; }, 2500);
},
async addToLibraryWithRating() {
const res = await fetch('/library', { const res = await fetch('/library', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -253,11 +343,11 @@ function libraryWidget(gameId, initialStatus, initialRating) {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json', 'Accept': 'application/json',
}, },
body: JSON.stringify({ game_id: this.gameId, status }), body: JSON.stringify({ game_id: this.gameId, status: this.pendingStatus, rating: this.pendingRating || null }),
}); });
if (res.ok) { if (res.ok) {
this.status = status; this.showToast('Added to library!');
window.location.reload(); setTimeout(() => window.location.reload(), 1000);
} }
}, },
+1
View File
@@ -77,6 +77,7 @@
@if(auth()->user()->username) @if(auth()->user()->username)
<a href="{{ route('users.show', auth()->user()->username) }}" class="block px-4 py-2.5 text-sm text-slate-300 hover:bg-white/5 hover:text-white transition-colors">Profile</a> <a href="{{ route('users.show', auth()->user()->username) }}" class="block px-4 py-2.5 text-sm text-slate-300 hover:bg-white/5 hover:text-white transition-colors">Profile</a>
<a href="{{ route('users.library', auth()->user()->username) }}" class="block px-4 py-2.5 text-sm text-slate-300 hover:bg-white/5 hover:text-white transition-colors">My Library</a> <a href="{{ route('users.library', auth()->user()->username) }}" class="block px-4 py-2.5 text-sm text-slate-300 hover:bg-white/5 hover:text-white transition-colors">My Library</a>
<a href="{{ route('users.show', auth()->user()->username) }}#lists" class="block px-4 py-2.5 text-sm text-slate-300 hover:bg-white/5 hover:text-white transition-colors">My Lists</a>
@endif @endif
<a href="{{ route('profile.edit') }}" class="block px-4 py-2.5 text-sm text-slate-300 hover:bg-white/5 hover:text-white transition-colors">Settings</a> <a href="{{ route('profile.edit') }}" class="block px-4 py-2.5 text-sm text-slate-300 hover:bg-white/5 hover:text-white transition-colors">Settings</a>
<div class="border-t border-white/5"></div> <div class="border-t border-white/5"></div>
+61 -6
View File
@@ -53,18 +53,73 @@
</div> </div>
</div> </div>
{{-- Recently Played --}} {{-- Top Genres --}}
@if($recentlyPlayed->isNotEmpty()) @if($genreStats->isNotEmpty())
<div class="mb-10">
<h2 class="text-lg font-semibold text-white mb-3">Top Genres</h2>
<div class="flex flex-wrap gap-2">
@foreach($genreStats as $genre)
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-[#16161d] border border-white/10 rounded-full text-sm text-slate-300">
{{ $genre->name }}
<span class="text-xs text-purple-400 font-semibold">{{ $genre->count }}</span>
</span>
@endforeach
</div>
</div>
@endif
{{-- Recent Library --}}
@if($allGames->isNotEmpty())
<div> <div>
<h2 class="text-lg font-semibold text-white mb-4">Recently Played</h2> <div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-white">Recent Library</h2>
<a href="{{ route('users.library', $user->username) }}" class="text-sm text-purple-400 hover:text-purple-300 transition-colors">
View all →
</a>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4"> <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
@foreach($recentlyPlayed as $entry) @foreach($allGames as $entry)
<x-game-card :game="$entry->game" /> @php
$statusColors = [
'playing' => 'bg-green-600',
'played' => 'bg-blue-600',
'want_to_play' => 'bg-yellow-600',
'on_hold' => 'bg-orange-600',
'dropped' => 'bg-red-700',
];
$statusLabels = [
'playing' => 'Playing',
'played' => 'Played',
'want_to_play' => 'Want to Play',
'on_hold' => 'On Hold',
'dropped' => 'Dropped',
];
@endphp
<div class="relative">
<x-game-card :game="$entry->game" />
<span class="absolute top-2 left-2 text-xs font-semibold px-1.5 py-0.5 rounded {{ $statusColors[$entry->status] ?? 'bg-slate-700' }} text-white leading-tight">
{{ $statusLabels[$entry->status] ?? $entry->status }}
</span>
</div>
@endforeach @endforeach
</div> </div>
</div> </div>
@else @else
<p class="text-slate-500">No games tracked yet.</p> {{-- Empty state --}}
<div class="text-center py-20">
<div class="w-16 h-16 rounded-full bg-[#16161d] flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"/>
</svg>
</div>
<h3 class="text-lg font-medium text-slate-300 mb-2">No games tracked yet</h3>
<p class="text-slate-500 mb-6">Start building your game library!</p>
<a href="{{ route('games.index') }}"
class="inline-flex items-center gap-2 px-5 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-semibold rounded-lg transition-colors">
Browse Games
</a>
</div>
@endif @endif
</div> </div>