From 5813bc779a5d17ff1f0d1dc5924f5e166843bbab Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 29 May 2026 09:37:07 +0300 Subject: [PATCH] Core implementation: controllers, views, routes, models, import command --- .gitignore | 1 + app/Console/Commands/ImportGamesCommand.php | 146 +++++++++ app/Http/Controllers/GameController.php | 106 ++++++ app/Http/Controllers/HomeController.php | 27 ++ app/Http/Controllers/LibraryController.php | 73 +++++ app/Http/Controllers/UserController.php | 42 +++ app/Models/Company.php | 8 +- app/Models/GameScreenshot.php | 8 +- app/Models/Genre.php | 8 +- app/Models/Platform.php | 8 +- app/Policies/UserGamePolicy.php | 19 ++ app/Providers/AppServiceProvider.php | 5 +- app/Services/IgdbService.php | 14 +- .../views/components/game-card.blade.php | 38 +++ resources/views/games/index.blade.php | 114 +++++++ resources/views/games/new-releases.blade.php | 23 ++ resources/views/games/search.blade.php | 36 +++ resources/views/games/show.blade.php | 304 ++++++++++++++++++ resources/views/games/top-rated.blade.php | 23 ++ resources/views/home.blade.php | 92 ++++++ resources/views/layouts/app.blade.php | 155 +++++++-- resources/views/users/library.blade.php | 48 +++ resources/views/users/show.blade.php | 71 ++++ routes/web.php | 29 +- 24 files changed, 1361 insertions(+), 37 deletions(-) create mode 100644 app/Console/Commands/ImportGamesCommand.php create mode 100644 app/Http/Controllers/GameController.php create mode 100644 app/Http/Controllers/HomeController.php create mode 100644 app/Http/Controllers/LibraryController.php create mode 100644 app/Http/Controllers/UserController.php create mode 100644 app/Policies/UserGamePolicy.php create mode 100644 resources/views/components/game-card.blade.php create mode 100644 resources/views/games/index.blade.php create mode 100644 resources/views/games/new-releases.blade.php create mode 100644 resources/views/games/search.blade.php create mode 100644 resources/views/games/show.blade.php create mode 100644 resources/views/games/top-rated.blade.php create mode 100644 resources/views/home.blade.php create mode 100644 resources/views/users/library.blade.php create mode 100644 resources/views/users/show.blade.php diff --git a/.gitignore b/.gitignore index 7be55e2..136d4e9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ _ide_helper.php Homestead.json Homestead.yaml Thumbs.db +.claude/ diff --git a/app/Console/Commands/ImportGamesCommand.php b/app/Console/Commands/ImportGamesCommand.php new file mode 100644 index 0000000..d2094e3 --- /dev/null +++ b/app/Console/Commands/ImportGamesCommand.php @@ -0,0 +1,146 @@ +option('limit'); + $offset = (int) $this->option('offset'); + + $this->info("Fetching {$limit} games from IGDB (offset: {$offset})…"); + + try { + if ($this->option('new')) { + $results = $igdb->getNewReleases($limit); + } else { + $results = $igdb->getPopularGames($limit, $offset); + } + } catch (\Exception $e) { + $this->error('IGDB request failed: ' . $e->getMessage()); + return self::FAILURE; + } + + if (empty($results)) { + $this->warn('No games returned from IGDB.'); + return self::SUCCESS; + } + + $imported = 0; + $bar = $this->output->createProgressBar(count($results)); + $bar->start(); + + foreach ($results as $data) { + try { + $this->upsertGame($data, $igdb); + $imported++; + } catch (\Exception $e) { + $this->newLine(); + $this->warn("Skipped game (igdb_id={$data['id']}): " . $e->getMessage()); + } + $bar->advance(); + } + + $bar->finish(); + $this->newLine(2); + $this->info("Done. Imported/updated {$imported} games."); + + return self::SUCCESS; + } + + private function upsertGame(array $data, IgdbService $igdb): void + { + $igdbId = $data['id']; + $title = $data['name'] ?? 'Unknown'; + $releaseDate = isset($data['first_release_date']) + ? date('Y-m-d', $data['first_release_date']) + : null; + $releaseYear = $releaseDate ? substr($releaseDate, 0, 4) : null; + + $coverUrl = null; + $coverThumbUrl = null; + if (!empty($data['cover']['image_id'])) { + $coverUrl = $igdb::coverUrl($data['cover']['image_id'], 'cover_big'); + $coverThumbUrl = $igdb::coverUrl($data['cover']['image_id'], 'cover_small'); + } + + $game = Game::updateOrCreate( + ['igdb_id' => $igdbId], + [ + 'title' => $title, + 'summary' => $data['summary'] ?? null, + 'cover_url' => $coverUrl, + 'cover_thumb_url'=> $coverThumbUrl, + 'release_date' => $releaseDate, + 'release_year' => $releaseYear, + 'status' => 'published', + ] + ); + + // Genres + if (!empty($data['genres'])) { + $genreIds = []; + foreach ($data['genres'] as $g) { + $genre = Genre::firstOrCreate( + ['igdb_id' => $g['id']], + ['name' => $g['name'], 'slug' => \Str::slug($g['name'])] + ); + $genreIds[] = $genre->id; + } + $game->genres()->sync($genreIds); + } + + // Platforms + if (!empty($data['platforms'])) { + $platformIds = []; + foreach ($data['platforms'] as $p) { + $platform = Platform::firstOrCreate( + ['igdb_id' => $p['id']], + ['name' => $p['name'], 'slug' => \Str::slug($p['name'])] + ); + $platformIds[] = $platform->id; + } + $game->platforms()->sync($platformIds); + } + + // Companies (involved_companies) + if (!empty($data['involved_companies'])) { + $companyPivot = []; + foreach ($data['involved_companies'] as $ic) { + if (empty($ic['company']['name'])) continue; + $company = Company::firstOrCreate( + ['igdb_id' => $ic['company']['id']], + ['name' => $ic['company']['name'], 'slug' => \Str::slug($ic['company']['name'])] + ); + $role = $ic['developer'] ? 'developer' : ($ic['publisher'] ? 'publisher' : 'other'); + $companyPivot[$company->id] = ['role' => $role]; + } + $game->companies()->sync($companyPivot); + } + + // Screenshots + if (!empty($data['screenshots'])) { + GameScreenshot::where('game_id', $game->id)->delete(); + foreach ($data['screenshots'] as $i => $shot) { + GameScreenshot::create([ + 'game_id' => $game->id, + 'url' => $igdb::screenshotUrl($shot['image_id'], 'screenshot_big'), + 'thumb_url' => $igdb::screenshotUrl($shot['image_id'], 'screenshot_med'), + 'order' => $i, + ]); + } + } + } +} diff --git a/app/Http/Controllers/GameController.php b/app/Http/Controllers/GameController.php new file mode 100644 index 0000000..f1d3987 --- /dev/null +++ b/app/Http/Controllers/GameController.php @@ -0,0 +1,106 @@ +with(['genres', 'platforms']); + + // Filters + if ($request->filled('genre')) { + $query->whereHas('genres', fn($q) => $q->where('genres.id', $request->genre)); + } + if ($request->filled('platform')) { + $query->whereHas('platforms', fn($q) => $q->where('platforms.id', $request->platform)); + } + if ($request->filled('year')) { + $query->where('release_year', $request->year); + } + + // Sort + match ($request->get('sort', 'rating')) { + 'rating' => $query->orderByDesc('rating_avg')->orderByDesc('rating_count'), + 'release' => $query->orderByDesc('release_date'), + 'title' => $query->orderBy('title'), + 'popular' => $query->orderByDesc('library_count'), + default => $query->orderByDesc('rating_avg'), + }; + + $games = $query->paginate(24)->withQueryString(); + $genres = Genre::orderBy('name')->get(); + $platforms = Platform::orderBy('name')->get(); + + return view('games.index', compact('games', 'genres', 'platforms')); + } + + public function show(Game $game) + { + $game->load(['genres', 'platforms', 'companies', 'screenshots', 'reviews.user']); + + $userGame = null; + if (auth()->check()) { + $userGame = $game->userGames()->where('user_id', auth()->id())->first(); + } + + return view('games.show', compact('game', 'userGame')); + } + + public function search(Request $request) + { + $q = $request->get('q', ''); + $games = collect(); + + if (strlen($q) >= 2) { + $games = Game::published() + ->where('title', 'like', "%{$q}%") + ->orderByDesc('rating_count') + ->limit(20) + ->get(); + } + + if ($request->expectsJson()) { + return response()->json($games->map(fn($g) => [ + 'id' => $g->id, + 'title' => $g->title, + 'slug' => $g->slug, + 'cover_thumb' => $g->cover_thumb_url, + 'release_year' => $g->release_year, + 'rating_avg' => $g->rating_avg, + 'url' => route('games.show', $g), + ])); + } + + return view('games.search', compact('games', 'q')); + } + + public function topRated(Request $request) + { + $games = Game::published() + ->where('rating_count', '>=', 1) + ->orderByDesc('rating_avg') + ->orderByDesc('rating_count') + ->paginate(24) + ->withQueryString(); + + return view('games.top-rated', compact('games')); + } + + public function newReleases(Request $request) + { + $games = Game::published() + ->whereNotNull('release_date') + ->where('release_date', '<=', now()) + ->orderByDesc('release_date') + ->paginate(24) + ->withQueryString(); + + return view('games.new-releases', compact('games')); + } +} diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php new file mode 100644 index 0000000..4bdebaa --- /dev/null +++ b/app/Http/Controllers/HomeController.php @@ -0,0 +1,27 @@ +where('rating_count', '>=', 1) + ->orderByDesc('rating_avg') + ->orderByDesc('rating_count') + ->limit(12) + ->get(); + + $newReleases = Game::published() + ->whereNotNull('release_date') + ->where('release_date', '<=', now()) + ->orderByDesc('release_date') + ->limit(8) + ->get(); + + return view('home', compact('topRated', 'newReleases')); + } +} diff --git a/app/Http/Controllers/LibraryController.php b/app/Http/Controllers/LibraryController.php new file mode 100644 index 0000000..932076b --- /dev/null +++ b/app/Http/Controllers/LibraryController.php @@ -0,0 +1,73 @@ +middleware('auth'); + } + + public function store(Request $request) + { + $request->validate([ + 'game_id' => 'required|exists:games,id', + 'status' => 'required|in:' . implode(',', UserGame::STATUSES), + 'rating' => 'nullable|integer|min:1|max:10', + ]); + + $userGame = UserGame::updateOrCreate( + ['user_id' => auth()->id(), 'game_id' => $request->game_id], + ['status' => $request->status, 'rating' => $request->rating] + ); + + // Recalculate rating on the game + $userGame->game->recalculateRating(); + + if ($request->expectsJson()) { + return response()->json(['status' => 'ok', 'userGame' => $userGame]); + } + + return back()->with('success', 'Added to your library.'); + } + + public function update(Request $request, UserGame $userGame) + { + $this->authorize('update', $userGame); + + $request->validate([ + 'status' => 'sometimes|in:' . implode(',', UserGame::STATUSES), + 'rating' => 'nullable|integer|min:1|max:10', + 'notes' => 'nullable|string|max:1000', + ]); + + $userGame->update($request->only(['status', 'rating', 'notes'])); + $userGame->game->recalculateRating(); + + if ($request->expectsJson()) { + return response()->json(['status' => 'ok', 'userGame' => $userGame]); + } + + return back()->with('success', 'Library entry updated.'); + } + + public function destroy(UserGame $userGame) + { + $this->authorize('delete', $userGame); + + $game = $userGame->game; + $userGame->delete(); + $game->recalculateRating(); + + if (request()->expectsJson()) { + return response()->json(['status' => 'ok']); + } + + return back()->with('success', 'Removed from your library.'); + } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..7d8ba59 --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,42 @@ + $user->library()->where('status', 'played')->count(), + 'playing' => $user->library()->where('status', 'playing')->count(), + 'want_to_play' => $user->library()->where('status', 'want_to_play')->count(), + 'total' => $user->library()->count(), + ]; + + $recentlyPlayed = $user->library() + ->with('game') + ->where('status', 'played') + ->latest('updated_at') + ->limit(6) + ->get(); + + return view('users.show', compact('user', 'stats', 'recentlyPlayed')); + } + + public function library(User $user) + { + $status = request('status'); + $query = $user->library()->with('game'); + + if ($status && in_array($status, UserGame::STATUSES)) { + $query->where('status', $status); + } + + $entries = $query->latest('updated_at')->paginate(24)->withQueryString(); + + return view('users.library', compact('user', 'entries', 'status')); + } +} diff --git a/app/Models/Company.php b/app/Models/Company.php index 5323f59..ae37309 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -3,8 +3,14 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Company extends Model { - // + protected $fillable = ['igdb_id', 'name', 'slug']; + + public function games(): BelongsToMany + { + return $this->belongsToMany(Game::class, 'game_company')->withPivot('role'); + } } diff --git a/app/Models/GameScreenshot.php b/app/Models/GameScreenshot.php index a51bc8a..e27470b 100644 --- a/app/Models/GameScreenshot.php +++ b/app/Models/GameScreenshot.php @@ -3,8 +3,14 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class GameScreenshot extends Model { - // + protected $fillable = ['game_id', 'url', 'thumb_url', 'order']; + + public function game(): BelongsTo + { + return $this->belongsTo(Game::class); + } } diff --git a/app/Models/Genre.php b/app/Models/Genre.php index 9b48517..0875f4c 100644 --- a/app/Models/Genre.php +++ b/app/Models/Genre.php @@ -3,8 +3,14 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Genre extends Model { - // + protected $fillable = ['igdb_id', 'name', 'slug']; + + public function games(): BelongsToMany + { + return $this->belongsToMany(Game::class, 'game_genre'); + } } diff --git a/app/Models/Platform.php b/app/Models/Platform.php index aeb3af9..7286b0c 100644 --- a/app/Models/Platform.php +++ b/app/Models/Platform.php @@ -3,8 +3,14 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Platform extends Model { - // + protected $fillable = ['igdb_id', 'name', 'slug', 'abbreviation']; + + public function games(): BelongsToMany + { + return $this->belongsToMany(Game::class, 'game_platform'); + } } diff --git a/app/Policies/UserGamePolicy.php b/app/Policies/UserGamePolicy.php new file mode 100644 index 0000000..70de3a5 --- /dev/null +++ b/app/Policies/UserGamePolicy.php @@ -0,0 +1,19 @@ +id === $userGame->user_id; + } + + public function delete(User $user, UserGame $userGame): bool + { + return $user->id === $userGame->user_id; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..107d503 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,9 @@ namespace App\Providers; +use App\Models\UserGame; +use App\Policies\UserGamePolicy; +use Illuminate\Support\Facades\Gate; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,6 +22,6 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + Gate::policy(UserGame::class, UserGamePolicy::class); } } diff --git a/app/Services/IgdbService.php b/app/Services/IgdbService.php index 684a132..6b5758e 100644 --- a/app/Services/IgdbService.php +++ b/app/Services/IgdbService.php @@ -102,7 +102,12 @@ class IgdbService { return $this->query('games', " fields id,name,slug,summary,cover.image_id,first_release_date, - genres.name,genres.slug,platforms.abbreviation, + genres.id,genres.name,genres.slug, + platforms.id,platforms.name,platforms.slug,platforms.abbreviation, + involved_companies.company.id,involved_companies.company.name, + involved_companies.company.slug,involved_companies.developer, + involved_companies.publisher, + screenshots.image_id, rating,rating_count; where rating != null & rating_count > 100 & cover != null; sort rating_count desc; @@ -121,7 +126,12 @@ class IgdbService return $this->query('games', " fields id,name,slug,summary,cover.image_id,first_release_date, - genres.name,genres.slug,platforms.abbreviation, + genres.id,genres.name,genres.slug, + platforms.id,platforms.name,platforms.slug,platforms.abbreviation, + involved_companies.company.id,involved_companies.company.name, + involved_companies.company.slug,involved_companies.developer, + involved_companies.publisher, + screenshots.image_id, rating,rating_count; where first_release_date >= {$timestamp} & first_release_date <= {$now} & cover != null; sort first_release_date desc; diff --git a/resources/views/components/game-card.blade.php b/resources/views/components/game-card.blade.php new file mode 100644 index 0000000..fd03864 --- /dev/null +++ b/resources/views/components/game-card.blade.php @@ -0,0 +1,38 @@ +@props(['game']) + + + +
+ @if($game->cover_thumb_url) + {{ $game->title }} + @else +
+ + + +
+ @endif +
+ + +
+

+ {{ $game->title }} +

+
+ {{ $game->release_year ?? '—' }} + @if($game->rating_avg) +
+ + + + {{ number_format($game->rating_avg, 1) }} +
+ @endif +
+
+
diff --git a/resources/views/games/index.blade.php b/resources/views/games/index.blade.php new file mode 100644 index 0000000..e0f5ffa --- /dev/null +++ b/resources/views/games/index.blade.php @@ -0,0 +1,114 @@ +@extends('layouts.app') + +@section('title', 'Browse Games — GameLog') + +@section('content') +
+ +
+ + {{-- Sidebar Filters --}} + + + {{-- Games Grid --}} +
+
+

+ Browse Games + ({{ number_format($games->total()) }}) +

+
+ + @if($games->isEmpty()) +
+

No games found.

+ Clear filters +
+ @else +
+ @foreach($games as $game) + + @endforeach +
+ +
+ {{ $games->links() }} +
+ @endif +
+
+
+@endsection diff --git a/resources/views/games/new-releases.blade.php b/resources/views/games/new-releases.blade.php new file mode 100644 index 0000000..55c389f --- /dev/null +++ b/resources/views/games/new-releases.blade.php @@ -0,0 +1,23 @@ +@extends('layouts.app') + +@section('title', 'New Releases — GameLog') + +@section('content') +
+

New Releases

+

The latest games in the GameLog database.

+ + @if($games->isEmpty()) +
+

No new releases found.

+
+ @else +
+ @foreach($games as $game) + + @endforeach +
+
{{ $games->links() }}
+ @endif +
+@endsection diff --git a/resources/views/games/search.blade.php b/resources/views/games/search.blade.php new file mode 100644 index 0000000..a145283 --- /dev/null +++ b/resources/views/games/search.blade.php @@ -0,0 +1,36 @@ +@extends('layouts.app') + +@section('title', $q ? "Search: {$q} — GameLog" : 'Search Games — GameLog') + +@section('content') +
+ +

+ @if($q) + Search results for "{{ $q }}" + @else + Search Games + @endif +

+ +
+
+ + + + +
+
+ + @if($q && $games->isEmpty()) +

No games found for "{{ $q }}".

+ @elseif($games->isNotEmpty()) +
+ @foreach($games as $game) + + @endforeach +
+ @endif +
+@endsection diff --git a/resources/views/games/show.blade.php b/resources/views/games/show.blade.php new file mode 100644 index 0000000..308b356 --- /dev/null +++ b/resources/views/games/show.blade.php @@ -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') +
+ + {{-- Main Info --}} +
+ + {{-- Cover --}} +
+
+ @if($game->cover_url) + {{ $game->title }} + @else +
+ + + +
+ @endif +
+
+ + {{-- Details --}} +
+ + {{-- Title & Year --}} +
+ @if($game->release_year) + {{ $game->release_year }} + @endif +

{{ $game->title }}

+
+ + {{-- Rating --}} +
+ @if($game->rating_avg) +
+ + + + {{ number_format($game->rating_avg, 1) }} + / 10 +
+ {{ number_format($game->rating_count) }} {{ Str::plural('rating', $game->rating_count) }} + @else + No ratings yet + @endif +
+ + {{-- Genres & Platforms --}} + @if($game->genres->isNotEmpty()) +
+ @foreach($game->genres as $genre) + + {{ $genre->name }} + + @endforeach +
+ @endif + + @if($game->platforms->isNotEmpty()) +
+ @foreach($game->platforms as $platform) + + {{ $platform->name }} + + @endforeach +
+ @endif + + {{-- Add to Library --}} +
+ + @if($userGame) + {{-- Already in library --}} +
+
+ +
+ @foreach(['playing' => 'Playing', 'played' => 'Played', 'want_to_play' => 'Want to Play', 'on_hold' => 'On Hold', 'dropped' => 'Dropped'] as $s => $label) + + @endforeach +
+ +
+
+ + {{-- Rating --}} +
+ Your rating: + + / 10 +
+
+ @else + @auth +
+
+ +
+ @foreach(['playing' => 'Playing', 'played' => 'Played', 'want_to_play' => 'Want to Play', 'on_hold' => 'On Hold', 'dropped' => 'Dropped'] as $s => $label) + + @endforeach +
+
+
+ @else + + Log in to Add to Library + + @endauth + @endif +
+ + {{-- Companies --}} + @if($game->companies->isNotEmpty()) +
+ @php + $developers = $game->companies->where('pivot.role', 'developer'); + $publishers = $game->companies->where('pivot.role', 'publisher'); + @endphp + @if($developers->isNotEmpty()) +
Developer: {{ $developers->pluck('name')->join(', ') }}
+ @endif + @if($publishers->isNotEmpty()) +
Publisher: {{ $publishers->pluck('name')->join(', ') }}
+ @endif +
+ @endif + +
+
+ + {{-- Summary --}} + @if($game->summary) +
+

Summary

+

{{ $game->summary }}

+
+ @endif + + {{-- Screenshots --}} + @if($game->screenshots->isNotEmpty()) +
+

Screenshots

+
+ @foreach($game->screenshots as $shot) + + Screenshot + + @endforeach +
+
+ @endif + + {{-- Reviews --}} +
+

Reviews

+ @if($game->reviews->isEmpty()) +

No reviews yet. Be the first!

+ @else +
+ @foreach($game->reviews->take(5) as $review) +
+
+
+
+ {{ strtoupper(substr($review->user->name, 0, 1)) }} +
+ {{ $review->user->username ?? $review->user->name }} +
+ @if($review->rating) +
+ + + + {{ $review->rating }} + /10 +
+ @endif +
+ @if($review->body) +

{{ $review->body }}

+ @endif +
+ @endforeach +
+ @endif +
+ +
+ +@push('scripts') + +@endpush + +@endsection diff --git a/resources/views/games/top-rated.blade.php b/resources/views/games/top-rated.blade.php new file mode 100644 index 0000000..b4ff7d5 --- /dev/null +++ b/resources/views/games/top-rated.blade.php @@ -0,0 +1,23 @@ +@extends('layouts.app') + +@section('title', 'Top Rated Games — GameLog') + +@section('content') +
+

Top Rated

+

The highest-rated games on GameLog.

+ + @if($games->isEmpty()) +
+

No games rated yet.

+
+ @else +
+ @foreach($games as $game) + + @endforeach +
+
{{ $games->links() }}
+ @endif +
+@endsection diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php new file mode 100644 index 0000000..7163055 --- /dev/null +++ b/resources/views/home.blade.php @@ -0,0 +1,92 @@ +@extends('layouts.app') + +@section('title', 'GameLog — Track Every Game You Play') + +@section('content') + +{{-- Hero --}} +
+
+

+ Track Every Game
+ You Play +

+

+ Rate games, build your library, discover what to play next. +

+ +
+
+ + + + + +
+
+ + @guest + + @endguest +
+
+ +{{-- Top Rated --}} +@if($topRated->isNotEmpty()) +
+
+

Top Rated

+ + See all → + +
+
+ @foreach($topRated as $game) + + @endforeach +
+
+@endif + +{{-- New Releases --}} +@if($newReleases->isNotEmpty()) +
+
+

New Releases

+ + See all → + +
+
+ @foreach($newReleases as $game) + + @endforeach +
+
+@endif + +{{-- Empty state --}} +@if($topRated->isEmpty() && $newReleases->isEmpty()) +
+
+ + + +

No games in the database yet.

+

Run php artisan games:import to import games from IGDB.

+
+
+@endif + +@endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 0a471a4..f4d69ae 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -1,36 +1,137 @@ - - - - - + + + + + + @yield('title', 'GameLog — Track Every Game You Play') + + @hasSection('canonical') + + @endif - {{ config('app.name', 'Laravel') }} + + + + + - - - + + + - - @vite(['resources/css/app.css', 'resources/js/app.js']) - - -
- @include('layouts.navigation') + @vite(['resources/css/app.css', 'resources/js/app.js']) + @stack('styles') + + - - @isset($header) -
-
- {{ $header }} + +
- @endisset +
- -
- {{ $slot }} -
+ + + + +
+ @auth +
+ +
+ @if(auth()->user()->username) + Profile + My Library + @endif + Settings +
+
+ @csrf + +
+
+
+ @else + Log in + + Sign up + + @endauth +
+ - + + + + @if(session('success')) +
+ {{ session('success') }} +
+ @endif + @if(session('error')) +
+ {{ session('error') }} +
+ @endif + + +
+ @yield('content') +
+ + + + + @stack('scripts') + diff --git a/resources/views/users/library.blade.php b/resources/views/users/library.blade.php new file mode 100644 index 0000000..7e4c983 --- /dev/null +++ b/resources/views/users/library.blade.php @@ -0,0 +1,48 @@ +@extends('layouts.app') + +@section('title', ($user->username ?? $user->name) . "'s Library — GameLog") + +@section('content') +
+ + + + {{-- Status Tabs --}} +
+ @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) + + {{ $tabLabel }} + + @endforeach +
+ + @if($entries->isEmpty()) +
+

No games here yet.

+
+ @else +
+ @foreach($entries as $entry) + + @endforeach +
+
{{ $entries->links() }}
+ @endif + +
+@endsection diff --git a/resources/views/users/show.blade.php b/resources/views/users/show.blade.php new file mode 100644 index 0000000..04ad235 --- /dev/null +++ b/resources/views/users/show.blade.php @@ -0,0 +1,71 @@ +@extends('layouts.app') + +@section('title', ($user->username ?? $user->name) . ' — GameLog') + +@section('content') +
+ + {{-- Profile Header --}} +
+ @if($user->avatar) + + @else +
+ {{ strtoupper(substr($user->name, 0, 1)) }} +
+ @endif + +
+

{{ $user->username ?? $user->name }}

+ @if($user->username && $user->name !== $user->username) +

{{ $user->name }}

+ @endif + @if($user->bio) +

{{ $user->bio }}

+ @endif + + {{-- Stats --}} +
+
+
{{ number_format($stats['total']) }}
+
Games
+
+
+
{{ number_format($stats['played']) }}
+
Played
+
+
+
{{ number_format($stats['playing']) }}
+
Playing
+
+
+
{{ number_format($stats['want_to_play']) }}
+
Want to Play
+
+
+
+ + +
+ + {{-- Recently Played --}} + @if($recentlyPlayed->isNotEmpty()) +
+

Recently Played

+
+ @foreach($recentlyPlayed as $entry) + + @endforeach +
+
+ @else +

No games tracked yet.

+ @endif + +
+@endsection diff --git a/routes/web.php b/routes/web.php index 74bb7ca..0db31d4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,16 +1,39 @@ name('home'); + +// Games (public) +Route::get('/games', [GameController::class, 'index'])->name('games.index'); +Route::get('/games/search', [GameController::class, 'search'])->name('games.search'); +Route::get('/top-rated', [GameController::class, 'topRated'])->name('games.top-rated'); +Route::get('/new-releases', [GameController::class, 'newReleases'])->name('games.new-releases'); +Route::get('/games/{game:slug}', [GameController::class, 'show'])->name('games.show'); + +// User profiles (public) +Route::get('/users/{user:username}', [UserController::class, 'show'])->name('users.show'); +Route::get('/users/{user:username}/library', [UserController::class, 'library'])->name('users.library'); + +// Library (auth required) +Route::middleware('auth')->group(function () { + Route::post('/library', [LibraryController::class, 'store'])->name('library.store'); + Route::put('/library/{userGame}', [LibraryController::class, 'update'])->name('library.update'); + Route::delete('/library/{userGame}', [LibraryController::class, 'destroy'])->name('library.destroy'); }); +// Dashboard (Breeze default, keep for now) Route::get('/dashboard', function () { - return view('dashboard'); + return redirect()->route('home'); })->middleware(['auth', 'verified'])->name('dashboard'); +// Profile (Breeze) Route::middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');