lots of progress
This commit is contained in:
@@ -40,12 +40,7 @@ class BuildGameLibrary extends Command
|
|||||||
$startTimestamp = strtotime("{$year}-01-01");
|
$startTimestamp = strtotime("{$year}-01-01");
|
||||||
$endTimestamp = strtotime("{$year}-12-31");
|
$endTimestamp = strtotime("{$year}-12-31");
|
||||||
|
|
||||||
// Generate a random query to get diverse results
|
$games = $igdbService->get($limit);
|
||||||
$query = $this->generateRandomQuery();
|
|
||||||
|
|
||||||
$this->info("Searching for games from {$year} with query: {$query}");
|
|
||||||
|
|
||||||
$games = $igdbService->get();
|
|
||||||
|
|
||||||
if (empty($games)) {
|
if (empty($games)) {
|
||||||
$this->warn("No games found for this batch. Moving to next batch...");
|
$this->warn("No games found for this batch. Moving to next batch...");
|
||||||
@@ -66,6 +61,7 @@ class BuildGameLibrary extends Command
|
|||||||
$totalAdded++;
|
$totalAdded++;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->warn("\nFailed to process game: {$e->getMessage()}");
|
$this->warn("\nFailed to process game: {$e->getMessage()}");
|
||||||
|
dd($e);
|
||||||
$totalSkipped++;
|
$totalSkipped++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use Illuminate\Console\Command;
|
|||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use JetBrains\PhpStorm\NoReturn;
|
||||||
use ZipArchive;
|
use ZipArchive;
|
||||||
|
|
||||||
class FetchIgdbDumps extends Command
|
class FetchIgdbDumps extends Command
|
||||||
@@ -68,6 +69,7 @@ class FetchIgdbDumps extends Command
|
|||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[NoReturn]
|
||||||
private function fetchDumps(string $type, int $limit, int $offset, string $outputDir, IgdbAuthService $authService): void
|
private function fetchDumps(string $type, int $limit, int $offset, string $outputDir, IgdbAuthService $authService): void
|
||||||
{
|
{
|
||||||
$endpoint = self::ENDPOINTS[$type];
|
$endpoint = self::ENDPOINTS[$type];
|
||||||
@@ -81,13 +83,13 @@ class FetchIgdbDumps extends Command
|
|||||||
$accessToken = $authService->getAccessToken();
|
$accessToken = $authService->getAccessToken();
|
||||||
|
|
||||||
$response = Http::withHeaders([
|
$response = Http::withHeaders([
|
||||||
'Client-ID' => config('services.igdb.credentials.client_id'),
|
'Client-ID' => config('igdb.credentials.client_id'),
|
||||||
'Authorization' => 'Bearer ' . $accessToken,
|
'Authorization' => 'Bearer ' . $accessToken,
|
||||||
])->post($endpoint, [
|
])->post($endpoint, [
|
||||||
'fields' => $fields,
|
'fields' => $fields,
|
||||||
'limit' => $limit,
|
'limit' => $limit,
|
||||||
'offset' => $offset,
|
'offset' => $offset,
|
||||||
'where' => 'rating_count > 0',
|
// 'where' => 'rating_count > 0',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!$response->successful()) {
|
if (!$response->successful()) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
abstract class Controller
|
abstract class Controller extends \Illuminate\Routing\Controller
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,4 +29,79 @@ class GameController extends Controller
|
|||||||
|
|
||||||
return response()->json($game, 201);
|
return response()->json($game, 201);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$query = Game::query();
|
||||||
|
|
||||||
|
// Apply search
|
||||||
|
if ($request->has('search')) {
|
||||||
|
$search = $request->get('search');
|
||||||
|
$query->where(function($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('summary', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
$sort = $request->get('sort', 'first_release_date');
|
||||||
|
$direction = $request->get('direction', 'desc');
|
||||||
|
|
||||||
|
if ($sort === 'rating') {
|
||||||
|
$query->orderBy('rating', $direction);
|
||||||
|
} elseif ($sort === 'rating_count') {
|
||||||
|
$query->orderBy('rating_count', $direction);
|
||||||
|
} else {
|
||||||
|
$query->orderBy('first_release_date', $direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if ($request->has('platform')) {
|
||||||
|
$query->whereHas('platforms', function ($q) use ($request) {
|
||||||
|
$q->where('slug', $request->platform);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('genre')) {
|
||||||
|
$query->whereHas('genres', function ($q) use ($request) {
|
||||||
|
$q->where('slug', $request->genre);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('year')) {
|
||||||
|
$query->whereYear('first_release_date', $request->year);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('rating_range')) {
|
||||||
|
$range = explode('-', $request->rating_range);
|
||||||
|
if (count($range) === 2) {
|
||||||
|
$min = (float)$range[0];
|
||||||
|
$max = (float)$range[1];
|
||||||
|
$query->whereBetween('rating', [$min, $max]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$games = $query->paginate(12)->withQueryString();
|
||||||
|
|
||||||
|
// Get data for filter dropdowns
|
||||||
|
$platforms = \App\Models\Platform::orderBy('name')->get();
|
||||||
|
$genres = \App\Models\Genre::orderBy('name')->get();
|
||||||
|
|
||||||
|
// Get unique years from games
|
||||||
|
$years = Game::selectRaw('YEAR(first_release_date) as year')
|
||||||
|
->whereNotNull('first_release_date')
|
||||||
|
->distinct()
|
||||||
|
->orderBy('year', 'desc')
|
||||||
|
->pluck('year')
|
||||||
|
->filter()
|
||||||
|
->values();
|
||||||
|
|
||||||
|
return view('games.index', compact('games', 'platforms', 'genres', 'years'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Game $game)
|
||||||
|
{
|
||||||
|
$game->load(['platforms', 'genres', 'screenshots', 'videos', 'similar_games', 'dlcs', 'reviews.user']);
|
||||||
|
return view('games.show', compact('game'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Game;
|
||||||
|
use App\Models\GameList;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class GameListController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$lists = Auth::user()->lists()->withCount('games')->get();
|
||||||
|
return view('lists.index', compact('lists'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('lists.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'description' => 'nullable|string'
|
||||||
|
]);
|
||||||
|
|
||||||
|
Auth::user()->lists()->create([
|
||||||
|
'name' => $request->name,
|
||||||
|
'description' => $request->description
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('lists.index')->with('success', 'List created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(GameList $list)
|
||||||
|
{
|
||||||
|
$this->authorize('view', $list);
|
||||||
|
|
||||||
|
$games = $list->games()->paginate(12);
|
||||||
|
return view('lists.show', compact('list', 'games'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(GameList $list)
|
||||||
|
{
|
||||||
|
$this->authorize('update', $list);
|
||||||
|
return view('lists.edit', compact('list'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, GameList $list)
|
||||||
|
{
|
||||||
|
$this->authorize('update', $list);
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'description' => 'nullable|string'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$list->update([
|
||||||
|
'name' => $request->name,
|
||||||
|
'description' => $request->description
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('lists.show', $list)->with('success', 'List updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(GameList $list)
|
||||||
|
{
|
||||||
|
$this->authorize('delete', $list);
|
||||||
|
|
||||||
|
if ($list->is_default) {
|
||||||
|
return redirect()->back()->with('error', 'Cannot delete default lists.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$list->delete();
|
||||||
|
return redirect()->route('lists.index')->with('success', 'List deleted successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addGame(Request $request, GameList $list)
|
||||||
|
{
|
||||||
|
$this->authorize('update', $list);
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'game_id' => 'required|exists:games,id'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$list->games()->attach($request->game_id);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', 'Game added to list successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeGame(GameList $list, Game $game)
|
||||||
|
{
|
||||||
|
$this->authorize('update', $list);
|
||||||
|
|
||||||
|
$list->games()->detach($game->id);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', 'Game removed from list successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Game;
|
||||||
|
use App\Models\Review;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class ReviewController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth', ['except' => ['index', 'show']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Game $game)
|
||||||
|
{
|
||||||
|
$reviews = $game->reviews()->with('user')->latest()->paginate(10);
|
||||||
|
return view('reviews.index', compact('game', 'reviews'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Game $game)
|
||||||
|
{
|
||||||
|
return view('reviews.create', compact('game'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request, Game $game)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'content' => 'required|string|min:10|max:1000',
|
||||||
|
'recommended' => 'required|boolean',
|
||||||
|
'rating' => 'required|integer|between:1,10',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$review = new Review($validated);
|
||||||
|
$review->user_id = Auth::id();
|
||||||
|
$review->game_id = $game->id;
|
||||||
|
$review->save();
|
||||||
|
|
||||||
|
return redirect()->route('games.show', $game->slug)
|
||||||
|
->with('success', 'Review posted successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Game $game, Review $review)
|
||||||
|
{
|
||||||
|
return view('reviews.show', compact('game', 'review'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Game $game, Review $review)
|
||||||
|
{
|
||||||
|
$this->authorize('update', $review);
|
||||||
|
return view('reviews.edit', compact('game', 'review'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Game $game, Review $review)
|
||||||
|
{
|
||||||
|
$this->authorize('update', $review);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'content' => 'required|string|min:10|max:1000',
|
||||||
|
'recommended' => 'required|boolean',
|
||||||
|
'rating' => 'required|integer|between:1,10',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$review->update($validated);
|
||||||
|
|
||||||
|
return redirect()->route('games.show', $game->slug)
|
||||||
|
->with('success', 'Review updated successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Game $game, Review $review)
|
||||||
|
{
|
||||||
|
$this->authorize('delete', $review);
|
||||||
|
$review->delete();
|
||||||
|
|
||||||
|
return redirect()->route('games.show', $game->slug)
|
||||||
|
->with('success', 'Review deleted successfully!');
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
-3
@@ -5,6 +5,7 @@ namespace App\Models;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class Game extends Model
|
class Game extends Model
|
||||||
{
|
{
|
||||||
@@ -34,11 +35,81 @@ class Game extends Model
|
|||||||
|
|
||||||
public function genres(): BelongsToMany
|
public function genres(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Genre::class);
|
return $this->belongsToMany(Genre::class, 'game_genre', 'game_id', 'genre_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function platforms(): BelongsToMany
|
public function platforms(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Platform::class);
|
return $this->belongsToMany(Platform::class, 'game_platform', 'game_id', 'platform_id');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public function screenshots(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Screenshot::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function videos(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Video::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bundles(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Game::class, 'game_bundles', 'game_id', 'bundle_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collections(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Game::class, 'game_collections', 'game_id', 'collection_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dlcs(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Game::class, 'game_dlcs', 'game_id', 'dlc_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function expansions(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Game::class, 'game_expansions', 'game_id', 'expansion_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function franchises(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Game::class, 'game_franchises', 'game_id', 'franchise_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ports(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Game::class, 'game_ports', 'game_id', 'port_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remakes(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Game::class, 'game_remakes', 'game_id', 'remake_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function similar_games(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Game::class, 'game_similar', 'game_id', 'similar_game_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reviews(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Review::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ratings()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Rating::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lists()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(GameList::class)->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function averageRating()
|
||||||
|
{
|
||||||
|
return $this->ratings()->avg('rating');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class GameList extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'name',
|
||||||
|
'slug',
|
||||||
|
'description',
|
||||||
|
'is_default'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::creating(function ($list) {
|
||||||
|
$list->slug = Str::slug($list->name);
|
||||||
|
});
|
||||||
|
|
||||||
|
static::updating(function ($list) {
|
||||||
|
$list->slug = Str::slug($list->name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function games(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Game::class)->withTimestamps();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Rating extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'game_id',
|
||||||
|
'rating',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'rating' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function game(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Game::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Review extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'game_id',
|
||||||
|
'content',
|
||||||
|
'recommended',
|
||||||
|
'rating',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'recommended' => 'boolean',
|
||||||
|
'rating' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function game(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Game::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
-5
@@ -6,6 +6,7 @@ namespace App\Models;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
@@ -38,11 +39,40 @@ class User extends Authenticatable
|
|||||||
*
|
*
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
*/
|
*/
|
||||||
protected function casts(): array
|
protected $casts = [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function reviews(): HasMany
|
||||||
{
|
{
|
||||||
return [
|
return $this->hasMany(Review::class);
|
||||||
'email_verified_at' => 'datetime',
|
}
|
||||||
'password' => 'hashed',
|
|
||||||
];
|
public function ratings(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Rating::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lists()
|
||||||
|
{
|
||||||
|
return $this->hasMany(GameList::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::created(function ($user) {
|
||||||
|
// Create default lists
|
||||||
|
$defaultLists = [
|
||||||
|
['name' => 'Played Games', 'is_default' => true],
|
||||||
|
['name' => 'Want to Play', 'is_default' => true]
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($defaultLists as $list) {
|
||||||
|
$user->lists()->create($list);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\GameList;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\Response;
|
||||||
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
|
||||||
|
class GameListPolicy
|
||||||
|
{
|
||||||
|
use HandlesAuthorization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any models.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the model.
|
||||||
|
*/
|
||||||
|
public function view(User $user, GameList $list)
|
||||||
|
{
|
||||||
|
return $user->id === $list->user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create models.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the model.
|
||||||
|
*/
|
||||||
|
public function update(User $user, GameList $list)
|
||||||
|
{
|
||||||
|
return $user->id === $list->user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the model.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, GameList $list)
|
||||||
|
{
|
||||||
|
return $user->id === $list->user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the model.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, GameList $gameList): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the model.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, GameList $gameList): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Review;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\Response;
|
||||||
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
|
||||||
|
class ReviewPolicy
|
||||||
|
{
|
||||||
|
use HandlesAuthorization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any models.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the model.
|
||||||
|
*/
|
||||||
|
public function view(User $user, Review $review): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create models.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the model.
|
||||||
|
*/
|
||||||
|
public function update(User $user, Review $review): bool
|
||||||
|
{
|
||||||
|
return $user->id === $review->user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the model.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, Review $review): bool
|
||||||
|
{
|
||||||
|
return $user->id === $review->user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the model.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, Review $review): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the model.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, Review $review): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Pagination\Paginator;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@@ -19,6 +20,6 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
//
|
Paginator::useBootstrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Log;
|
|||||||
class IgdbAuthService
|
class IgdbAuthService
|
||||||
{
|
{
|
||||||
private const TOKEN_CACHE_KEY = 'igdb_access_token';
|
private const TOKEN_CACHE_KEY = 'igdb_access_token';
|
||||||
private const TOKEN_EXPIRY = 3600; // 1 hour in seconds
|
private const TOKEN_EXPIRY = 3600*24*31; // 1 month in seconds
|
||||||
|
|
||||||
public function getAccessToken(): string
|
public function getAccessToken(): string
|
||||||
{
|
{
|
||||||
@@ -21,13 +21,30 @@ class IgdbAuthService
|
|||||||
private function fetchNewAccessToken(): string
|
private function fetchNewAccessToken(): string
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$response = Http::post('https://id.twitch.tv/oauth2/token', [
|
// Log the config values to debug
|
||||||
'client_id' => config('services.igdb.client_id'),
|
Log::info('IGDB Auth Attempt', [
|
||||||
'client_secret' => config('services.igdb.client_secret'),
|
'client_id' => config('services.igdb.credentials.client_id'),
|
||||||
|
'client_secret' => config('services.igdb.credentials.client_secret')
|
||||||
|
]);
|
||||||
|
|
||||||
|
$clientId = config('igdb.credentials.client_id');
|
||||||
|
$clientSecret = config('igdb.credentials.client_secret');
|
||||||
|
|
||||||
|
if (empty($clientId) || empty($clientSecret)) {
|
||||||
|
throw new \Exception("IGDB credentials are not properly configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = Http::asForm()->post('https://id.twitch.tv/oauth2/token', [
|
||||||
|
'client_id' => $clientId,
|
||||||
|
'client_secret' => $clientSecret,
|
||||||
'grant_type' => 'client_credentials',
|
'grant_type' => 'client_credentials',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!$response->successful()) {
|
if (!$response->successful()) {
|
||||||
|
Log::error('IGDB Auth Response', [
|
||||||
|
'status' => $response->status(),
|
||||||
|
'body' => $response->body()
|
||||||
|
]);
|
||||||
throw new \Exception("Failed to get access token: " . $response->body());
|
throw new \Exception("Failed to get access token: " . $response->body());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,4 +57,4 @@ class IgdbAuthService
|
|||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-34
@@ -17,7 +17,7 @@ use MarcReichel\IGDBLaravel\Models\Platform as IgdbPlatform;
|
|||||||
|
|
||||||
class IgdbService
|
class IgdbService
|
||||||
{
|
{
|
||||||
public function get()
|
public function get(int $limit)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$existingIds = $this->getExistingIds();
|
$existingIds = $this->getExistingIds();
|
||||||
@@ -26,7 +26,7 @@ class IgdbService
|
|||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'summary',
|
'summary',
|
||||||
'cover.url',
|
'cover',
|
||||||
'slug',
|
'slug',
|
||||||
'genres',
|
'genres',
|
||||||
'platforms',
|
'platforms',
|
||||||
@@ -34,17 +34,21 @@ class IgdbService
|
|||||||
'rating',
|
'rating',
|
||||||
'rating_count'
|
'rating_count'
|
||||||
])
|
])
|
||||||
->where('rating_count', '>', 0)
|
// ->where('rating_count', '>', 0)
|
||||||
->where('category', '=', 0);
|
// ->where('category', '=', 0)
|
||||||
|
;
|
||||||
|
|
||||||
if (!empty($existingIds)) {
|
if (!empty($existingIds)) {
|
||||||
$queryBuilder = $queryBuilder->whereNotIn('id', array_map(function($id) {
|
// Convert array of quoted strings to array of integers
|
||||||
return (int)str_replace('"', '', $id);
|
// $existingIds = array_map(function($id) {
|
||||||
}, $existingIds));
|
// return (int)str_replace('"', '', $id);
|
||||||
|
// }, $existingIds);
|
||||||
|
$queryBuilder->whereNotIn('id', $existingIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
$a = $queryBuilder
|
$a = $queryBuilder
|
||||||
->limit(50)
|
->orderBy('first_release_date', 'asc')
|
||||||
|
->limit($limit)
|
||||||
->get();
|
->get();
|
||||||
}catch (\Throwable $e) {
|
}catch (\Throwable $e) {
|
||||||
dd(
|
dd(
|
||||||
@@ -68,12 +72,13 @@ class IgdbService
|
|||||||
|
|
||||||
/** @var Game $existingGame */
|
/** @var Game $existingGame */
|
||||||
foreach ($existingGames as $existingGame) {
|
foreach ($existingGames as $existingGame) {
|
||||||
$existingIds[] = (string)$existingGame->id;
|
$existingIds[] = (string)$existingGame->igdb_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return array_map(function($id) {
|
return $existingIds;
|
||||||
return '"'.$id.'"';
|
// return array_map(function($id) {
|
||||||
}, $existingIds);
|
// return '"'.$id.'"';
|
||||||
|
// }, $existingIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function searchGames(string $query, int $limit = 10, ?int $startTimestamp = null, ?int $endTimestamp = null): array
|
public function searchGames(string $query, int $limit = 10, ?int $startTimestamp = null, ?int $endTimestamp = null): array
|
||||||
@@ -240,13 +245,41 @@ class IgdbService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process genres
|
||||||
|
$genres = [];
|
||||||
|
if (isset($gameData->genres) && is_array($gameData->genres)) {
|
||||||
|
$genreData = IgdbGenre::select(['id', 'name', 'slug'])
|
||||||
|
->whereIn('id', $gameData->genres)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($genreData as $genreDetails) {
|
||||||
|
$genre = Genre::updateOrCreate(
|
||||||
|
['igdb_id' => $genreDetails->id],
|
||||||
|
[
|
||||||
|
'name' => $genreDetails->name,
|
||||||
|
'slug' => $genreDetails->slug,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$genres[] = $genre->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cover = Cover::where('game', '=', $gameData->id)->first();
|
||||||
|
|
||||||
|
$coverUrl = null;
|
||||||
|
|
||||||
|
if ($cover) {
|
||||||
|
$coverUrl = 'https:'.$cover->url;
|
||||||
|
$coverUrl = str_replace('t_thumb', 't_cover_big', $coverUrl);
|
||||||
|
}
|
||||||
|
|
||||||
// Create or update the game
|
// Create or update the game
|
||||||
$game = Game::updateOrCreate(
|
$game = Game::updateOrCreate(
|
||||||
['igdb_id' => $gameData->id],
|
['igdb_id' => $gameData->id],
|
||||||
[
|
[
|
||||||
'name' => $gameData->name,
|
'name' => $gameData->name,
|
||||||
'summary' => $gameData->summary ?? null,
|
'summary' => $gameData->summary ?? null,
|
||||||
'cover_url' => $gameData->cover?->url ?? null,
|
'cover_url' => $coverUrl,
|
||||||
'slug' => $gameData->slug,
|
'slug' => $gameData->slug,
|
||||||
'first_release_date' => $gameData->first_release_date ?? null,
|
'first_release_date' => $gameData->first_release_date ?? null,
|
||||||
'rating' => $gameData->rating ?? null,
|
'rating' => $gameData->rating ?? null,
|
||||||
@@ -254,12 +287,20 @@ class IgdbService
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Sync platforms and genres
|
||||||
|
if (!empty($platforms)) {
|
||||||
|
$game->platforms()->sync($platforms);
|
||||||
|
}
|
||||||
|
if (!empty($genres)) {
|
||||||
|
$game->genres()->sync($genres);
|
||||||
|
}
|
||||||
|
|
||||||
// Process artworks
|
// Process artworks
|
||||||
try {
|
try {
|
||||||
$artworks = \MarcReichel\IGDBLaravel\Models\Artwork::select([
|
$artworks = \MarcReichel\IGDBLaravel\Models\Artwork::select([
|
||||||
'id',
|
'id',
|
||||||
'url',
|
'url',
|
||||||
'width',
|
'width',
|
||||||
'height',
|
'height',
|
||||||
'image_id'
|
'image_id'
|
||||||
])
|
])
|
||||||
@@ -285,11 +326,6 @@ class IgdbService
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process platforms relationship
|
|
||||||
if (!empty($platforms)) {
|
|
||||||
$game->platforms()->sync($platforms);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process screenshots
|
// Process screenshots
|
||||||
try {
|
try {
|
||||||
$screenshots = \MarcReichel\IGDBLaravel\Models\Screenshot::select([
|
$screenshots = \MarcReichel\IGDBLaravel\Models\Screenshot::select([
|
||||||
@@ -325,7 +361,7 @@ class IgdbService
|
|||||||
try {
|
try {
|
||||||
$videos = \MarcReichel\IGDBLaravel\Models\GameVideo::select([
|
$videos = \MarcReichel\IGDBLaravel\Models\GameVideo::select([
|
||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'video_id',
|
'video_id',
|
||||||
'url',
|
'url',
|
||||||
'checksum'
|
'checksum'
|
||||||
@@ -365,25 +401,55 @@ class IgdbService
|
|||||||
throw new \InvalidArgumentException('Game data must be an instance of IgdbGame');
|
throw new \InvalidArgumentException('Game data must be an instance of IgdbGame');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch related games data using IGDB API
|
// Initialize relationships array
|
||||||
$relationships = [
|
$relationships = [];
|
||||||
'bundles' => IgdbGame::select(['id'])->where('id', 'in', $gameData->bundles ?? [])->get()->pluck('id')->toArray(),
|
|
||||||
'collections' => IgdbGame::select(['id'])->where('id', 'in', $gameData->collections ?? [])->get()->pluck('id')->toArray(),
|
// Define relationship mappings and check each one
|
||||||
'dlcs' => IgdbGame::select(['id'])->where('id', 'in', $gameData->dlcs ?? [])->get()->pluck('id')->toArray(),
|
$relationshipMappings = [
|
||||||
'expanded_games' => IgdbGame::select(['id'])->where('id', 'in', $gameData->expanded_games ?? [])->get()->pluck('id')->toArray(),
|
'bundles',
|
||||||
'franchises' => IgdbGame::select(['id'])->where('id', 'in', $gameData->franchises ?? [])->get()->pluck('id')->toArray(),
|
'collections',
|
||||||
'ports' => IgdbGame::select(['id'])->where('id', 'in', $gameData->ports ?? [])->get()->pluck('id')->toArray(),
|
'dlcs',
|
||||||
'remakes' => IgdbGame::select(['id'])->where('id', 'in', $gameData->remakes ?? [])->get()->pluck('id')->toArray(),
|
'expanded_games',
|
||||||
'similar_games' => IgdbGame::select(['id'])->where('id', 'in', $gameData->similar_games ?? [])->get()->pluck('id')->toArray(),
|
'franchises',
|
||||||
|
'ports',
|
||||||
|
'remakes',
|
||||||
|
'similar_games'
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($relationships as $igdbKey => $relationship) {
|
// Only query IGDB for relationships that have data
|
||||||
|
foreach ($relationshipMappings as $relationship) {
|
||||||
|
if (!empty($gameData->$relationship)) {
|
||||||
|
$relationships[$relationship] = IgdbGame::select(['id'])
|
||||||
|
->where('id', 'in', $gameData->$relationship)
|
||||||
|
->get()
|
||||||
|
->pluck('id')
|
||||||
|
->toArray();
|
||||||
|
} else {
|
||||||
|
$relationships[$relationship] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process and sync each relationship
|
||||||
|
foreach ($relationships as $igdbKey => $relatedIds) {
|
||||||
|
// Convert IGDB relationship key to Laravel relationship method
|
||||||
|
$relationshipMethod = str_replace('_', '', $igdbKey); // Remove underscores
|
||||||
|
|
||||||
$relatedGames = [];
|
$relatedGames = [];
|
||||||
if (isset($gameData->$igdbKey) && !empty($gameData->$igdbKey)) {
|
if (!empty($relatedIds)) {
|
||||||
$relatedIds = collect($gameData->$igdbKey)->pluck('id')->toArray();
|
|
||||||
$relatedGames = Game::whereIn('igdb_id', $relatedIds)->pluck('id')->toArray();
|
$relatedGames = Game::whereIn('igdb_id', $relatedIds)->pluck('id')->toArray();
|
||||||
}
|
}
|
||||||
$game->$relationship()->sync($relatedGames);
|
|
||||||
|
// Only sync if the relationship method exists
|
||||||
|
if (method_exists($game, $relationshipMethod)) {
|
||||||
|
Log::info("Syncing relationship '$relationshipMethod' for game {$game->id}");
|
||||||
|
|
||||||
|
$game->$relationshipMethod()->sync($relatedGames);
|
||||||
|
} else {
|
||||||
|
Log::warning("Relationship method '$relationshipMethod' does not exist on Game model", [
|
||||||
|
'game_id' => $game->id,
|
||||||
|
'igdb_key' => $igdbKey
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('ratings', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('game_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->integer('rating');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['user_id', 'game_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('ratings');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('reviews', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('game_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->text('content');
|
||||||
|
$table->boolean('recommended');
|
||||||
|
$table->integer('rating')->unsigned()->between(1, 10);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['user_id', 'game_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('reviews');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('game_lists', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->boolean('is_default')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('game_lists');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('game_game_list', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('game_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('game_list_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['game_id', 'game_list_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('game_game_list');
|
||||||
|
}
|
||||||
|
};
|
||||||
Generated
+234
-2695
File diff suppressed because it is too large
Load Diff
+7
-12
@@ -2,19 +2,14 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"dev": "vite",
|
||||||
"dev": "vite"
|
"build": "vite build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.2",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"axios": "^1.6.4",
|
||||||
"alpinejs": "^3.4.2",
|
"bootstrap": "^5.3.2",
|
||||||
"autoprefixer": "^10.4.2",
|
"laravel-vite-plugin": "^1.0.0",
|
||||||
"axios": "^1.8.2",
|
"vite": "^5.0.0"
|
||||||
"concurrently": "^9.0.1",
|
|
||||||
"laravel-vite-plugin": "^1.2.0",
|
|
||||||
"postcss": "^8.4.31",
|
|
||||||
"tailwindcss": "^3.1.0",
|
|
||||||
"vite": "^6.2.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
+167
-3
@@ -1,3 +1,167 @@
|
|||||||
@tailwind base;
|
@import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
:root {
|
||||||
|
--primary: #7F00FF;
|
||||||
|
--accent: #00C9FF;
|
||||||
|
--background: #121212;
|
||||||
|
--text: #EAEAEA;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-text {
|
||||||
|
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-border {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background: linear-gradient(var(--background), var(--background)) padding-box,
|
||||||
|
linear-gradient(90deg, var(--primary), var(--accent)) border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gradient {
|
||||||
|
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gradient:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-gradient {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background: linear-gradient(var(--background), var(--background)) padding-box,
|
||||||
|
linear-gradient(90deg, var(--primary), var(--accent)) border-box;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-gradient:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styles */
|
||||||
|
.form-control {
|
||||||
|
background-color: var(--background) !important;
|
||||||
|
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
color: var(--text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
background-color: var(--background) !important;
|
||||||
|
border-color: var(--primary) !important;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(127, 0, 255, 0.25) !important;
|
||||||
|
color: var(--text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input {
|
||||||
|
background-color: var(--background) !important;
|
||||||
|
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input:checked {
|
||||||
|
background-color: var(--primary) !important;
|
||||||
|
border-color: var(--primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input:focus {
|
||||||
|
border-color: var(--primary) !important;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(127, 0, 255, 0.25) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert Styles */
|
||||||
|
.alert {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: rgba(40, 167, 69, 0.1);
|
||||||
|
border-color: rgba(40, 167, 69, 0.2);
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background-color: rgba(220, 53, 69, 0.1);
|
||||||
|
border-color: rgba(220, 53, 69, 0.2);
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Pagination Styles */
|
||||||
|
.pagination {
|
||||||
|
--bs-pagination-color: var(--text);
|
||||||
|
--bs-pagination-bg: var(--background);
|
||||||
|
--bs-pagination-border-color: transparent;
|
||||||
|
--bs-pagination-hover-color: var(--text);
|
||||||
|
--bs-pagination-hover-bg: rgba(255, 255, 255, 0.1);
|
||||||
|
--bs-pagination-hover-border-color: transparent;
|
||||||
|
--bs-pagination-focus-color: var(--text);
|
||||||
|
--bs-pagination-focus-bg: rgba(255, 255, 255, 0.1);
|
||||||
|
--bs-pagination-focus-box-shadow: none;
|
||||||
|
--bs-pagination-active-color: white;
|
||||||
|
--bs-pagination-active-bg: linear-gradient(90deg, var(--primary), var(--accent));
|
||||||
|
--bs-pagination-active-border-color: transparent;
|
||||||
|
--bs-pagination-disabled-color: #6c757d;
|
||||||
|
--bs-pagination-disabled-bg: var(--background);
|
||||||
|
--bs-pagination-disabled-border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.active .page-link {
|
||||||
|
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.disabled .page-link {
|
||||||
|
background-color: var(--background);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom dropdown styles */
|
||||||
|
.dropdown-menu {
|
||||||
|
background-color: var(--background);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom badge styles */
|
||||||
|
.badge {
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.bg-primary {
|
||||||
|
background: linear-gradient(90deg, var(--primary), var(--accent)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.bg-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|||||||
+2
-5
@@ -1,7 +1,4 @@
|
|||||||
|
import 'bootstrap';
|
||||||
import './bootstrap';
|
import './bootstrap';
|
||||||
|
|
||||||
import Alpine from 'alpinejs';
|
// Custom JavaScript can be added here
|
||||||
|
|
||||||
window.Alpine = Alpine;
|
|
||||||
|
|
||||||
Alpine.start();
|
|
||||||
|
|||||||
@@ -1,47 +1,59 @@
|
|||||||
<x-guest-layout>
|
@extends('layouts.auth')
|
||||||
<!-- Session Status -->
|
|
||||||
<x-auth-session-status class="mb-4" :status="session('status')" />
|
|
||||||
|
|
||||||
|
@section('title', 'Login')
|
||||||
|
@section('subtitle', 'Welcome back! Please enter your details.')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
<form method="POST" action="{{ route('login') }}">
|
<form method="POST" action="{{ route('login') }}">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<!-- Email Address -->
|
<div class="mb-3">
|
||||||
<div>
|
<label for="email" class="form-label text-gray-400">Email</label>
|
||||||
<x-input-label for="email" :value="__('Email')" />
|
<input id="email" type="email" class="form-control bg-dark text-white border-gray-800 @error('email') is-invalid @enderror"
|
||||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
|
name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>
|
||||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
@error('email')
|
||||||
|
<span class="invalid-feedback" role="alert">
|
||||||
|
<strong>{{ $message }}</strong>
|
||||||
|
</span>
|
||||||
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Password -->
|
<div class="mb-3">
|
||||||
<div class="mt-4">
|
<label for="password" class="form-label text-gray-400">Password</label>
|
||||||
<x-input-label for="password" :value="__('Password')" />
|
<input id="password" type="password" class="form-control bg-dark text-white border-gray-800 @error('password') is-invalid @enderror"
|
||||||
|
name="password" required autocomplete="current-password">
|
||||||
<x-text-input id="password" class="block mt-1 w-full"
|
@error('password')
|
||||||
type="password"
|
<span class="invalid-feedback" role="alert">
|
||||||
name="password"
|
<strong>{{ $message }}</strong>
|
||||||
required autocomplete="current-password" />
|
</span>
|
||||||
|
@enderror
|
||||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Remember Me -->
|
<div class="mb-3 form-check">
|
||||||
<div class="block mt-4">
|
<input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
|
||||||
<label for="remember_me" class="inline-flex items-center">
|
<label class="form-check-label text-gray-400" for="remember">
|
||||||
<input id="remember_me" type="checkbox" class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" name="remember">
|
Remember me
|
||||||
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Remember me') }}</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-end mt-4">
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-gradient">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
@if (Route::has('password.request'))
|
@if (Route::has('password.request'))
|
||||||
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('password.request') }}">
|
<a class="text-decoration-none text-gray-400" href="{{ route('password.request') }}">
|
||||||
{{ __('Forgot your password?') }}
|
Forgot your password?
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<x-primary-button class="ms-3">
|
|
||||||
{{ __('Log in') }}
|
|
||||||
</x-primary-button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</x-guest-layout>
|
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<p class="text-gray-400 mb-0">Don't have an account?
|
||||||
|
<a href="{{ route('register') }}" class="text-decoration-none gradient-text">Sign up</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|||||||
@@ -1,52 +1,61 @@
|
|||||||
<x-guest-layout>
|
@extends('layouts.auth')
|
||||||
|
|
||||||
|
@section('title', 'Register')
|
||||||
|
@section('subtitle', 'Create your account to start tracking your games.')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
<form method="POST" action="{{ route('register') }}">
|
<form method="POST" action="{{ route('register') }}">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<!-- Name -->
|
<div class="mb-3">
|
||||||
<div>
|
<label for="name" class="form-label text-gray-400">Name</label>
|
||||||
<x-input-label for="name" :value="__('Name')" />
|
<input id="name" type="text" class="form-control bg-dark text-white border-gray-800 @error('name') is-invalid @enderror"
|
||||||
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
|
name="name" value="{{ old('name') }}" required autocomplete="name" autofocus>
|
||||||
<x-input-error :messages="$errors->get('name')" class="mt-2" />
|
@error('name')
|
||||||
|
<span class="invalid-feedback" role="alert">
|
||||||
|
<strong>{{ $message }}</strong>
|
||||||
|
</span>
|
||||||
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Email Address -->
|
<div class="mb-3">
|
||||||
<div class="mt-4">
|
<label for="email" class="form-label text-gray-400">Email</label>
|
||||||
<x-input-label for="email" :value="__('Email')" />
|
<input id="email" type="email" class="form-control bg-dark text-white border-gray-800 @error('email') is-invalid @enderror"
|
||||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
|
name="email" value="{{ old('email') }}" required autocomplete="email">
|
||||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
@error('email')
|
||||||
|
<span class="invalid-feedback" role="alert">
|
||||||
|
<strong>{{ $message }}</strong>
|
||||||
|
</span>
|
||||||
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Password -->
|
<div class="mb-3">
|
||||||
<div class="mt-4">
|
<label for="password" class="form-label text-gray-400">Password</label>
|
||||||
<x-input-label for="password" :value="__('Password')" />
|
<input id="password" type="password" class="form-control bg-dark text-white border-gray-800 @error('password') is-invalid @enderror"
|
||||||
|
name="password" required autocomplete="new-password">
|
||||||
<x-text-input id="password" class="block mt-1 w-full"
|
@error('password')
|
||||||
type="password"
|
<span class="invalid-feedback" role="alert">
|
||||||
name="password"
|
<strong>{{ $message }}</strong>
|
||||||
required autocomplete="new-password" />
|
</span>
|
||||||
|
@enderror
|
||||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Password -->
|
<div class="mb-3">
|
||||||
<div class="mt-4">
|
<label for="password-confirm" class="form-label text-gray-400">Confirm Password</label>
|
||||||
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
<input id="password-confirm" type="password" class="form-control bg-dark text-white border-gray-800"
|
||||||
|
name="password_confirmation" required autocomplete="new-password">
|
||||||
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
|
||||||
type="password"
|
|
||||||
name="password_confirmation" required autocomplete="new-password" />
|
|
||||||
|
|
||||||
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-end mt-4">
|
<div class="d-grid gap-2">
|
||||||
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('login') }}">
|
<button type="submit" class="btn btn-gradient">
|
||||||
{{ __('Already registered?') }}
|
Register
|
||||||
</a>
|
</button>
|
||||||
|
|
||||||
<x-primary-button class="ms-4">
|
|
||||||
{{ __('Register') }}
|
|
||||||
</x-primary-button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</x-guest-layout>
|
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<p class="text-gray-400 mb-0">Already have an account?
|
||||||
|
<a href="{{ route('login') }}" class="text-decoration-none gradient-text">Login</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Games - ClearGame')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h2 fw-bold gradient-text">All Games</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<form action="{{ route('games.index') }}" method="GET" class="d-flex me-2">
|
||||||
|
<input type="text"
|
||||||
|
name="search"
|
||||||
|
class="form-control bg-dark text-white border-0"
|
||||||
|
placeholder="Search games..."
|
||||||
|
value="{{ request('search') }}"
|
||||||
|
style="min-width: 250px;">
|
||||||
|
<button type="submit" class="btn btn-gradient ms-2">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-gradient dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-sort me-1"></i> Sort by
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="{{ route('games.index', array_merge(request()->query(), ['sort' => 'first_release_date', 'direction' => 'desc'])) }}">
|
||||||
|
<i class="fas fa-calendar me-2"></i> Release Date
|
||||||
|
</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ route('games.index', array_merge(request()->query(), ['sort' => 'rating', 'direction' => 'desc'])) }}">
|
||||||
|
<i class="fas fa-star me-2"></i> Rating
|
||||||
|
</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ route('games.index', array_merge(request()->query(), ['sort' => 'rating_count', 'direction' => 'desc'])) }}">
|
||||||
|
<i class="fas fa-fire me-2"></i> Popularity
|
||||||
|
</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-gradient dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-filter me-1"></i> Filter
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu p-3" style="min-width: 300px;">
|
||||||
|
<li class="mb-2">
|
||||||
|
<label class="form-label text-white mb-1"><i class="fas fa-tv me-1"></i> Platform</label>
|
||||||
|
<select name="platform" class="form-select bg-dark text-white border-0" onchange="this.form.submit()">
|
||||||
|
<option value="">All Platforms</option>
|
||||||
|
@foreach($platforms as $platform)
|
||||||
|
<option value="{{ $platform->slug }}" {{ request('platform') == $platform->slug ? 'selected' : '' }}>
|
||||||
|
{{ $platform->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<label class="form-label text-white mb-1"><i class="fas fa-tags me-1"></i> Genre</label>
|
||||||
|
<select name="genre" class="form-select bg-dark text-white border-0" onchange="this.form.submit()">
|
||||||
|
<option value="">All Genres</option>
|
||||||
|
@foreach($genres as $genre)
|
||||||
|
<option value="{{ $genre->slug }}" {{ request('genre') == $genre->slug ? 'selected' : '' }}>
|
||||||
|
{{ $genre->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<label class="form-label text-white mb-1"><i class="fas fa-calendar-alt me-1"></i> Release Year</label>
|
||||||
|
<select name="year" class="form-select bg-dark text-white border-0" onchange="this.form.submit()">
|
||||||
|
<option value="">All Years</option>
|
||||||
|
@foreach($years as $year)
|
||||||
|
<option value="{{ $year }}" {{ request('year') == $year ? 'selected' : '' }}>
|
||||||
|
{{ $year }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<label class="form-label text-white mb-1"><i class="fas fa-star me-1"></i> Rating Range</label>
|
||||||
|
<select name="rating_range" class="form-select bg-dark text-white border-0" onchange="this.form.submit()">
|
||||||
|
<option value="">All Ratings</option>
|
||||||
|
<option value="90-100" {{ request('rating_range') == '90-100' ? 'selected' : '' }}>90+</option>
|
||||||
|
<option value="80-89" {{ request('rating_range') == '80-89' ? 'selected' : '' }}>80-89</option>
|
||||||
|
<option value="70-79" {{ request('rating_range') == '70-79' ? 'selected' : '' }}>70-79</option>
|
||||||
|
<option value="60-69" {{ request('rating_range') == '60-69' ? 'selected' : '' }}>60-69</option>
|
||||||
|
<option value="0-59" {{ request('rating_range') == '0-59' ? 'selected' : '' }}>Below 60</option>
|
||||||
|
</select>
|
||||||
|
</li>
|
||||||
|
<li class="mt-3">
|
||||||
|
<a href="{{ route('games.index') }}" class="btn btn-outline-gradient btn-sm w-100">
|
||||||
|
<i class="fas fa-times me-1"></i> Clear Filters
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(request()->hasAny(['platform', 'genre', 'year', 'rating_range']))
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
@if(request('platform'))
|
||||||
|
<span class="badge bg-primary">
|
||||||
|
<i class="fas fa-tv me-1"></i> {{ $platforms->firstWhere('slug', request('platform'))->name }}
|
||||||
|
<a href="{{ route('games.index', array_merge(request()->except('platform'), ['page' => 1])) }}" class="text-white ms-2">×</a>
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if(request('genre'))
|
||||||
|
<span class="badge bg-primary">
|
||||||
|
<i class="fas fa-tags me-1"></i> {{ $genres->firstWhere('slug', request('genre'))->name }}
|
||||||
|
<a href="{{ route('games.index', array_merge(request()->except('genre'), ['page' => 1])) }}" class="text-white ms-2">×</a>
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if(request('year'))
|
||||||
|
<span class="badge bg-primary">
|
||||||
|
<i class="fas fa-calendar-alt me-1"></i> {{ request('year') }}
|
||||||
|
<a href="{{ route('games.index', array_merge(request()->except('year'), ['page' => 1])) }}" class="text-white ms-2">×</a>
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if(request('rating_range'))
|
||||||
|
<span class="badge bg-primary">
|
||||||
|
<i class="fas fa-star me-1"></i> {{ request('rating_range') }}
|
||||||
|
<a href="{{ route('games.index', array_merge(request()->except('rating_range'), ['page' => 1])) }}" class="text-white ms-2">×</a>
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-4">
|
||||||
|
@foreach($games as $game)
|
||||||
|
<div class="col">
|
||||||
|
<div class="card gradient-border h-100">
|
||||||
|
<img src="{{ $game->cover_url ?? 'https://via.placeholder.com/300x400' }}"
|
||||||
|
class="card-img-top"
|
||||||
|
alt="{{ $game->name }}"
|
||||||
|
style="height: 300px; object-fit: cover;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ $game->name }}</h5>
|
||||||
|
<p class="card-text text-gray-400 small">
|
||||||
|
{{ Str::limit($game->summary, 100) }}
|
||||||
|
</p>
|
||||||
|
<div class="d-flex gap-2 mb-3">
|
||||||
|
@if($game->rating)
|
||||||
|
<span class="badge bg-primary"><i class="fas fa-star me-1"></i> {{ number_format($game->rating, 1) }}</span>
|
||||||
|
@endif
|
||||||
|
@if($game->first_release_date)
|
||||||
|
<span class="badge bg-secondary">
|
||||||
|
<i class="fas fa-calendar-alt me-1"></i> {{ \Carbon\Carbon::parse($game->first_release_date)->format('Y') }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('games.show', $game->slug) }}" class="btn btn-gradient w-100">
|
||||||
|
<i class="fas fa-info-circle me-1"></i> View Details
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center mt-5">
|
||||||
|
{{ $games->links() }}
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', $game->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card bg-dark text-white mb-4">
|
||||||
|
<img src="{{ $game->cover_url }}" class="card-img-top" alt="{{ $game->name }}">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h5 class="card-title mb-0">{{ $game->name }}</h5>
|
||||||
|
<p class="text-muted mb-0">{{ $game->first_release_date->format('Y') }}</p>
|
||||||
|
</div>
|
||||||
|
@auth
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="addToListDropdown"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fas fa-plus me-2"></i>Add to List
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="addToListDropdown">
|
||||||
|
@foreach(Auth::user()->lists as $list)
|
||||||
|
<li>
|
||||||
|
<form action="{{ route('lists.addGame', $list) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="game_id" value="{{ $game->id }}">
|
||||||
|
<button type="submit" class="dropdown-item">
|
||||||
|
<i class="fas fa-list me-2"></i>{{ $list->name }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ route('lists.create') }}">
|
||||||
|
<i class="fas fa-plus me-2"></i>Create New List
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-dark text-white mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6 class="text-muted">Platforms</h6>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
@foreach($game->platforms()->get() as $platform)
|
||||||
|
<span class="badge bg-secondary">{{ $platform->name }}</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6 class="text-muted">Genres</h6>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
@foreach($game->genres()->get() as $genre)
|
||||||
|
<span class="badge bg-primary">{{ $genre->name }}</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($game->rating)
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6 class="text-muted">Rating</h6>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="rating-stars d-flex gap-1">
|
||||||
|
@for($i = 1; $i <= 10; $i++)
|
||||||
|
<i class="fas fa-star {{ $i <= $game->rating ? 'text-warning' : 'text-secondary' }}"></i>
|
||||||
|
@endfor
|
||||||
|
</div>
|
||||||
|
<span class="ms-2">({{ $game->rating_count }} ratings)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card bg-dark text-white mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">About</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text">{{ $game->summary }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($game->screenshots()->count())
|
||||||
|
<div class="card bg-dark text-white mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Screenshots</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
@foreach($game->screenshots()->get() as $screenshot)
|
||||||
|
<div class="col-md-6">
|
||||||
|
<img src="{{ $screenshot->url }}" class="img-fluid rounded" alt="Game Screenshot">
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($game->videos()->count())
|
||||||
|
<div class="card bg-dark text-white mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Videos</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
@foreach($game->videos()->get() as $video)
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="ratio ratio-16x9">
|
||||||
|
<iframe src="https://www.youtube.com/embed/{{ $video->video_id }}"
|
||||||
|
title="{{ $video->name }}" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">Reviews</h5>
|
||||||
|
@auth
|
||||||
|
<a href="{{ route('games.reviews.create', $game) }}" class="btn btn-gradient btn-sm">
|
||||||
|
<i class="fas fa-plus me-2"></i>Write a Review
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@if($game->reviews->isEmpty())
|
||||||
|
<p class="text-muted mb-0">No reviews yet. Be the first to review this game!</p>
|
||||||
|
@else
|
||||||
|
@foreach($game->reviews as $review)
|
||||||
|
<div class="mb-4 pb-4 border-bottom border-secondary">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-0">{{ $review->user->name }}</h6>
|
||||||
|
<small class="text-muted">{{ $review->created_at->diffForHumans() }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="rating-stars d-flex gap-1 me-2">
|
||||||
|
@for($i = 1; $i <= 10; $i++)
|
||||||
|
<i class="fas fa-star {{ $i <= $review->rating ? 'text-warning' : 'text-secondary' }}"></i>
|
||||||
|
@endfor
|
||||||
|
</div>
|
||||||
|
@if($review->recommended)
|
||||||
|
<span class="badge bg-success">Recommended</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-danger">Not Recommended</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mb-0">{{ $review->content }}</p>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ClearGame - Track Your Games. Level Up Your Backlog.</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: #7F00FF;
|
||||||
|
--accent: #00C9FF;
|
||||||
|
--background: #121212;
|
||||||
|
--text: #EAEAEA;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.gradient-text {
|
||||||
|
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
.gradient-border {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background: linear-gradient(var(--background), var(--background)) padding-box,
|
||||||
|
linear-gradient(90deg, var(--primary), var(--accent)) border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand text-2xl fw-bold gradient-text" href="#">ClearGame</a>
|
||||||
|
<div class="d-flex">
|
||||||
|
<a href="#" class="text-gray-300 text-decoration-none me-3">Login</a>
|
||||||
|
<a href="#" class="btn btn-gradient">Sign Up</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<section class="text-center py-5">
|
||||||
|
<h1 class="display-4 fw-bold mb-4">
|
||||||
|
<span class="gradient-text">Track Your Games.</span><br>
|
||||||
|
Level Up Your Backlog.
|
||||||
|
</h1>
|
||||||
|
<p class="lead text-gray-400 mb-5 mx-auto" style="max-width: 600px;">
|
||||||
|
Organize your gaming journey, discover new titles, and connect with fellow gamers.
|
||||||
|
Your ultimate companion for managing and completing your game collection.
|
||||||
|
</p>
|
||||||
|
<div class="d-flex justify-content-center gap-3">
|
||||||
|
<a href="#" class="btn btn-gradient btn-lg">Get Started</a>
|
||||||
|
<a href="#" class="btn btn-outline-gradient btn-lg">Learn More</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="py-5">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="gradient-border p-4 rounded-3 h-100">
|
||||||
|
<div class="display-4 mb-3 gradient-text">🎮</div>
|
||||||
|
<h3 class="h4 fw-semibold mb-2">Track Your Progress</h3>
|
||||||
|
<p class="text-gray-400">Keep tabs on your gaming journey and celebrate your achievements.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="gradient-border p-4 rounded-3 h-100">
|
||||||
|
<div class="display-4 mb-3 gradient-text">📊</div>
|
||||||
|
<h3 class="h4 fw-semibold mb-2">Discover New Games</h3>
|
||||||
|
<p class="text-gray-400">Find your next favorite game with personalized recommendations.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="gradient-border p-4 rounded-3 h-100">
|
||||||
|
<div class="display-4 mb-3 gradient-text">👥</div>
|
||||||
|
<h3 class="h4 fw-semibold mb-2">Connect with Gamers</h3>
|
||||||
|
<p class="text-gray-400">Share your experiences and connect with like-minded gamers.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="text-center py-5">
|
||||||
|
<h2 class="display-5 fw-bold mb-4">Ready to Level Up Your Gaming Experience?</h2>
|
||||||
|
<a href="#" class="btn btn-gradient btn-lg">Join ClearGame Today</a>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="border-top border-gray-800 py-4">
|
||||||
|
<div class="container text-center text-gray-400">
|
||||||
|
<p class="mb-0">© 2024 ClearGame. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -5,32 +5,149 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
<title>@yield('title', config('app.name', 'Laravel'))</title>
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--gradient-start: #4f46e5;
|
||||||
|
--gradient-end: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient {
|
||||||
|
background: linear-gradient(to right, var(--gradient-start), var(--gradient-end));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gradient {
|
||||||
|
background: linear-gradient(to right, var(--gradient-start), var(--gradient-end));
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gradient:hover {
|
||||||
|
background: linear-gradient(to right, var(--gradient-end), var(--gradient-start));
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-gradient {
|
||||||
|
border: 1px solid var(--gradient-start);
|
||||||
|
color: var(--gradient-start);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-gradient:hover {
|
||||||
|
background: linear-gradient(to right, var(--gradient-start), var(--gradient-end));
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-border {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: linear-gradient(#0f172a, #0f172a) padding-box,
|
||||||
|
linear-gradient(to right, var(--gradient-start), var(--gradient-end)) border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: rgba(15, 23, 42, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
background-color: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background-color: #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
border-top: 1px solid #334155;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="font-sans antialiased">
|
<body class="font-sans antialiased">
|
||||||
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||||
@include('layouts.navigation')
|
<nav class="navbar navbar-expand-lg">
|
||||||
|
<div class="container">
|
||||||
<!-- Page Heading -->
|
<a class="navbar-brand text-2xl fw-bold gradient-text" href="/">ClearGame</a>
|
||||||
@isset($header)
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
<header class="bg-white dark:bg-gray-800 shadow">
|
<span class="navbar-toggler-icon"></span>
|
||||||
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
</button>
|
||||||
{{ $header }}
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {{ request()->routeIs('games.index') ? 'active' : '' }}" href="{{ route('games.index') }}">
|
||||||
|
<i class="fas fa-gamepad me-1"></i> Games
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="d-flex">
|
||||||
|
@auth
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-gradient dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-user me-1"></i> {{ Auth::user()->name }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="dropdown-item">
|
||||||
|
<i class="fas fa-sign-out-alt me-1"></i> Logout
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="text-gray-300 text-decoration-none me-3">
|
||||||
|
<i class="fas fa-sign-in-alt me-1"></i> Login
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('register') }}" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-user-plus me-1"></i> Sign Up
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
@endisset
|
</nav>
|
||||||
|
|
||||||
<!-- Page Content -->
|
<!-- Page Content -->
|
||||||
<main>
|
<main class="container py-5">
|
||||||
{{ $slot }}
|
@yield('content')
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<footer class="border-top border-gray-800 py-4 mt-5">
|
||||||
|
<div class="container text-center text-gray-400">
|
||||||
|
<p class="mb-0">© 2024 ClearGame. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap Bundle with Popper -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>@yield('title') - ClearGame</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand text-2xl fw-bold gradient-text" href="/">ClearGame</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8 col-lg-6">
|
||||||
|
<div class="gradient-border rounded-3 p-4">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h1 class="h3 fw-bold gradient-text">@yield('title')</h1>
|
||||||
|
<p class="text-gray-400">@yield('subtitle')</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (session('status'))
|
||||||
|
<div class="alert alert-success" role="alert">
|
||||||
|
{{ session('status') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<ul class="mb-0">
|
||||||
|
@foreach ($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@yield('content')
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="border-top border-gray-800 py-4 mt-5">
|
||||||
|
<div class="container text-center text-gray-400">
|
||||||
|
<p class="mb-0">© 2024 ClearGame. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Create New List')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">Create New List</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('lists.store') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">List Name</label>
|
||||||
|
<input type="text" class="form-control bg-dark text-white @error('name') is-invalid @enderror"
|
||||||
|
id="name" name="name" value="{{ old('name') }}" required>
|
||||||
|
@error('name')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="description" class="form-label">Description (Optional)</label>
|
||||||
|
<textarea class="form-control bg-dark text-white @error('description') is-invalid @enderror"
|
||||||
|
id="description" name="description" rows="3">{{ old('description') }}</textarea>
|
||||||
|
@error('description')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<a href="{{ route('lists.index') }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Back to Lists
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-save me-2"></i>Create List
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Edit List')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">Edit List</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('lists.update', $list) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">List Name</label>
|
||||||
|
<input type="text" class="form-control bg-dark text-white @error('name') is-invalid @enderror"
|
||||||
|
id="name" name="name" value="{{ old('name', $list->name) }}" required>
|
||||||
|
@error('name')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="description" class="form-label">Description (Optional)</label>
|
||||||
|
<textarea class="form-control bg-dark text-white @error('description') is-invalid @enderror"
|
||||||
|
id="description" name="description" rows="3">{{ old('description', $list->description) }}</textarea>
|
||||||
|
@error('description')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<a href="{{ route('lists.show', $list) }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Back to List
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-save me-2"></i>Update List
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'My Lists')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="text-gradient">My Lists</h1>
|
||||||
|
<a href="{{ route('lists.create') }}" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-plus me-2"></i>Create New List
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
@foreach($lists as $list)
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card bg-dark text-white h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<h5 class="card-title mb-0">{{ $list->name }}</h5>
|
||||||
|
<span class="badge bg-secondary">{{ $list->games_count }} games</span>
|
||||||
|
</div>
|
||||||
|
@if($list->description)
|
||||||
|
<p class="card-text text-muted">{{ $list->description }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-dark border-top border-secondary">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<a href="{{ route('lists.show', $list) }}" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="fas fa-eye me-1"></i> View
|
||||||
|
</a>
|
||||||
|
@if(!$list->is_default)
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{{ route('lists.edit', $list) }}" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="fas fa-edit me-1"></i> Edit
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('lists.destroy', $list) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-outline-danger btn-sm"
|
||||||
|
onclick="return confirm('Are you sure you want to delete this list?')">
|
||||||
|
<i class="fas fa-trash me-1"></i> Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', $list->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-gradient mb-1">{{ $list->name }}</h1>
|
||||||
|
@if($list->description)
|
||||||
|
<p class="text-muted mb-0">{{ $list->description }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
@if(!$list->is_default)
|
||||||
|
<a href="{{ route('lists.edit', $list) }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-edit me-2"></i>Edit List
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
<a href="{{ route('lists.index') }}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-list me-2"></i>All Lists
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($games->isEmpty())
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-gamepad fa-3x text-muted mb-3"></i>
|
||||||
|
<h3 class="text-muted">No games in this list yet</h3>
|
||||||
|
<p class="text-muted">Start adding games to your list!</p>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="row g-4">
|
||||||
|
@foreach($games as $game)
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card bg-dark text-white h-100">
|
||||||
|
<img src="{{ $game->cover_url }}" class="card-img-top" alt="{{ $game->name }}"
|
||||||
|
style="height: 200px; object-fit: cover;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ $game->name }}</h5>
|
||||||
|
<div class="d-flex gap-2 mb-2">
|
||||||
|
@foreach($game->platforms as $platform)
|
||||||
|
<span class="badge bg-secondary">{{ $platform->name }}</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
@foreach($game->genres as $genre)
|
||||||
|
<span class="badge bg-primary">{{ $genre->name }}</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-dark border-top border-secondary">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<a href="{{ route('games.show', $game) }}" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="fas fa-eye me-1"></i> View Game
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('lists.removeGame', [$list, $game]) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||||
|
<i class="fas fa-times me-1"></i> Remove
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center mt-4">
|
||||||
|
{{ $games->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="content" class="form-label">Review</label>
|
||||||
|
<textarea name="content" id="content" rows="5"
|
||||||
|
class="form-control bg-dark text-white @error('content') is-invalid @enderror"
|
||||||
|
required>{{ old('content', $review->content ?? '') }}</textarea>
|
||||||
|
@error('content')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">Rating</label>
|
||||||
|
<div class="rating-container">
|
||||||
|
<div class="rating-stars d-flex gap-1">
|
||||||
|
@for($i = 1; $i <= 10; $i++)
|
||||||
|
<div class="rating-item" data-rating="{{ $i }}">
|
||||||
|
<input type="radio" name="rating" id="rating{{ $i }}" value="{{ $i }}"
|
||||||
|
class="rating-input" {{ old('rating', $review->rating ?? '') == $i ? 'checked' : '' }}>
|
||||||
|
<label for="rating{{ $i }}" class="rating-star">
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
</label>
|
||||||
|
<span class="rating-number">{{ $i }}</span>
|
||||||
|
</div>
|
||||||
|
@endfor
|
||||||
|
</div>
|
||||||
|
<div class="rating-value mt-2 text-muted">
|
||||||
|
<span class="current-rating">{{ old('rating', $review->rating ?? '0') }}</span>/10
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@error('rating')
|
||||||
|
<div class="invalid-feedback d-block">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">Recommendation</label>
|
||||||
|
<div class="recommendation-buttons d-flex gap-3">
|
||||||
|
<input type="radio" name="recommended" id="recommended1" value="1" class="recommendation-input"
|
||||||
|
{{ old('recommended', $review->recommended ?? '') == 1 ? 'checked' : '' }}>
|
||||||
|
<label for="recommended1" class="recommendation-button btn btn-outline-success">
|
||||||
|
<i class="fas fa-thumbs-up me-2"></i> Recommended
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input type="radio" name="recommended" id="recommended0" value="0" class="recommendation-input"
|
||||||
|
{{ old('recommended', $review->recommended ?? '') == 0 ? 'checked' : '' }}>
|
||||||
|
<label for="recommended0" class="recommendation-button btn btn-outline-danger">
|
||||||
|
<i class="fas fa-thumbs-down me-2"></i> Not Recommended
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
@error('recommended')
|
||||||
|
<div class="invalid-feedback d-block">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-paper-plane me-2"></i> {{ isset($review) ? 'Update Review' : 'Post Review' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@push('styles')
|
||||||
|
<style>
|
||||||
|
.rating-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-stars {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-star {
|
||||||
|
color: #6c757d;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-number {
|
||||||
|
position: absolute;
|
||||||
|
top: -20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #6c757d;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-item:hover .rating-number {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-item:hover .rating-star,
|
||||||
|
.rating-item:hover ~ .rating-item .rating-star,
|
||||||
|
.rating-input:checked ~ .rating-item .rating-star {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-input:checked + .rating-star {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-value {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommendation-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommendation-button {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommendation-button:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommendation-input:checked + .recommendation-button {
|
||||||
|
background-color: var(--bs-btn-bg);
|
||||||
|
color: var(--bs-btn-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#recommended1:checked + .recommendation-button {
|
||||||
|
background-color: var(--bs-success);
|
||||||
|
border-color: var(--bs-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
#recommended0:checked + .recommendation-button {
|
||||||
|
background-color: var(--bs-danger);
|
||||||
|
border-color: var(--bs-danger);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const ratingContainer = document.querySelector('.rating-stars');
|
||||||
|
const currentRating = document.querySelector('.current-rating');
|
||||||
|
const ratingInputs = document.querySelectorAll('.rating-input');
|
||||||
|
|
||||||
|
ratingContainer.addEventListener('mouseover', function(e) {
|
||||||
|
const ratingItem = e.target.closest('.rating-item');
|
||||||
|
if (!ratingItem) return;
|
||||||
|
|
||||||
|
const rating = ratingItem.dataset.rating;
|
||||||
|
currentRating.textContent = rating;
|
||||||
|
|
||||||
|
// Highlight stars up to the hovered rating
|
||||||
|
const stars = document.querySelectorAll('.rating-star');
|
||||||
|
stars.forEach((star, index) => {
|
||||||
|
if (index < rating) {
|
||||||
|
star.style.color = '#ffc107';
|
||||||
|
} else {
|
||||||
|
star.style.color = '#6c757d';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ratingContainer.addEventListener('mouseout', function() {
|
||||||
|
const checkedInput = document.querySelector('.rating-input:checked');
|
||||||
|
if (checkedInput) {
|
||||||
|
currentRating.textContent = checkedInput.value;
|
||||||
|
// Restore the checked state colors
|
||||||
|
const stars = document.querySelectorAll('.rating-star');
|
||||||
|
stars.forEach((star, index) => {
|
||||||
|
if (index < checkedInput.value) {
|
||||||
|
star.style.color = '#ffc107';
|
||||||
|
} else {
|
||||||
|
star.style.color = '#6c757d';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
currentRating.textContent = '0';
|
||||||
|
// Reset all stars to default color
|
||||||
|
const stars = document.querySelectorAll('.rating-star');
|
||||||
|
stars.forEach(star => {
|
||||||
|
star.style.color = '#6c757d';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update current rating when a star is clicked
|
||||||
|
ratingInputs.forEach(input => {
|
||||||
|
input.addEventListener('change', function() {
|
||||||
|
currentRating.textContent = this.value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<div class="gradient-border rounded-3 p-4 mb-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="h5 mb-1">{{ $review->user->name }}</h3>
|
||||||
|
<div class="d-flex gap-2 mb-2">
|
||||||
|
<span class="badge {{ $review->recommended ? 'bg-success' : 'bg-danger' }}">
|
||||||
|
{{ $review->recommended ? 'Recommended' : 'Not Recommended' }}
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-primary">{{ $review->rating }}/10</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="text-gray-400">{{ $review->created_at->diffForHumans() }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-gray-400 mb-0">{{ $review->content }}</p>
|
||||||
|
|
||||||
|
@can('update', $review)
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{{ route('reviews.edit', $review->id) }}" class="btn btn-sm btn-outline-gradient me-2">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('reviews.destroy', $review->id) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-gradient" onclick="return confirm('Are you sure?')">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Write a Review - ' . $game->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="gradient-border rounded-3 p-4">
|
||||||
|
<h1 class="h3 mb-4">Write a Review for {{ $game->name }}</h1>
|
||||||
|
|
||||||
|
<form action="{{ route('games.reviews.store', $game->slug) }}" method="POST">
|
||||||
|
@include('reviews._form')
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Edit Review - ' . $game->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="gradient-border rounded-3 p-4">
|
||||||
|
<h1 class="h3 mb-4">Edit Your Review for {{ $game->name }}</h1>
|
||||||
|
|
||||||
|
<form action="{{ route('reviews.update', $review->id) }}" method="POST">
|
||||||
|
@method('PUT')
|
||||||
|
@include('reviews._form')
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
+27
-1
@@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\ProfileController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Http\Controllers\GameController;
|
||||||
|
use App\Http\Controllers\ReviewController;
|
||||||
|
use App\Http\Controllers\GameListController;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
return view('welcome');
|
return view('landing');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('/dashboard', function () {
|
Route::get('/dashboard', function () {
|
||||||
@@ -17,4 +20,27 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::get('/games', [App\Http\Controllers\GameController::class, 'index'])->name('games.index');
|
||||||
|
Route::get('/games/{game:slug}', [App\Http\Controllers\GameController::class, 'show'])->name('games.show');
|
||||||
|
|
||||||
|
// Review routes
|
||||||
|
Route::middleware('auth')->group(function () {
|
||||||
|
Route::get('/games/{game:slug}/reviews/create', [App\Http\Controllers\ReviewController::class, 'create'])->name('games.reviews.create');
|
||||||
|
Route::post('/games/{game:slug}/reviews', [App\Http\Controllers\ReviewController::class, 'store'])->name('games.reviews.store');
|
||||||
|
Route::get('/games/{game:slug}/reviews/{review}', [App\Http\Controllers\ReviewController::class, 'show'])->name('games.reviews.show');
|
||||||
|
Route::get('/games/{game:slug}/reviews/{review}/edit', [App\Http\Controllers\ReviewController::class, 'edit'])->name('games.reviews.edit');
|
||||||
|
Route::put('/games/{game:slug}/reviews/{review}', [App\Http\Controllers\ReviewController::class, 'update'])->name('games.reviews.update');
|
||||||
|
Route::delete('/games/{game:slug}/reviews/{review}', [App\Http\Controllers\ReviewController::class, 'destroy'])->name('games.reviews.destroy');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Public review routes
|
||||||
|
Route::get('/games/{game:slug}/reviews', [App\Http\Controllers\ReviewController::class, 'index'])->name('games.reviews.index');
|
||||||
|
|
||||||
|
// Game Lists
|
||||||
|
Route::middleware('auth')->group(function () {
|
||||||
|
Route::resource('lists', GameListController::class);
|
||||||
|
Route::post('lists/{list}/games', [GameListController::class, 'addGame'])->name('lists.addGame');
|
||||||
|
Route::delete('lists/{list}/games/{game}', [GameListController::class, 'removeGame'])->name('lists.removeGame');
|
||||||
|
});
|
||||||
|
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import defaultTheme from 'tailwindcss/defaultTheme';
|
|
||||||
import forms from '@tailwindcss/forms';
|
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
content: [
|
|
||||||
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
|
|
||||||
'./storage/framework/views/*.php',
|
|
||||||
'./resources/views/**/*.blade.php',
|
|
||||||
],
|
|
||||||
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
fontFamily: {
|
|
||||||
sans: ['Figtree', ...defaultTheme.fontFamily.sans],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [forms],
|
|
||||||
};
|
|
||||||
@@ -8,4 +8,9 @@ export default defineConfig({
|
|||||||
refresh: true,
|
refresh: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['bootstrap']
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user