Fix: lightbox, profile stats, toast, rating UX
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -119,7 +119,9 @@
|
|||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
@auth
|
@auth
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="space-y-3">
|
||||||
|
{{-- Step 1: pick a status --}}
|
||||||
|
<div x-show="!pendingStatus">
|
||||||
<div class="relative" x-data="{ open: false }">
|
<div class="relative" x-data="{ open: false }">
|
||||||
<button @click="open = !open"
|
<button @click="open = !open"
|
||||||
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">
|
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">
|
||||||
@@ -131,7 +133,7 @@
|
|||||||
<div x-show="open" @click.outside="open = false" x-transition
|
<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">
|
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)
|
@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"
|
<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">
|
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 }}
|
{{ $label }}
|
||||||
</button>
|
</button>
|
||||||
@@ -139,6 +141,28 @@
|
|||||||
</div>
|
</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>
|
||||||
@else
|
@else
|
||||||
<a href="{{ route('login') }}"
|
<a href="{{ route('login') }}"
|
||||||
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">
|
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">
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
@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" />
|
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user