Core implementation: controllers, views, routes, models, import command

This commit is contained in:
Mark
2026-05-29 09:37:07 +03:00
parent 8e1fe4a2f5
commit 5813bc779a
24 changed files with 1361 additions and 37 deletions
@@ -0,0 +1,38 @@
@props(['game'])
<a href="{{ route('games.show', $game) }}"
class="group block bg-[#16161d] rounded-lg overflow-hidden hover:ring-1 hover:ring-purple-500 transition-all duration-200">
<!-- Cover -->
<div class="aspect-[3/4] bg-[#0f0f13] overflow-hidden">
@if($game->cover_thumb_url)
<img src="{{ $game->cover_thumb_url }}"
alt="{{ $game->title }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300">
@else
<div class="w-full h-full flex items-center justify-center">
<svg class="w-12 h-12 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>
<!-- Info -->
<div class="p-3">
<h3 class="text-sm font-medium text-slate-200 group-hover:text-white truncate leading-tight">
{{ $game->title }}
</h3>
<div class="flex items-center justify-between mt-1.5">
<span class="text-xs text-slate-500">{{ $game->release_year ?? '—' }}</span>
@if($game->rating_avg)
<div class="flex items-center gap-1">
<svg class="w-3 h-3 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-xs text-slate-400 font-medium">{{ number_format($game->rating_avg, 1) }}</span>
</div>
@endif
</div>
</div>
</a>
+114
View File
@@ -0,0 +1,114 @@
@extends('layouts.app')
@section('title', 'Browse Games — GameLog')
@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<div class="flex flex-col lg:flex-row gap-8">
{{-- Sidebar Filters --}}
<aside class="lg:w-56 flex-shrink-0">
<form method="GET" action="{{ route('games.index') }}" id="filter-form">
<div class="bg-[#16161d] rounded-xl border border-white/5 p-5 space-y-6">
<div>
<h3 class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Sort By</h3>
<select name="sort" onchange="document.getElementById('filter-form').submit()"
class="w-full bg-[#0f0f13] border border-white/10 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-purple-500">
<option value="rating" {{ request('sort', 'rating') === 'rating' ? 'selected' : '' }}>Top Rated</option>
<option value="release" {{ request('sort') === 'release' ? 'selected' : '' }}>Newest</option>
<option value="popular" {{ request('sort') === 'popular' ? 'selected' : '' }}>Most Popular</option>
<option value="title" {{ request('sort') === 'title' ? 'selected' : '' }}>A–Z</option>
</select>
</div>
@if($genres->isNotEmpty())
<div>
<h3 class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Genre</h3>
<div class="space-y-1.5 max-h-52 overflow-y-auto pr-1">
<label class="flex items-center gap-2 cursor-pointer group">
<input type="radio" name="genre" value="" {{ !request('genre') ? 'checked' : '' }}
onchange="this.form.submit()"
class="accent-purple-500">
<span class="text-sm text-slate-400 group-hover:text-slate-200 transition-colors">All Genres</span>
</label>
@foreach($genres as $genre)
<label class="flex items-center gap-2 cursor-pointer group">
<input type="radio" name="genre" value="{{ $genre->id }}" {{ request('genre') == $genre->id ? 'checked' : '' }}
onchange="this.form.submit()"
class="accent-purple-500">
<span class="text-sm text-slate-400 group-hover:text-slate-200 transition-colors">{{ $genre->name }}</span>
</label>
@endforeach
</div>
</div>
@endif
@if($platforms->isNotEmpty())
<div>
<h3 class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Platform</h3>
<div class="space-y-1.5 max-h-52 overflow-y-auto pr-1">
<label class="flex items-center gap-2 cursor-pointer group">
<input type="radio" name="platform" value="" {{ !request('platform') ? 'checked' : '' }}
onchange="this.form.submit()"
class="accent-purple-500">
<span class="text-sm text-slate-400 group-hover:text-slate-200 transition-colors">All Platforms</span>
</label>
@foreach($platforms as $platform)
<label class="flex items-center gap-2 cursor-pointer group">
<input type="radio" name="platform" value="{{ $platform->id }}" {{ request('platform') == $platform->id ? 'checked' : '' }}
onchange="this.form.submit()"
class="accent-purple-500">
<span class="text-sm text-slate-400 group-hover:text-slate-200 transition-colors">{{ $platform->name }}</span>
</label>
@endforeach
</div>
</div>
@endif
@if(request()->anyFilled(['genre', 'platform', 'year']))
<a href="{{ route('games.index') }}"
class="block text-center text-xs text-purple-400 hover:text-purple-300 transition-colors pt-2 border-t border-white/5">
Clear Filters
</a>
@endif
</div>
{{-- Keep sort in URL when filtering --}}
@if(request('sort'))
<input type="hidden" name="sort" value="{{ request('sort') }}">
@endif
</form>
</aside>
{{-- Games Grid --}}
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between mb-6">
<h1 class="text-xl font-bold text-white">
Browse Games
<span class="text-slate-500 font-normal text-base ml-2">({{ number_format($games->total()) }})</span>
</h1>
</div>
@if($games->isEmpty())
<div class="text-center py-24 text-slate-500">
<p class="text-lg">No games found.</p>
<a href="{{ route('games.index') }}" class="mt-3 inline-block text-sm text-purple-400 hover:text-purple-300">Clear filters</a>
</div>
@else
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 gap-4">
@foreach($games as $game)
<x-game-card :game="$game" />
@endforeach
</div>
<div class="mt-8">
{{ $games->links() }}
</div>
@endif
</div>
</div>
</div>
@endsection
@@ -0,0 +1,23 @@
@extends('layouts.app')
@section('title', 'New Releases — GameLog')
@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<h1 class="text-2xl font-bold text-white mb-2">New Releases</h1>
<p class="text-slate-500 mb-8">The latest games in the GameLog database.</p>
@if($games->isEmpty())
<div class="text-center py-24 text-slate-500">
<p>No new releases found.</p>
</div>
@else
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
@foreach($games as $game)
<x-game-card :game="$game" />
@endforeach
</div>
<div class="mt-8">{{ $games->links() }}</div>
@endif
</div>
@endsection
+36
View File
@@ -0,0 +1,36 @@
@extends('layouts.app')
@section('title', $q ? "Search: {$q} — GameLog" : 'Search Games — GameLog')
@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<h1 class="text-2xl font-bold text-white mb-6">
@if($q)
Search results for "<span class="text-purple-400">{{ $q }}</span>"
@else
Search Games
@endif
</h1>
<form action="{{ route('games.search') }}" method="GET" class="mb-8 max-w-xl">
<div class="relative">
<svg class="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0"/>
</svg>
<input type="text" name="q" value="{{ $q }}" placeholder="Search for a game…" autofocus
class="w-full bg-[#16161d] border border-white/10 rounded-xl pl-12 pr-4 py-3 text-base text-slate-200 placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition">
</div>
</form>
@if($q && $games->isEmpty())
<p class="text-slate-500">No games found for "{{ $q }}".</p>
@elseif($games->isNotEmpty())
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
@foreach($games as $game)
<x-game-card :game="$game" />
@endforeach
</div>
@endif
</div>
@endsection
+304
View File
@@ -0,0 +1,304 @@
@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
+23
View File
@@ -0,0 +1,23 @@
@extends('layouts.app')
@section('title', 'Top Rated Games — GameLog')
@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<h1 class="text-2xl font-bold text-white mb-2">Top Rated</h1>
<p class="text-slate-500 mb-8">The highest-rated games on GameLog.</p>
@if($games->isEmpty())
<div class="text-center py-24 text-slate-500">
<p>No games rated yet.</p>
</div>
@else
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
@foreach($games as $game)
<x-game-card :game="$game" />
@endforeach
</div>
<div class="mt-8">{{ $games->links() }}</div>
@endif
</div>
@endsection
+92
View File
@@ -0,0 +1,92 @@
@extends('layouts.app')
@section('title', 'GameLog — Track Every Game You Play')
@section('content')
{{-- Hero --}}
<section class="bg-gradient-to-b from-[#1a1025] to-[#0f0f13] py-20 px-4">
<div class="max-w-3xl mx-auto text-center">
<h1 class="text-4xl sm:text-5xl font-bold text-white leading-tight mb-4">
Track Every Game<br>
<span class="text-purple-400">You Play</span>
</h1>
<p class="text-lg text-slate-400 mb-8">
Rate games, build your library, discover what to play next.
</p>
<form action="{{ route('games.search') }}" method="GET" class="max-w-xl mx-auto">
<div class="relative">
<svg class="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0"/>
</svg>
<input type="text" name="q" placeholder="Search for a game…"
class="w-full bg-[#16161d] border border-white/10 rounded-xl pl-12 pr-4 py-4 text-base text-slate-200 placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 transition">
<button type="submit" class="absolute right-3 top-1/2 -translate-y-1/2 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium rounded-lg transition-colors">
Search
</button>
</div>
</form>
@guest
<div class="mt-8 flex items-center justify-center gap-4">
<a href="{{ route('register') }}" class="px-6 py-3 bg-purple-600 hover:bg-purple-700 text-white font-semibold rounded-lg transition-colors">
Create Free Account
</a>
<a href="{{ route('games.index') }}" class="px-6 py-3 border border-white/10 hover:border-white/20 text-slate-300 font-semibold rounded-lg transition-colors">
Browse Games
</a>
</div>
@endguest
</div>
</section>
{{-- Top Rated --}}
@if($topRated->isNotEmpty())
<section class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-white">Top Rated</h2>
<a href="{{ route('games.top-rated') }}" class="text-sm text-purple-400 hover:text-purple-300 transition-colors">
See all →
</a>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
@foreach($topRated as $game)
<x-game-card :game="$game" />
@endforeach
</div>
</section>
@endif
{{-- New Releases --}}
@if($newReleases->isNotEmpty())
<section class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 border-t border-white/5">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-white">New Releases</h2>
<a href="{{ route('games.new-releases') }}" class="text-sm text-purple-400 hover:text-purple-300 transition-colors">
See all →
</a>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
@foreach($newReleases as $game)
<x-game-card :game="$game" />
@endforeach
</div>
</section>
@endif
{{-- Empty state --}}
@if($topRated->isEmpty() && $newReleases->isEmpty())
<section class="max-w-3xl mx-auto px-4 py-24 text-center">
<div class="text-slate-500 mb-4">
<svg class="w-16 h-16 mx-auto mb-4 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>
<p class="text-lg text-slate-400">No games in the database yet.</p>
<p class="text-sm mt-2">Run <code class="bg-[#16161d] px-2 py-1 rounded text-purple-400">php artisan games:import</code> to import games from IGDB.</p>
</div>
</section>
@endif
@endsection
+128 -27
View File
@@ -1,36 +1,137 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title', 'GameLog — Track Every Game You Play')</title>
<meta name="description" content="@yield('description', 'GameLog lets you track every game you play, rate your favorites, and discover what to play next.')">
@hasSection('canonical')
<link rel="canonical" href="@yield('canonical')">
@endif
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Open Graph -->
<meta property="og:title" content="@yield('og_title', 'GameLog')">
<meta property="og:description" content="@yield('og_description', 'Track every game you play.')">
<meta property="og:type" content="@yield('og_type', 'website')">
<meta property="og:url" content="@yield('og_url', url()->current())">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
@include('layouts.navigation')
@vite(['resources/css/app.css', 'resources/js/app.js'])
@stack('styles')
</head>
<body class="h-full bg-[#0f0f13] text-slate-200 antialiased font-sans">
<!-- Page Heading -->
@isset($header)
<header class="bg-white dark:bg-gray-800 shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
<!-- Navigation -->
<nav class="bg-[#16161d] border-b border-white/5 sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<!-- Left: Logo + Nav Links -->
<div class="flex items-center gap-8">
<a href="{{ route('home') }}" class="text-xl font-bold text-purple-400 hover:text-purple-300 transition-colors">
GameLog
</a>
<div class="hidden md:flex items-center gap-6">
<a href="{{ route('games.index') }}" class="text-sm text-slate-400 hover:text-slate-200 transition-colors">Browse</a>
<a href="{{ route('games.top-rated') }}" class="text-sm text-slate-400 hover:text-slate-200 transition-colors">Top Rated</a>
<a href="{{ route('games.new-releases') }}" class="text-sm text-slate-400 hover:text-slate-200 transition-colors">New Releases</a>
</div>
</header>
@endisset
</div>
<!-- Page Content -->
<main>
{{ $slot }}
</main>
<!-- Center: Search -->
<div class="flex-1 max-w-md mx-8 hidden md:block">
<form action="{{ route('games.search') }}" method="GET">
<div class="relative">
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0"/>
</svg>
<input type="text" name="q" value="{{ request('q') }}" placeholder="Search games…"
class="w-full bg-[#0f0f13] border border-white/10 rounded-lg pl-10 pr-4 py-2 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition">
</div>
</form>
</div>
<!-- Right: Auth -->
<div class="flex items-center gap-3">
@auth
<div class="relative" x-data="{ open: false }">
<button @click="open = !open" class="flex items-center gap-2 text-sm text-slate-400 hover:text-slate-200 transition-colors">
@if(auth()->user()->avatar)
<img src="{{ auth()->user()->avatar }}" class="w-8 h-8 rounded-full object-cover border border-white/10" alt="">
@else
<div class="w-8 h-8 rounded-full bg-purple-600 flex items-center justify-center text-xs font-bold text-white">
{{ strtoupper(substr(auth()->user()->name, 0, 1)) }}
</div>
@endif
<span class="hidden sm:block">{{ auth()->user()->username ?? auth()->user()->name }}</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 right-0 mt-2 w-48 bg-[#16161d] border border-white/10 rounded-lg shadow-xl overflow-hidden z-50">
@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.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>
@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>
<div class="border-t border-white/5"></div>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="w-full text-left px-4 py-2.5 text-sm text-slate-400 hover:bg-white/5 hover:text-white transition-colors">
Log Out
</button>
</form>
</div>
</div>
@else
<a href="{{ route('login') }}" class="text-sm text-slate-400 hover:text-slate-200 transition-colors">Log in</a>
<a href="{{ route('register') }}" class="text-sm px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium">
Sign up
</a>
@endauth
</div>
</div>
</div>
</body>
</nav>
<!-- Flash Messages -->
@if(session('success'))
<div class="bg-green-900/50 border-b border-green-700/50 text-green-300 text-sm text-center py-2 px-4">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="bg-red-900/50 border-b border-red-700/50 text-red-300 text-sm text-center py-2 px-4">
{{ session('error') }}
</div>
@endif
<!-- Page Content -->
<main>
@yield('content')
</main>
<!-- Footer -->
<footer class="border-t border-white/5 mt-16 py-10">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row items-center justify-between gap-4">
<div class="text-slate-500 text-sm">
© {{ date('Y') }} GameLog. Track every game you play.
</div>
<div class="flex items-center gap-6 text-sm text-slate-500">
<a href="{{ route('games.index') }}" class="hover:text-slate-300 transition-colors">Browse</a>
<a href="{{ route('games.top-rated') }}" class="hover:text-slate-300 transition-colors">Top Rated</a>
<a href="{{ route('games.new-releases') }}" class="hover:text-slate-300 transition-colors">New Releases</a>
</div>
</div>
</div>
</footer>
@stack('scripts')
</body>
</html>
+48
View File
@@ -0,0 +1,48 @@
@extends('layouts.app')
@section('title', ($user->username ?? $user->name) . "'s Library — GameLog")
@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-2xl font-bold text-white">
<a href="{{ route('users.show', $user->username) }}" class="hover:text-purple-400 transition-colors">
{{ $user->username ?? $user->name }}
</a>'s Library
</h1>
</div>
</div>
{{-- Status Tabs --}}
<div class="flex flex-wrap gap-2 mb-8 border-b border-white/5 pb-4">
@php
$tabs = ['' => 'All', 'playing' => 'Playing', 'played' => 'Played', 'want_to_play' => 'Want to Play', 'on_hold' => 'On Hold', 'dropped' => 'Dropped'];
@endphp
@foreach($tabs as $tabStatus => $tabLabel)
<a href="{{ route('users.library', ['user' => $user->username, 'status' => $tabStatus ?: null]) }}"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors
{{ $status === $tabStatus || (!$status && !$tabStatus)
? 'bg-purple-600 text-white'
: 'text-slate-400 hover:text-slate-200 hover:bg-white/5' }}">
{{ $tabLabel }}
</a>
@endforeach
</div>
@if($entries->isEmpty())
<div class="text-center py-24 text-slate-500">
<p class="text-lg">No games here yet.</p>
</div>
@else
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
@foreach($entries as $entry)
<x-game-card :game="$entry->game" />
@endforeach
</div>
<div class="mt-8">{{ $entries->links() }}</div>
@endif
</div>
@endsection
+71
View File
@@ -0,0 +1,71 @@
@extends('layouts.app')
@section('title', ($user->username ?? $user->name) . ' — GameLog')
@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
{{-- Profile Header --}}
<div class="flex items-start gap-6 mb-10">
@if($user->avatar)
<img src="{{ $user->avatar }}" class="w-20 h-20 rounded-full object-cover border-2 border-white/10" alt="">
@else
<div class="w-20 h-20 rounded-full bg-purple-700 flex items-center justify-center text-2xl font-bold text-white flex-shrink-0">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
@endif
<div class="flex-1">
<h1 class="text-2xl font-bold text-white">{{ $user->username ?? $user->name }}</h1>
@if($user->username && $user->name !== $user->username)
<p class="text-slate-500 text-sm">{{ $user->name }}</p>
@endif
@if($user->bio)
<p class="text-slate-400 mt-2 max-w-xl">{{ $user->bio }}</p>
@endif
{{-- Stats --}}
<div class="flex flex-wrap gap-6 mt-4">
<div class="text-center">
<div class="text-xl font-bold text-white">{{ number_format($stats['total']) }}</div>
<div class="text-xs text-slate-500 uppercase tracking-wide">Games</div>
</div>
<div class="text-center">
<div class="text-xl font-bold text-white">{{ number_format($stats['played']) }}</div>
<div class="text-xs text-slate-500 uppercase tracking-wide">Played</div>
</div>
<div class="text-center">
<div class="text-xl font-bold text-white">{{ number_format($stats['playing']) }}</div>
<div class="text-xs text-slate-500 uppercase tracking-wide">Playing</div>
</div>
<div class="text-center">
<div class="text-xl font-bold text-white">{{ number_format($stats['want_to_play']) }}</div>
<div class="text-xs text-slate-500 uppercase tracking-wide">Want to Play</div>
</div>
</div>
</div>
<div class="flex gap-2">
<a href="{{ route('users.library', $user->username) }}"
class="px-4 py-2 border border-white/10 hover:border-white/20 text-slate-300 hover:text-white text-sm font-medium rounded-lg transition-colors">
View Library
</a>
</div>
</div>
{{-- Recently Played --}}
@if($recentlyPlayed->isNotEmpty())
<div>
<h2 class="text-lg font-semibold text-white mb-4">Recently Played</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
@foreach($recentlyPlayed as $entry)
<x-game-card :game="$entry->game" />
@endforeach
</div>
</div>
@else
<p class="text-slate-500">No games tracked yet.</p>
@endif
</div>
@endsection