Fix: lightbox, profile stats, toast, rating UX
This commit is contained in:
@@ -119,25 +119,49 @@
|
||||
</div>
|
||||
@else
|
||||
@auth
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
<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">
|
||||
+ Add to Library
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<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="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 }}
|
||||
<div class="space-y-3">
|
||||
{{-- Step 1: pick a status --}}
|
||||
<div x-show="!pendingStatus">
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
<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">
|
||||
+ Add to Library
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</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>
|
||||
|
||||
{{-- 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
|
||||
<a href="{{ route('login') }}"
|
||||
@@ -146,6 +170,13 @@
|
||||
</a>
|
||||
@endauth
|
||||
@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>
|
||||
|
||||
{{-- Companies --}}
|
||||
@@ -177,15 +208,66 @@
|
||||
|
||||
{{-- Screenshots --}}
|
||||
@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>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
@foreach($game->screenshots as $shot)
|
||||
<a href="{{ $shot->url }}" target="_blank" class="block rounded-lg overflow-hidden bg-[#16161d] hover:ring-1 hover:ring-purple-500 transition-all">
|
||||
@foreach($game->screenshots as $i => $shot)
|
||||
<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">
|
||||
</a>
|
||||
</button>
|
||||
@endforeach
|
||||
</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>
|
||||
@endif
|
||||
|
||||
@@ -233,6 +315,9 @@ function libraryWidget(gameId, initialStatus, initialRating) {
|
||||
gameId,
|
||||
status: initialStatus,
|
||||
rating: initialRating,
|
||||
toast: null,
|
||||
pendingStatus: null,
|
||||
pendingRating: null,
|
||||
|
||||
statusLabel(s) {
|
||||
const labels = {
|
||||
@@ -245,7 +330,12 @@ function libraryWidget(gameId, initialStatus, initialRating) {
|
||||
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', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -253,11 +343,11 @@ function libraryWidget(gameId, initialStatus, initialRating) {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'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) {
|
||||
this.status = status;
|
||||
window.location.reload();
|
||||
this.showToast('Added to library!');
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user