Core implementation: controllers, views, routes, models, import command
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Game;
|
||||
use App\Models\GameScreenshot;
|
||||
use App\Models\Genre;
|
||||
use App\Models\Platform;
|
||||
use App\Services\IgdbService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ImportGamesCommand extends Command
|
||||
{
|
||||
protected $signature = 'games:import {--limit=100} {--offset=0} {--popular} {--new}';
|
||||
protected $description = 'Import games from IGDB into the local database';
|
||||
|
||||
public function handle(IgdbService $igdb): int
|
||||
{
|
||||
$limit = (int) $this->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Game;
|
||||
use App\Models\Genre;
|
||||
use App\Models\Platform;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GameController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Game::published()->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'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Game;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$topRated = Game::published()
|
||||
->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'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Game;
|
||||
use App\Models\UserGame;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LibraryController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->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.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserGame;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function show(User $user)
|
||||
{
|
||||
$stats = [
|
||||
'played' => $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'));
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserGame;
|
||||
|
||||
class UserGamePolicy
|
||||
{
|
||||
public function update(User $user, UserGame $userGame): bool
|
||||
{
|
||||
return $user->id === $userGame->user_id;
|
||||
}
|
||||
|
||||
public function delete(User $user, UserGame $userGame): bool
|
||||
{
|
||||
return $user->id === $userGame->user_id;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user