305 lines
15 KiB
PHP
305 lines
15 KiB
PHP
@extends('layouts.app')
|
|
@php use Illuminate\Support\Str; @endphp
|
|
|
|
@section('title', $game->title . ' — GameLog')
|
|
@section('description', $game->summary ? Str::limit($game->summary, 155) : 'Track ' . $game->title . ' on GameLog.')
|
|
|
|
@section('content')
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
|
|
|
|
{{-- Main Info --}}
|
|
<div class="flex flex-col md:flex-row gap-8">
|
|
|
|
{{-- Cover --}}
|
|
<div class="flex-shrink-0">
|
|
<div class="w-48 md:w-56 rounded-xl overflow-hidden bg-[#16161d] shadow-2xl shadow-black/50 mx-auto md:mx-0">
|
|
@if($game->cover_url)
|
|
<img src="{{ $game->cover_url }}" alt="{{ $game->title }}" class="w-full">
|
|
@else
|
|
<div class="aspect-[3/4] flex items-center justify-center">
|
|
<svg class="w-16 h-16 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>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Details --}}
|
|
<div class="flex-1 min-w-0">
|
|
|
|
{{-- Title & Year --}}
|
|
<div class="mb-2">
|
|
@if($game->release_year)
|
|
<span class="text-sm text-slate-500 font-medium">{{ $game->release_year }}</span>
|
|
@endif
|
|
<h1 class="text-3xl sm:text-4xl font-bold text-white leading-tight mt-1">{{ $game->title }}</h1>
|
|
</div>
|
|
|
|
{{-- Rating --}}
|
|
<div class="flex items-center gap-4 mb-6">
|
|
@if($game->rating_avg)
|
|
<div class="flex items-center gap-1.5 bg-[#16161d] px-3 py-1.5 rounded-lg border border-white/10">
|
|
<svg class="w-4 h-4 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
|
</svg>
|
|
<span class="text-white font-bold">{{ number_format($game->rating_avg, 1) }}</span>
|
|
<span class="text-slate-500 text-sm">/ 10</span>
|
|
</div>
|
|
<span class="text-sm text-slate-500">{{ number_format($game->rating_count) }} {{ Str::plural('rating', $game->rating_count) }}</span>
|
|
@else
|
|
<span class="text-sm text-slate-500">No ratings yet</span>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Genres & Platforms --}}
|
|
@if($game->genres->isNotEmpty())
|
|
<div class="flex flex-wrap gap-2 mb-3">
|
|
@foreach($game->genres as $genre)
|
|
<a href="{{ route('games.index', ['genre' => $genre->id]) }}"
|
|
class="px-2.5 py-1 text-xs bg-purple-900/30 text-purple-300 border border-purple-700/30 rounded-full hover:bg-purple-900/50 transition-colors">
|
|
{{ $genre->name }}
|
|
</a>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
@if($game->platforms->isNotEmpty())
|
|
<div class="flex flex-wrap gap-2 mb-5">
|
|
@foreach($game->platforms as $platform)
|
|
<a href="{{ route('games.index', ['platform' => $platform->id]) }}"
|
|
class="px-2.5 py-1 text-xs bg-[#16161d] text-slate-400 border border-white/10 rounded-full hover:text-slate-200 transition-colors">
|
|
{{ $platform->name }}
|
|
</a>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Add to Library --}}
|
|
<div class="mb-6" x-data="libraryWidget({{ $game->id }}, '{{ $userGame?->status }}', {{ $userGame?->rating ?? 'null' }})">
|
|
|
|
@if($userGame)
|
|
{{-- Already in library --}}
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<div class="relative" x-data="{ open: false }">
|
|
<button @click="open = !open"
|
|
class="flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium rounded-lg transition-colors">
|
|
<span x-text="statusLabel(status)"></span>
|
|
<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="updateStatus('{{ $s }}', {{ $userGame->id }}); open = false"
|
|
class="w-full text-left px-4 py-2.5 text-sm hover:bg-white/5 transition-colors"
|
|
:class="status === '{{ $s }}' ? 'text-purple-400' : 'text-slate-300'">
|
|
{{ $label }}
|
|
</button>
|
|
@endforeach
|
|
<div class="border-t border-white/5"></div>
|
|
<button @click="removeFromLibrary({{ $userGame->id }})"
|
|
class="w-full text-left px-4 py-2.5 text-sm text-red-400 hover:bg-white/5 transition-colors">
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Rating --}}
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm text-slate-400">Your rating:</span>
|
|
<input type="number" min="1" max="10" x-model="rating"
|
|
@change="updateRating({{ $userGame->id }})"
|
|
class="w-16 bg-[#16161d] border border-white/10 rounded-lg px-2 py-1.5 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>
|
|
</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 }}
|
|
</button>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<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">
|
|
Log in to Add to Library
|
|
</a>
|
|
@endauth
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Companies --}}
|
|
@if($game->companies->isNotEmpty())
|
|
<div class="text-sm text-slate-500 space-y-1">
|
|
@php
|
|
$developers = $game->companies->where('pivot.role', 'developer');
|
|
$publishers = $game->companies->where('pivot.role', 'publisher');
|
|
@endphp
|
|
@if($developers->isNotEmpty())
|
|
<div><span class="text-slate-400">Developer:</span> {{ $developers->pluck('name')->join(', ') }}</div>
|
|
@endif
|
|
@if($publishers->isNotEmpty())
|
|
<div><span class="text-slate-400">Publisher:</span> {{ $publishers->pluck('name')->join(', ') }}</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Summary --}}
|
|
@if($game->summary)
|
|
<div class="mt-10 pt-8 border-t border-white/5">
|
|
<h2 class="text-lg font-semibold text-white mb-3">Summary</h2>
|
|
<p class="text-slate-400 leading-relaxed max-w-3xl">{{ $game->summary }}</p>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Screenshots --}}
|
|
@if($game->screenshots->isNotEmpty())
|
|
<div class="mt-10 pt-8 border-t border-white/5">
|
|
<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">
|
|
<img src="{{ $shot->thumb_url ?? $shot->url }}" alt="Screenshot" class="w-full aspect-video object-cover">
|
|
</a>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Reviews --}}
|
|
<div class="mt-10 pt-8 border-t border-white/5">
|
|
<h2 class="text-lg font-semibold text-white mb-4">Reviews</h2>
|
|
@if($game->reviews->isEmpty())
|
|
<p class="text-slate-500">No reviews yet. Be the first!</p>
|
|
@else
|
|
<div class="space-y-4 max-w-3xl">
|
|
@foreach($game->reviews->take(5) as $review)
|
|
<div class="bg-[#16161d] rounded-lg p-5 border border-white/5">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-7 h-7 rounded-full bg-purple-700 flex items-center justify-center text-xs font-bold text-white">
|
|
{{ strtoupper(substr($review->user->name, 0, 1)) }}
|
|
</div>
|
|
<span class="text-sm font-medium text-slate-300">{{ $review->user->username ?? $review->user->name }}</span>
|
|
</div>
|
|
@if($review->rating)
|
|
<div class="flex items-center gap-1">
|
|
<svg class="w-3.5 h-3.5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
|
</svg>
|
|
<span class="text-sm font-bold text-white">{{ $review->rating }}</span>
|
|
<span class="text-slate-500 text-xs">/10</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@if($review->body)
|
|
<p class="text-slate-400 text-sm leading-relaxed">{{ $review->body }}</p>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
</div>
|
|
|
|
@push('scripts')
|
|
<script>
|
|
function libraryWidget(gameId, initialStatus, initialRating) {
|
|
return {
|
|
gameId,
|
|
status: initialStatus,
|
|
rating: initialRating,
|
|
|
|
statusLabel(s) {
|
|
const labels = {
|
|
playing: 'Playing',
|
|
played: 'Played',
|
|
want_to_play: 'Want to Play',
|
|
on_hold: 'On Hold',
|
|
dropped: 'Dropped',
|
|
};
|
|
return labels[s] || 'In Library';
|
|
},
|
|
|
|
async addToLibrary(status) {
|
|
const res = await fetch('/library', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify({ game_id: this.gameId, status }),
|
|
});
|
|
if (res.ok) {
|
|
this.status = status;
|
|
window.location.reload();
|
|
}
|
|
},
|
|
|
|
async updateStatus(status, userGameId) {
|
|
const res = await fetch(`/library/${userGameId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify({ status }),
|
|
});
|
|
if (res.ok) this.status = status;
|
|
},
|
|
|
|
async updateRating(userGameId) {
|
|
await fetch(`/library/${userGameId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify({ rating: this.rating }),
|
|
});
|
|
},
|
|
|
|
async removeFromLibrary(userGameId) {
|
|
const res = await fetch(`/library/${userGameId}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
'Accept': 'application/json',
|
|
},
|
|
});
|
|
if (res.ok) window.location.reload();
|
|
},
|
|
};
|
|
}
|
|
</script>
|
|
@endpush
|
|
|
|
@endsection
|