2026-05-29 09:37:07 +03:00
|
|
|
@extends('layouts.app')
|
|
|
|
|
@php use Illuminate\Support\Str; @endphp
|
|
|
|
|
|
2026-05-31 14:19:47 +03:00
|
|
|
@section('title', $game->title . ' — LoreKeepers')
|
|
|
|
|
@section('description', $game->summary ? Str::limit($game->summary, 155) : 'Track ' . $game->title . ' on LoreKeepers.')
|
|
|
|
|
@section('og_title', $game->title . ' — LoreKeepers')
|
|
|
|
|
@section('og_description', $game->summary ? Str::limit($game->summary, 155) : 'Track ' . $game->title . ' on LoreKeepers.')
|
2026-05-30 02:50:34 +03:00
|
|
|
@section('og_image', $game->cover_url ?? asset('images/og-default.jpg'))
|
2026-05-31 03:02:12 +03:00
|
|
|
@section('og_type', 'website')
|
|
|
|
|
@section('canonical', route('games.show', $game))
|
2026-05-29 09:37:07 +03:00
|
|
|
|
|
|
|
|
@section('content')
|
2026-05-31 03:02:12 +03:00
|
|
|
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-6 pb-0" aria-label="Breadcrumb">
|
|
|
|
|
<ol class="flex items-center gap-1.5 text-sm text-slate-500">
|
|
|
|
|
<li><a href="{{ route('home') }}" class="hover:text-slate-300 transition-colors">Home</a></li>
|
|
|
|
|
<li class="text-slate-700">/</li>
|
|
|
|
|
<li><a href="{{ route('games.index') }}" class="hover:text-slate-300 transition-colors">Games</a></li>
|
|
|
|
|
<li class="text-slate-700">/</li>
|
|
|
|
|
<li class="text-slate-400 truncate max-w-xs">{{ $game->title }}</li>
|
|
|
|
|
</ol>
|
|
|
|
|
</nav>
|
2026-05-29 09:37:07 +03:00
|
|
|
<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 --}}
|
2026-05-30 02:50:34 +03:00
|
|
|
<div class="mb-3" x-data="libraryWidget({{ $game->id }}, '{{ $userGame?->status }}', {{ $userGame?->rating ?? 'null' }})">
|
2026-05-29 09:37:07 +03:00
|
|
|
|
|
|
|
|
@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
|
2026-05-30 01:57:35 +03:00
|
|
|
<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>
|
2026-05-29 09:37:07 +03:00
|
|
|
</button>
|
2026-05-30 01:57:35 +03:00
|
|
|
<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>
|
2026-05-29 09:37:07 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-30 01:57:35 +03:00
|
|
|
|
|
|
|
|
{{-- 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>
|
2026-05-29 09:37:07 +03:00
|
|
|
</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
|
2026-05-30 01:57:35 +03:00
|
|
|
|
|
|
|
|
{{-- 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>
|
2026-05-29 09:37:07 +03:00
|
|
|
</div>
|
|
|
|
|
|
2026-05-30 02:50:34 +03:00
|
|
|
{{-- Add to List --}}
|
|
|
|
|
@auth
|
|
|
|
|
<div class="mb-6" x-data="addToListWidget({{ $game->id }}, {{ $userLists->toJson() }})">
|
|
|
|
|
<button @click="open = !open"
|
|
|
|
|
class="flex items-center gap-2 px-4 py-2 bg-[#16161d] border border-white/10 hover:border-purple-500/50 text-slate-300 hover:text-white text-sm rounded-lg transition-all">
|
|
|
|
|
<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="M12 4v16m8-8H4"/>
|
|
|
|
|
</svg>
|
|
|
|
|
Add to List
|
|
|
|
|
</button>
|
|
|
|
|
<div x-show="open" @click.outside="open = false" x-transition
|
|
|
|
|
class="mt-1 w-56 bg-[#16161d] border border-white/10 rounded-lg shadow-xl overflow-hidden z-10"
|
|
|
|
|
style="display:none;">
|
2026-05-30 18:56:17 +03:00
|
|
|
<template x-if="lists.length === 0">
|
|
|
|
|
<div class="px-4 py-3 text-sm text-slate-500">
|
|
|
|
|
No lists yet.
|
|
|
|
|
<a href="{{ route('lists.index') }}" class="text-purple-400 hover:text-purple-300">Create one</a>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template x-for="list in lists" :key="list.slug">
|
|
|
|
|
<button type="button" @click="toggleList(list.slug)"
|
2026-05-30 02:50:34 +03:00
|
|
|
class="w-full flex items-center gap-2 px-4 py-2.5 text-sm hover:bg-white/5 transition-colors"
|
2026-05-30 18:56:17 +03:00
|
|
|
:class="added[list.slug] ? 'text-purple-400' : 'text-slate-300'">
|
|
|
|
|
<svg x-show="added[list.slug]" class="w-4 h-4 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
2026-05-30 02:50:34 +03:00
|
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
|
|
|
|
</svg>
|
2026-05-30 18:56:17 +03:00
|
|
|
<span x-show="!added[list.slug]" class="w-4 h-4 flex-shrink-0"></span>
|
2026-05-30 02:50:34 +03:00
|
|
|
<span x-text="list.title" class="truncate"></span>
|
|
|
|
|
</button>
|
|
|
|
|
</template>
|
2026-05-30 18:56:17 +03:00
|
|
|
<template x-if="lists.length > 0">
|
|
|
|
|
<div class="border-t border-white/5 px-4 py-2">
|
|
|
|
|
<a href="{{ route('lists.index') }}" class="text-xs text-slate-500 hover:text-slate-300 transition-colors">Manage lists →</a>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
2026-05-30 02:50:34 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@else
|
|
|
|
|
<div class="mb-6"></div>
|
|
|
|
|
@endauth
|
|
|
|
|
|
2026-05-29 09:37:07 +03:00
|
|
|
{{-- Companies --}}
|
|
|
|
|
@if($game->companies->isNotEmpty())
|
2026-06-01 00:24:05 +03:00
|
|
|
<div class="text-sm text-slate-500 space-y-1 mb-4">
|
2026-05-29 09:37:07 +03:00
|
|
|
@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
|
|
|
|
|
|
2026-06-01 00:24:05 +03:00
|
|
|
{{-- Extended metadata --}}
|
|
|
|
|
<div class="text-sm text-slate-400 space-y-2">
|
|
|
|
|
@if($game->collection)
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<span class="text-slate-500 w-36 flex-shrink-0">Series / Collection</span>
|
|
|
|
|
<span class="text-slate-300">{{ $game->collection->name }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
@if($game->gameModes->isNotEmpty())
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<span class="text-slate-500 w-36 flex-shrink-0">Game Modes</span>
|
|
|
|
|
<span class="text-slate-300">{{ $game->gameModes->pluck('name')->join(', ') }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
@if($game->themes->isNotEmpty())
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<span class="text-slate-500 w-36 flex-shrink-0">Themes</span>
|
|
|
|
|
<span class="text-slate-300">{{ $game->themes->pluck('name')->join(', ') }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
@if($game->playerPerspectives->isNotEmpty())
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<span class="text-slate-500 w-36 flex-shrink-0">Perspective</span>
|
|
|
|
|
<span class="text-slate-300">{{ $game->playerPerspectives->pluck('name')->join(', ') }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
@if($game->aggregated_rating)
|
|
|
|
|
<div class="flex gap-2 items-center">
|
|
|
|
|
<span class="text-slate-500 w-36 flex-shrink-0">Critics Score</span>
|
|
|
|
|
<span class="px-2 py-0.5 rounded text-xs font-bold
|
|
|
|
|
@if($game->aggregated_rating >= 75) bg-green-900/50 text-green-300
|
|
|
|
|
@elseif($game->aggregated_rating >= 50) bg-yellow-900/50 text-yellow-300
|
|
|
|
|
@else bg-red-900/50 text-red-300
|
|
|
|
|
@endif">
|
|
|
|
|
{{ round($game->aggregated_rating) }}/100
|
|
|
|
|
</span>
|
|
|
|
|
@if($game->aggregated_rating_count)
|
|
|
|
|
<span class="text-slate-600 text-xs">({{ $game->aggregated_rating_count }} {{ Str::plural('review', $game->aggregated_rating_count) }})</span>
|
|
|
|
|
@endif
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
@if($game->age_rating_pegi || $game->age_rating_esrb)
|
|
|
|
|
<div class="flex gap-2 items-center">
|
|
|
|
|
<span class="text-slate-500 w-36 flex-shrink-0">Age Ratings</span>
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
@if($game->age_rating_pegi)
|
|
|
|
|
<span class="px-2 py-0.5 bg-[#16161d] border border-white/10 rounded text-xs font-bold text-slate-300">PEGI {{ $game->age_rating_pegi }}</span>
|
|
|
|
|
@endif
|
|
|
|
|
@if($game->age_rating_esrb)
|
|
|
|
|
<span class="px-2 py-0.5 bg-[#16161d] border border-white/10 rounded text-xs font-bold text-slate-300">ESRB {{ $game->age_rating_esrb }}</span>
|
|
|
|
|
@endif
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{{-- Websites --}}
|
|
|
|
|
@php
|
|
|
|
|
$importantCategories = [1, 13, 17, 16];
|
|
|
|
|
$featuredSites = $game->websites->whereIn('category', $importantCategories)->sortBy(fn($w) => array_search($w->category, $importantCategories));
|
|
|
|
|
@endphp
|
|
|
|
|
@if($featuredSites->isNotEmpty())
|
|
|
|
|
<div class="flex flex-wrap gap-2 mt-4">
|
|
|
|
|
@foreach($featuredSites as $site)
|
|
|
|
|
<a href="{{ $site->url }}" target="_blank" rel="noopener noreferrer"
|
|
|
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-[#16161d] border border-white/10 hover:border-purple-500/50 rounded-lg text-xs text-slate-300 hover:text-white transition-all">
|
|
|
|
|
@if($site->category === 13)
|
|
|
|
|
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor"><path d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.187.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.029 4.524 4.524s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999-5.373 11.999-12S18.606 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.455 1.012H7.54zm11.415-9.303c0-1.662-1.353-3.015-3.015-3.015-1.665 0-3.015 1.353-3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015-1.35 3.015-3.015zm-5.273-.005c0-1.252 1.013-2.266 2.265-2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251-1.017 2.265-2.266 2.265-1.252 0-2.265-1.014-2.265-2.265z"/></svg>
|
|
|
|
|
@elseif($site->category === 17)
|
|
|
|
|
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12s5.37 12 12 12 12-5.37 12-12S18.63 0 12 0zm5.61 17.26c-.2.2-.52.2-.72 0l-4.89-4.89-4.89 4.89c-.2.2-.52.2-.72 0-.2-.2-.2-.52 0-.72l4.89-4.89-4.89-4.89c-.2-.2-.2-.52 0-.72.2-.2.52-.2.72 0l4.89 4.89 4.89-4.89c.2-.2.52-.2.72 0 .2.2.2.52 0 .72l-4.89 4.89 4.89 4.89c.2.2.2.52 0 .72z"/></svg>
|
|
|
|
|
@elseif($site->category === 16)
|
|
|
|
|
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor"><path d="M0 0h24v24H0z" fill="none"/><path d="M21 6.5C21 4.6 19.5 3 17.5 3c-1.9 0-3.5 1.6-3.5 3.5C14 8.4 15.6 10 17.5 10c1.9 0 3.5-1.5 3.5-3.5zM6.5 3C4.6 3 3 4.6 3 6.5S4.6 10 6.5 10 10 8.4 10 6.5 8.4 3 6.5 3zM17.5 14c-1.9 0-3.5 1.6-3.5 3.5S15.6 21 17.5 21c1.9 0 3.5-1.5 3.5-3.5S19.4 14 17.5 14zm-11 0C4.6 14 3 15.6 3 17.5S4.6 21 6.5 21 10 19.4 10 17.5 8.4 14 6.5 14z"/></svg>
|
|
|
|
|
@else
|
|
|
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
|
|
|
|
@endif
|
|
|
|
|
{{ $site->label }}
|
|
|
|
|
</a>
|
|
|
|
|
@endforeach
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
|
2026-05-29 09:37:07 +03:00
|
|
|
</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
|
|
|
|
|
|
2026-06-01 00:24:05 +03:00
|
|
|
{{-- Storyline (only if different from summary) --}}
|
|
|
|
|
@if($game->storyline && $game->storyline !== $game->summary)
|
|
|
|
|
<div class="mt-8">
|
|
|
|
|
<h2 class="text-lg font-semibold text-white mb-3">Storyline</h2>
|
|
|
|
|
<p class="text-slate-400 leading-relaxed max-w-3xl">{{ $game->storyline }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
|
|
|
|
|
{{-- Trailers --}}
|
|
|
|
|
@if($game->videos->isNotEmpty())
|
|
|
|
|
<div class="mt-10 pt-8 border-t border-white/5"
|
|
|
|
|
x-data="{
|
|
|
|
|
activeVideo: '{{ $game->videos->first()->video_id }}',
|
|
|
|
|
activeName: {{ json_encode($game->videos->first()->name ?? 'Trailer') }},
|
|
|
|
|
}">
|
|
|
|
|
<h2 class="text-lg font-semibold text-white mb-4">Videos</h2>
|
|
|
|
|
<div class="flex flex-col lg:flex-row gap-4">
|
|
|
|
|
<div class="flex-1 min-w-0">
|
|
|
|
|
<div class="aspect-video rounded-xl overflow-hidden bg-black">
|
|
|
|
|
<iframe
|
|
|
|
|
:src="'https://www.youtube.com/embed/' + activeVideo + '?rel=0'"
|
|
|
|
|
class="w-full h-full"
|
|
|
|
|
loading="lazy"
|
|
|
|
|
allowfullscreen
|
|
|
|
|
title="Game video"
|
|
|
|
|
></iframe>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="text-sm text-slate-500 mt-2" x-text="activeName"></p>
|
|
|
|
|
</div>
|
|
|
|
|
@if($game->videos->count() > 1)
|
|
|
|
|
<div class="flex lg:flex-col gap-2 overflow-x-auto lg:overflow-y-auto lg:max-h-72 lg:w-48 flex-shrink-0">
|
|
|
|
|
@foreach($game->videos as $video)
|
|
|
|
|
<button @click="activeVideo = '{{ $video->video_id }}'; activeName = {{ json_encode($video->name ?? 'Video') }}"
|
|
|
|
|
class="flex-shrink-0 lg:flex-shrink rounded-lg overflow-hidden border-2 transition-all w-32 lg:w-full aspect-video bg-black"
|
|
|
|
|
:class="activeVideo === '{{ $video->video_id }}' ? 'border-purple-500' : 'border-transparent hover:border-white/20'">
|
|
|
|
|
<img src="https://img.youtube.com/vi/{{ $video->video_id }}/mqdefault.jpg"
|
|
|
|
|
alt="{{ $video->name ?? 'Video' }}"
|
|
|
|
|
class="w-full h-full object-cover">
|
|
|
|
|
</button>
|
|
|
|
|
@endforeach
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
|
2026-05-29 09:37:07 +03:00
|
|
|
{{-- Screenshots --}}
|
|
|
|
|
@if($game->screenshots->isNotEmpty())
|
2026-05-30 01:57:35 +03:00
|
|
|
<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()">
|
2026-05-29 09:37:07 +03:00
|
|
|
<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">
|
2026-05-30 01:57:35 +03:00
|
|
|
@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">
|
2026-05-31 03:02:12 +03:00
|
|
|
<img src="{{ $shot->thumb_url ?? $shot->url }}" alt="{{ $game->title }} screenshot {{ $loop->iteration }}" class="w-full aspect-video object-cover">
|
2026-05-30 01:57:35 +03:00
|
|
|
</button>
|
2026-05-29 09:37:07 +03:00
|
|
|
@endforeach
|
|
|
|
|
</div>
|
2026-05-30 01:57:35 +03:00
|
|
|
|
|
|
|
|
{{-- 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 --}}
|
2026-05-31 03:02:12 +03:00
|
|
|
<img :src="screenshots[current]?.url" :alt="`{{ addslashes($game->title) }} screenshot ` + (current + 1)" class="max-h-[85vh] max-w-[90vw] rounded-lg shadow-2xl object-contain">
|
2026-05-30 01:57:35 +03:00
|
|
|
|
|
|
|
|
{{-- 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>
|
2026-05-29 09:37:07 +03:00
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
|
2026-05-30 12:02:24 +03:00
|
|
|
{{-- Comments --}}
|
|
|
|
|
@php
|
|
|
|
|
$commentsData = $comments->map(fn($c) => [
|
|
|
|
|
'id' => $c->id,
|
|
|
|
|
'body' => $c->body,
|
2026-05-30 23:47:32 +03:00
|
|
|
'user' => ['name' => $c->user->name, 'slug' => $c->user->slug],
|
2026-05-30 12:02:24 +03:00
|
|
|
'user_id' => $c->user_id,
|
|
|
|
|
'created_at' => $c->created_at->diffForHumans(),
|
|
|
|
|
'replies' => $c->replies->map(fn($r) => [
|
|
|
|
|
'id' => $r->id,
|
|
|
|
|
'body' => $r->body,
|
2026-05-30 23:47:32 +03:00
|
|
|
'user' => ['name' => $r->user->name, 'slug' => $r->user->slug],
|
2026-05-30 12:02:24 +03:00
|
|
|
'user_id' => $r->user_id,
|
|
|
|
|
'created_at' => $r->created_at->diffForHumans(),
|
|
|
|
|
])->values()->all(),
|
|
|
|
|
])->values()->all();
|
|
|
|
|
@endphp
|
|
|
|
|
<div class="mt-10 pt-8 border-t border-white/5" id="comments"
|
|
|
|
|
x-data="commentsWidget({{ Js::from($commentsData) }}, '{{ route('games.comments.store', $game) }}', {{ auth()->id() ?? 'null' }})">
|
|
|
|
|
|
|
|
|
|
<h2 class="text-lg font-semibold text-white mb-4">Discussion</h2>
|
|
|
|
|
<div class="max-w-3xl">
|
|
|
|
|
|
|
|
|
|
{{-- New comment form (auth only) --}}
|
|
|
|
|
@auth
|
|
|
|
|
<div class="mb-6 bg-[#16161d] border border-white/10 rounded-lg p-4">
|
|
|
|
|
<textarea x-model="body" rows="3"
|
|
|
|
|
placeholder="Share your thoughts…"
|
|
|
|
|
class="w-full bg-[#0f0f13] border border-white/10 rounded-lg px-4 py-3 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 resize-none transition mb-3"></textarea>
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
<span class="text-xs text-slate-600" x-text="body.length + ' / 1000'"></span>
|
|
|
|
|
<button @click="submitComment()"
|
|
|
|
|
:disabled="!body.trim() || submitting"
|
|
|
|
|
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm font-semibold rounded-lg transition-colors">
|
|
|
|
|
Post Comment
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@else
|
|
|
|
|
<div class="mb-6 bg-[#16161d] border border-white/10 rounded-lg p-4 text-center">
|
|
|
|
|
<p class="text-slate-400 text-sm">
|
|
|
|
|
<a href="{{ route('login') }}" class="text-purple-400 hover:text-purple-300 transition-colors">Log in</a>
|
|
|
|
|
to join the discussion.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
@endauth
|
|
|
|
|
|
|
|
|
|
{{-- Comment list --}}
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
<template x-if="comments.length === 0">
|
|
|
|
|
<p class="text-slate-500 text-sm">No comments yet. Be the first!</p>
|
|
|
|
|
</template>
|
|
|
|
|
<template x-for="comment in comments" :key="comment.id">
|
|
|
|
|
<div class="bg-[#16161d] rounded-lg p-4 border border-white/5">
|
|
|
|
|
{{-- Comment header --}}
|
|
|
|
|
<div class="flex items-start justify-between gap-2 mb-2">
|
|
|
|
|
<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 flex-shrink-0"
|
|
|
|
|
x-text="comment.user.name.charAt(0).toUpperCase()"></div>
|
2026-05-30 23:47:32 +03:00
|
|
|
<span class="text-sm font-medium text-slate-300" x-text="comment.user.slug || comment.user.name"></span>
|
2026-05-30 12:02:24 +03:00
|
|
|
<span class="text-xs text-slate-600" x-text="comment.created_at"></span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
@auth
|
|
|
|
|
<button x-show="comment.user_id === authUserId"
|
|
|
|
|
@click="deleteComment(comment)"
|
|
|
|
|
class="text-xs text-red-500 hover:text-red-400 transition-colors">Delete</button>
|
|
|
|
|
@endauth
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{{-- Body --}}
|
|
|
|
|
<p class="text-slate-400 text-sm leading-relaxed mb-3" x-text="comment.body"></p>
|
|
|
|
|
|
|
|
|
|
{{-- Replies --}}
|
|
|
|
|
<div class="ml-6 space-y-3 mb-3">
|
|
|
|
|
<template x-for="reply in comment.replies" :key="reply.id">
|
|
|
|
|
<div class="bg-[#0f0f13] rounded-lg p-3 border border-white/5">
|
|
|
|
|
<div class="flex items-start justify-between gap-2 mb-1">
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<div class="w-5 h-5 rounded-full bg-purple-800 flex items-center justify-center text-xs font-bold text-white flex-shrink-0"
|
|
|
|
|
x-text="reply.user.name.charAt(0).toUpperCase()"></div>
|
2026-05-30 23:47:32 +03:00
|
|
|
<span class="text-xs font-medium text-slate-300" x-text="reply.user.slug || reply.user.name"></span>
|
2026-05-30 12:02:24 +03:00
|
|
|
<span class="text-xs text-slate-600" x-text="reply.created_at"></span>
|
|
|
|
|
</div>
|
|
|
|
|
@auth
|
|
|
|
|
<button x-show="reply.user_id === authUserId"
|
|
|
|
|
@click="deleteReply(reply, comment)"
|
|
|
|
|
class="text-xs text-red-500 hover:text-red-400 transition-colors">Delete</button>
|
|
|
|
|
@endauth
|
|
|
|
|
</div>
|
|
|
|
|
<p class="text-slate-400 text-xs leading-relaxed" x-text="reply.body"></p>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{{-- Reply toggle --}}
|
|
|
|
|
@auth
|
|
|
|
|
<div>
|
|
|
|
|
<button @click="comment.showReply = !comment.showReply"
|
|
|
|
|
class="text-xs text-slate-500 hover:text-purple-400 transition-colors">
|
|
|
|
|
<span x-text="comment.showReply ? 'Cancel' : 'Reply'"></span>
|
|
|
|
|
</button>
|
|
|
|
|
<div x-show="comment.showReply" x-transition class="mt-2">
|
|
|
|
|
<textarea x-model="comment.replyBody" rows="2"
|
|
|
|
|
placeholder="Write a reply…"
|
|
|
|
|
class="w-full bg-[#0f0f13] border border-white/10 rounded-lg px-3 py-2 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 resize-none transition mb-2"></textarea>
|
|
|
|
|
<button @click="submitReply(comment)"
|
|
|
|
|
:disabled="!comment.replyBody?.trim()"
|
|
|
|
|
class="px-3 py-1.5 bg-purple-600 hover:bg-purple-700 disabled:opacity-40 disabled:cursor-not-allowed text-white text-xs font-semibold rounded-lg transition-colors">
|
|
|
|
|
Post Reply
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@endauth
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-29 09:37:07 +03:00
|
|
|
{{-- Reviews --}}
|
2026-05-30 02:50:34 +03:00
|
|
|
<div class="mt-10 pt-8 border-t border-white/5" id="reviews">
|
2026-05-29 09:37:07 +03:00
|
|
|
<h2 class="text-lg font-semibold text-white mb-4">Reviews</h2>
|
2026-05-30 02:50:34 +03:00
|
|
|
<div class="max-w-3xl">
|
|
|
|
|
|
|
|
|
|
{{-- Review List --}}
|
|
|
|
|
<div id="review-list" class="space-y-4 mb-8">
|
|
|
|
|
@forelse($game->reviews->take(10) as $review)
|
|
|
|
|
<div class="bg-[#16161d] rounded-lg p-5 border border-white/5" id="review-{{ $review->id }}">
|
2026-05-29 09:37:07 +03:00
|
|
|
<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>
|
2026-05-30 23:47:32 +03:00
|
|
|
<span class="text-sm font-medium text-slate-300">{{ $review->user->slug ?? $review->user->name }}</span>
|
2026-05-30 02:50:34 +03:00
|
|
|
@if($review->has_spoilers)
|
|
|
|
|
<span class="px-1.5 py-0.5 text-xs bg-yellow-900/40 text-yellow-400 border border-yellow-700/30 rounded">Spoilers</span>
|
|
|
|
|
@endif
|
2026-05-29 09:37:07 +03:00
|
|
|
</div>
|
2026-05-30 02:50:34 +03:00
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
@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
|
|
|
|
|
@auth
|
|
|
|
|
@if(auth()->id() === $review->user_id)
|
|
|
|
|
<button onclick="deleteReview({{ $review->id }})"
|
|
|
|
|
class="text-xs text-red-500 hover:text-red-400 transition-colors">Delete</button>
|
|
|
|
|
@endif
|
|
|
|
|
@endauth
|
2026-05-29 09:37:07 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="text-slate-400 text-sm leading-relaxed">{{ $review->body }}</p>
|
|
|
|
|
</div>
|
2026-05-30 02:50:34 +03:00
|
|
|
@empty
|
|
|
|
|
<p class="text-slate-500" id="no-reviews-msg">No reviews yet. Be the first!</p>
|
|
|
|
|
@endforelse
|
2026-05-29 09:37:07 +03:00
|
|
|
</div>
|
2026-05-30 02:50:34 +03:00
|
|
|
|
|
|
|
|
{{-- Write / Edit Review Form --}}
|
|
|
|
|
@auth
|
|
|
|
|
<div x-data="reviewForm(
|
2026-05-30 13:26:41 +03:00
|
|
|
'{{ route('reviews.store', ['game' => $game->slug]) }}',
|
2026-05-30 02:50:34 +03:00
|
|
|
{{ $userReview ? $userReview->id : 'null' }},
|
|
|
|
|
{{ $userReview ? json_encode($userReview->body) : 'null' }},
|
|
|
|
|
{{ $userReview ? ($userReview->rating ?? 'null') : 'null' }},
|
|
|
|
|
{{ $userReview ? ($userReview->has_spoilers ? 'true' : 'false') : 'false' }}
|
|
|
|
|
)"
|
|
|
|
|
class="bg-[#16161d] border border-white/10 rounded-lg p-5">
|
|
|
|
|
|
|
|
|
|
<h3 class="text-sm font-semibold text-white mb-4" x-text="reviewId ? 'Your Review' : 'Write a Review'"></h3>
|
|
|
|
|
|
|
|
|
|
<div x-show="!reviewId || editing">
|
|
|
|
|
<textarea x-model="body" rows="5" placeholder="Share your thoughts (minimum 50 characters)…"
|
|
|
|
|
class="w-full bg-[#0f0f13] border border-white/10 rounded-lg px-4 py-3 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 resize-none transition"></textarea>
|
2026-05-30 17:18:19 +03:00
|
|
|
<p class="text-xs text-slate-500 mt-1 mb-3" x-text="body.length + ' | minimum 50 characters'"></p>
|
2026-05-30 02:50:34 +03:00
|
|
|
|
|
|
|
|
<div class="flex flex-wrap items-center gap-4 mb-4">
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<label class="text-sm text-slate-400">Rating (optional):</label>
|
|
|
|
|
<input type="number" min="1" max="10" x-model="rating"
|
|
|
|
|
class="w-16 bg-[#0f0f13] 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>
|
|
|
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
|
|
|
<input type="checkbox" x-model="hasSpoilers" class="w-4 h-4 accent-purple-600">
|
|
|
|
|
<span class="text-sm text-slate-400">Contains spoilers</span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<button @click="submitReview()"
|
|
|
|
|
:disabled="body.length < 50"
|
|
|
|
|
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm font-semibold rounded-lg transition-colors"
|
|
|
|
|
x-text="reviewId ? 'Update Review' : 'Submit Review'"></button>
|
|
|
|
|
<button x-show="reviewId" @click="editing = false"
|
|
|
|
|
class="px-4 py-2 text-slate-400 hover:text-slate-200 text-sm transition-colors">Cancel</button>
|
|
|
|
|
<span x-show="error" class="text-sm text-red-400" x-text="error"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div x-show="reviewId && !editing" class="flex items-center gap-3">
|
|
|
|
|
<p class="text-sm text-slate-400">You've already reviewed this game.</p>
|
|
|
|
|
<button @click="editing = true" class="text-sm text-purple-400 hover:text-purple-300 transition-colors">Edit</button>
|
|
|
|
|
<button @click="deleteMyReview()" class="text-sm text-red-400 hover:text-red-300 transition-colors">Delete</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@else
|
|
|
|
|
<div class="bg-[#16161d] border border-white/10 rounded-lg p-5 text-center">
|
|
|
|
|
<p class="text-slate-400 text-sm">
|
|
|
|
|
<a href="{{ route('login') }}" class="text-purple-400 hover:text-purple-300 transition-colors">Log in</a>
|
|
|
|
|
to write a review.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
@endauth
|
|
|
|
|
|
|
|
|
|
</div>
|
2026-05-29 09:37:07 +03:00
|
|
|
</div>
|
|
|
|
|
|
2026-05-31 03:02:12 +03:00
|
|
|
@if($similarGames->isNotEmpty())
|
|
|
|
|
<div class="mt-10 pt-8 border-t border-white/5">
|
2026-06-01 00:24:05 +03:00
|
|
|
<h2 class="text-lg font-semibold text-white mb-4">
|
|
|
|
|
@if($game->similarGames->isNotEmpty())
|
|
|
|
|
Similar Games
|
|
|
|
|
@else
|
|
|
|
|
You Might Also Like
|
|
|
|
|
@endif
|
|
|
|
|
</h2>
|
2026-05-31 03:02:12 +03:00
|
|
|
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
|
|
|
@foreach($similarGames as $similar)
|
|
|
|
|
<x-game-card :game="$similar" />
|
|
|
|
|
@endforeach
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
|
2026-05-29 09:37:07 +03:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@push('scripts')
|
2026-05-30 02:50:34 +03:00
|
|
|
<script type="application/ld+json">
|
2026-05-31 03:02:12 +03:00
|
|
|
{
|
|
|
|
|
"@@context": "https://schema.org",
|
|
|
|
|
"@@type": "BreadcrumbList",
|
|
|
|
|
"itemListElement": [
|
|
|
|
|
{"@@type": "ListItem", "position": 1, "name": "Home", "item": "{{ url('/') }}"},
|
|
|
|
|
{"@@type": "ListItem", "position": 2, "name": "Games", "item": "{{ route('games.index') }}"},
|
|
|
|
|
{"@@type": "ListItem", "position": 3, "name": "{{ addslashes($game->title) }}", "item": "{{ route('games.show', $game) }}"}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
<script type="application/ld+json">
|
2026-05-30 02:50:34 +03:00
|
|
|
{
|
2026-05-30 12:38:40 +03:00
|
|
|
"@@context": "https://schema.org",
|
|
|
|
|
"@@type": "VideoGame",
|
2026-05-30 02:50:34 +03:00
|
|
|
"name": {{ json_encode($game->title) }},
|
|
|
|
|
"description": {{ json_encode($game->summary ?? '') }},
|
|
|
|
|
"url": {{ json_encode(route('games.show', $game)) }}
|
|
|
|
|
@if($game->genres->isNotEmpty())
|
|
|
|
|
,"genre": {{ json_encode($game->genres->pluck('name')->values()) }}
|
|
|
|
|
@endif
|
|
|
|
|
@if($game->platforms->isNotEmpty())
|
|
|
|
|
,"gamePlatform": {{ json_encode($game->platforms->pluck('name')->values()) }}
|
|
|
|
|
@endif
|
|
|
|
|
@if($game->rating_count > 0)
|
|
|
|
|
,"aggregateRating": {
|
2026-05-30 12:38:40 +03:00
|
|
|
"@@type": "AggregateRating",
|
2026-05-30 02:50:34 +03:00
|
|
|
"ratingValue": {{ $game->rating_avg }},
|
|
|
|
|
"ratingCount": {{ $game->rating_count }},
|
|
|
|
|
"bestRating": 10,
|
|
|
|
|
"worstRating": 1
|
|
|
|
|
}
|
|
|
|
|
@endif
|
2026-05-31 03:02:12 +03:00
|
|
|
@if($game->reviews->isNotEmpty())
|
|
|
|
|
,"review": [
|
|
|
|
|
@foreach($game->reviews->take(5) as $r)
|
|
|
|
|
@if($r->rating)
|
|
|
|
|
{
|
|
|
|
|
"@@type": "Review",
|
|
|
|
|
"author": {"@@type": "Person", "name": "{{ addslashes($r->user->name ?? 'Anonymous') }}"},
|
|
|
|
|
"reviewRating": {"@@type": "Rating", "ratingValue": "{{ $r->rating }}", "bestRating": "10", "worstRating": "1"},
|
|
|
|
|
"reviewBody": "{{ addslashes(Str::limit(strip_tags($r->body), 200)) }}",
|
|
|
|
|
"datePublished": "{{ $r->created_at->toDateString() }}"
|
|
|
|
|
}{{ !$loop->last ? ',' : '' }}
|
|
|
|
|
@endif
|
|
|
|
|
@endforeach
|
|
|
|
|
]
|
|
|
|
|
@endif
|
2026-05-30 02:50:34 +03:00
|
|
|
}
|
|
|
|
|
</script>
|
2026-05-29 09:37:07 +03:00
|
|
|
<script>
|
|
|
|
|
function libraryWidget(gameId, initialStatus, initialRating) {
|
|
|
|
|
return {
|
|
|
|
|
gameId,
|
|
|
|
|
status: initialStatus,
|
|
|
|
|
rating: initialRating,
|
2026-05-30 01:57:35 +03:00
|
|
|
toast: null,
|
|
|
|
|
pendingStatus: null,
|
|
|
|
|
pendingRating: null,
|
2026-05-29 09:37:07 +03:00
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
},
|
|
|
|
|
|
2026-05-30 01:57:35 +03:00
|
|
|
showToast(message) {
|
|
|
|
|
this.toast = message;
|
|
|
|
|
setTimeout(() => { this.toast = null; }, 2500);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async addToLibraryWithRating() {
|
2026-05-29 09:37:07 +03:00
|
|
|
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',
|
|
|
|
|
},
|
2026-05-30 01:57:35 +03:00
|
|
|
body: JSON.stringify({ game_id: this.gameId, status: this.pendingStatus, rating: this.pendingRating || null }),
|
2026-05-29 09:37:07 +03:00
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
2026-05-30 01:57:35 +03:00
|
|
|
this.showToast('Added to library!');
|
|
|
|
|
setTimeout(() => window.location.reload(), 1000);
|
2026-05-29 09:37:07 +03:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-05-30 02:50:34 +03:00
|
|
|
|
|
|
|
|
function reviewForm(storeUrl, initialReviewId, initialBody, initialRating, initialSpoilers) {
|
|
|
|
|
return {
|
|
|
|
|
storeUrl,
|
|
|
|
|
reviewId: initialReviewId,
|
|
|
|
|
body: initialBody || '',
|
|
|
|
|
rating: initialRating,
|
|
|
|
|
hasSpoilers: initialSpoilers || false,
|
|
|
|
|
editing: false,
|
|
|
|
|
error: null,
|
|
|
|
|
|
|
|
|
|
async submitReview() {
|
|
|
|
|
this.error = null;
|
|
|
|
|
const isEdit = !!this.reviewId;
|
|
|
|
|
const url = isEdit ? `/reviews/${this.reviewId}` : this.storeUrl;
|
|
|
|
|
const method = isEdit ? 'PUT' : 'POST';
|
|
|
|
|
|
|
|
|
|
const res = await fetch(url, {
|
|
|
|
|
method,
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
body: this.body,
|
|
|
|
|
rating: this.rating || null,
|
|
|
|
|
has_spoilers: this.hasSpoilers,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (res.status === 409) {
|
|
|
|
|
this.error = 'You already have a review for this game.';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
|
this.error = data.message || 'Something went wrong.';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.location.reload();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async deleteMyReview() {
|
|
|
|
|
if (!confirm('Delete your review?')) return;
|
|
|
|
|
const res = await fetch(`/reviews/${this.reviewId}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: {
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) window.location.reload();
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deleteReview(id) {
|
|
|
|
|
if (!confirm('Delete this review?')) return;
|
|
|
|
|
fetch(`/reviews/${id}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: {
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
}).then(r => { if (r.ok) window.location.reload(); });
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 12:02:24 +03:00
|
|
|
function commentsWidget(initialComments, storeUrl, authUserId) {
|
|
|
|
|
return {
|
|
|
|
|
comments: initialComments.map(c => ({ ...c, showReply: false, replyBody: '' })),
|
|
|
|
|
body: '',
|
|
|
|
|
submitting: false,
|
|
|
|
|
authUserId,
|
|
|
|
|
|
|
|
|
|
async submitComment() {
|
|
|
|
|
if (!this.body.trim() || this.submitting) return;
|
|
|
|
|
this.submitting = true;
|
|
|
|
|
const res = await fetch(storeUrl, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ body: this.body }),
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const comment = await res.json();
|
|
|
|
|
this.comments.unshift({ ...comment, replies: [], showReply: false, replyBody: '' });
|
|
|
|
|
this.body = '';
|
|
|
|
|
}
|
|
|
|
|
this.submitting = false;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async submitReply(parentComment) {
|
|
|
|
|
if (!parentComment.replyBody?.trim()) return;
|
|
|
|
|
const res = await fetch(storeUrl, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ body: parentComment.replyBody, parent_id: parentComment.id }),
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const reply = await res.json();
|
|
|
|
|
parentComment.replies.push(reply);
|
|
|
|
|
parentComment.showReply = false;
|
|
|
|
|
parentComment.replyBody = '';
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async deleteComment(comment) {
|
|
|
|
|
if (!confirm('Delete this comment?')) return;
|
|
|
|
|
const res = await fetch(`/comments/${comment.id}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: {
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
this.comments = this.comments.filter(c => c.id !== comment.id);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async deleteReply(reply, parentComment) {
|
|
|
|
|
if (!confirm('Delete this reply?')) return;
|
|
|
|
|
const res = await fetch(`/comments/${reply.id}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: {
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
parentComment.replies = parentComment.replies.filter(r => r.id !== reply.id);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 02:50:34 +03:00
|
|
|
function addToListWidget(gameId, lists) {
|
|
|
|
|
return {
|
|
|
|
|
gameId,
|
|
|
|
|
lists,
|
|
|
|
|
open: false,
|
|
|
|
|
added: {},
|
|
|
|
|
|
2026-05-30 18:56:17 +03:00
|
|
|
async toggleList(listSlug) {
|
|
|
|
|
const isAdded = this.added[listSlug];
|
2026-05-30 02:50:34 +03:00
|
|
|
const url = isAdded
|
2026-05-30 18:56:17 +03:00
|
|
|
? `/lists/${listSlug}/games/${this.gameId}`
|
|
|
|
|
: `/lists/${listSlug}/games`;
|
2026-05-30 02:50:34 +03:00
|
|
|
const method = isAdded ? 'DELETE' : 'POST';
|
|
|
|
|
|
|
|
|
|
const res = await fetch(url, {
|
|
|
|
|
method,
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: method === 'POST' ? JSON.stringify({ game_id: this.gameId }) : null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
2026-05-30 18:56:17 +03:00
|
|
|
this.added[listSlug] = !isAdded;
|
2026-05-30 02:50:34 +03:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-05-29 09:37:07 +03:00
|
|
|
</script>
|
|
|
|
|
@endpush
|
|
|
|
|
|
|
|
|
|
@endsection
|