Skip to content

Commit 0ef9329

Browse files
dotMavriQclaude
andcommitted
feat: scaffold Playing (Games) category with IGDB Discover
- Add PlayingStatus and OwnershipStatus enums - Create Game model, migration, and policy - Build full CRUD: GameIndex (gallery/list views, search, filter by status/ownership/platform), GameForm (create/edit with multi-platform input), GameShow (detail view with 1-10 rating, completion bar) - Add PlayingIndex category hub - Integrate IGDB API via Saloon connector with Twitch OAuth - Build Discover Games wizard (search → results → configure & add) with platform filtering and owned-platform selection - Add igdb_id column (separate from rawg_id) - Add CSS custom properties for game status colors (both themes) - Update dashboard to link Playing category Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 050409c commit 0ef9329

26 files changed

Lines changed: 2588 additions & 3 deletions

app/Enums/OwnershipStatus.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Enums;
6+
7+
enum OwnershipStatus: string
8+
{
9+
case Owned = 'owned';
10+
case PreviouslyOwned = 'previously_owned';
11+
case NotOwned = 'not_owned';
12+
13+
public function label(): string
14+
{
15+
return match ($this) {
16+
self::Owned => 'Owned',
17+
self::PreviouslyOwned => 'Previously Owned',
18+
self::NotOwned => 'Not Owned',
19+
};
20+
}
21+
22+
public function color(): string
23+
{
24+
return match ($this) {
25+
self::Owned => 'green',
26+
self::PreviouslyOwned => 'yellow',
27+
self::NotOwned => 'gray',
28+
};
29+
}
30+
}

app/Enums/PlayingStatus.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Enums;
6+
7+
enum PlayingStatus: string
8+
{
9+
case WantToPlay = 'want_to_play';
10+
case Playing = 'playing';
11+
case Played = 'played';
12+
13+
public function label(): string
14+
{
15+
return match ($this) {
16+
self::WantToPlay => 'Want to Play',
17+
self::Playing => 'Playing',
18+
self::Played => 'Played',
19+
};
20+
}
21+
22+
public function color(): string
23+
{
24+
return match ($this) {
25+
self::WantToPlay => 'purple',
26+
self::Playing => 'yellow',
27+
self::Played => 'green',
28+
};
29+
}
30+
}

app/Livewire/Dashboard.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ public function getCategories(): array
3333
[
3434
'name' => 'Playing',
3535
'icon' => 'puzzle-piece',
36-
'description' => 'Video Games, Board Games',
37-
'route' => null,
38-
'active' => false,
36+
'description' => 'Video Games',
37+
'route' => 'playing.index',
38+
'active' => true,
3939
'color' => 'green',
4040
],
4141
[

app/Livewire/Games/GameForm.php

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Livewire\Games;
6+
7+
use App\Enums\OwnershipStatus;
8+
use App\Enums\PlayingStatus;
9+
use App\Models\Game;
10+
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
11+
use Illuminate\Support\Facades\Auth;
12+
use Illuminate\Validation\Rule;
13+
use Livewire\Component;
14+
15+
class GameForm extends Component
16+
{
17+
use AuthorizesRequests;
18+
19+
public ?Game $game = null;
20+
21+
public string $title = '';
22+
23+
public array $platform = [];
24+
25+
public string $genre = '';
26+
27+
public string $description = '';
28+
29+
public string $cover_url = '';
30+
31+
public ?string $release_date = null;
32+
33+
public string $developer = '';
34+
35+
public string $publisher = '';
36+
37+
public string $status = 'want_to_play';
38+
39+
public string $ownership = 'not_owned';
40+
41+
public ?int $rating = null;
42+
43+
public ?float $hours_played = null;
44+
45+
public ?int $completion_percentage = null;
46+
47+
public ?int $igdb_id = null;
48+
49+
public ?int $rawg_id = null;
50+
51+
public ?int $mobygames_id = null;
52+
53+
public ?string $date_started = null;
54+
55+
public ?string $date_finished = null;
56+
57+
public string $notes = '';
58+
59+
public string $platformInput = '';
60+
61+
public function mount(?Game $game = null): void
62+
{
63+
if ($game && $game->exists) {
64+
$this->authorize('update', $game);
65+
$this->game = $game;
66+
$this->fill([
67+
'title' => $game->title,
68+
'platform' => $game->platform ?? [],
69+
'genre' => $game->genre ?? '',
70+
'description' => $game->description ?? '',
71+
'cover_url' => $game->cover_url ?? '',
72+
'release_date' => $game->release_date?->format('d/m/Y'),
73+
'developer' => $game->developer ?? '',
74+
'publisher' => $game->publisher ?? '',
75+
'status' => $game->status->value,
76+
'ownership' => $game->ownership->value,
77+
'rating' => $game->rating,
78+
'hours_played' => $game->hours_played ? (float) $game->hours_played : null,
79+
'completion_percentage' => $game->completion_percentage,
80+
'igdb_id' => $game->igdb_id,
81+
'rawg_id' => $game->rawg_id,
82+
'mobygames_id' => $game->mobygames_id,
83+
'date_started' => $game->date_started?->format('d/m/Y'),
84+
'date_finished' => $game->date_finished?->format('d/m/Y'),
85+
'notes' => $game->notes ?? '',
86+
]);
87+
}
88+
}
89+
90+
public function rules(): array
91+
{
92+
return [
93+
'title' => ['required', 'string', 'max:255'],
94+
'platform' => ['nullable', 'array'],
95+
'platform.*' => ['string', 'max:50'],
96+
'genre' => ['nullable', 'string', 'max:500'],
97+
'description' => ['nullable', 'string', 'max:10000'],
98+
'cover_url' => ['nullable', 'url', 'max:2048'],
99+
'release_date' => ['nullable', 'date_format:d/m/Y'],
100+
'developer' => ['nullable', 'string', 'max:255'],
101+
'publisher' => ['nullable', 'string', 'max:255'],
102+
'status' => ['required', Rule::enum(PlayingStatus::class)],
103+
'ownership' => ['required', Rule::enum(OwnershipStatus::class)],
104+
'rating' => ['nullable', 'integer', 'min:1', 'max:10'],
105+
'hours_played' => ['nullable', 'numeric', 'min:0', 'max:99999'],
106+
'completion_percentage' => ['nullable', 'integer', 'min:0', 'max:100'],
107+
'igdb_id' => ['nullable', 'integer'],
108+
'rawg_id' => ['nullable', 'integer'],
109+
'mobygames_id' => ['nullable', 'integer'],
110+
'date_started' => ['nullable', 'date_format:d/m/Y'],
111+
'date_finished' => ['nullable', 'date_format:d/m/Y'],
112+
'notes' => ['nullable', 'string', 'max:10000'],
113+
];
114+
}
115+
116+
protected function parseDateInput(?string $date): ?string
117+
{
118+
if (empty($date)) {
119+
return null;
120+
}
121+
122+
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
123+
return $date;
124+
}
125+
126+
if (preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $date, $matches)) {
127+
$day = $matches[1];
128+
$month = $matches[2];
129+
$year = $matches[3];
130+
131+
if (checkdate((int) $month, (int) $day, (int) $year)) {
132+
return "{$year}-{$month}-{$day}";
133+
}
134+
}
135+
136+
return null;
137+
}
138+
139+
public function addPlatform(): void
140+
{
141+
$platform = trim($this->platformInput);
142+
if ($platform !== '' && ! in_array($platform, $this->platform)) {
143+
$this->platform[] = $platform;
144+
}
145+
$this->platformInput = '';
146+
}
147+
148+
public function removePlatform(int $index): void
149+
{
150+
unset($this->platform[$index]);
151+
$this->platform = array_values($this->platform);
152+
}
153+
154+
public function save(): void
155+
{
156+
$validated = $this->validate();
157+
158+
$validated['release_date'] = $this->parseDateInput($validated['release_date'] ?? null);
159+
$validated['date_started'] = $this->parseDateInput($validated['date_started'] ?? null);
160+
$validated['date_finished'] = $this->parseDateInput($validated['date_finished'] ?? null);
161+
162+
$data = [
163+
'title' => $validated['title'],
164+
'platform' => ! empty($validated['platform']) ? $validated['platform'] : null,
165+
'genre' => $validated['genre'] ?: null,
166+
'description' => $validated['description'] ?: null,
167+
'cover_url' => $validated['cover_url'] ?: null,
168+
'release_date' => $validated['release_date'],
169+
'developer' => $validated['developer'] ?: null,
170+
'publisher' => $validated['publisher'] ?: null,
171+
'status' => $validated['status'],
172+
'ownership' => $validated['ownership'],
173+
'rating' => $validated['rating'],
174+
'hours_played' => $validated['hours_played'],
175+
'completion_percentage' => $validated['completion_percentage'],
176+
'igdb_id' => $validated['igdb_id'],
177+
'rawg_id' => $validated['rawg_id'],
178+
'mobygames_id' => $validated['mobygames_id'],
179+
'date_started' => $validated['date_started'],
180+
'date_finished' => $validated['date_finished'],
181+
'notes' => $validated['notes'] ?: null,
182+
];
183+
184+
if ($this->game) {
185+
$this->game->update($data);
186+
$message = 'Game updated successfully.';
187+
} else {
188+
$data['user_id'] = Auth::id();
189+
$this->game = Game::create($data);
190+
$message = 'Game created successfully.';
191+
}
192+
193+
session()->flash('message', $message);
194+
195+
$this->redirect(route('games.show', $this->game));
196+
}
197+
198+
public function isEditing(): bool
199+
{
200+
return $this->game !== null && $this->game->exists;
201+
}
202+
203+
public function render()
204+
{
205+
return view('livewire.games.game-form', [
206+
'statuses' => PlayingStatus::cases(),
207+
'ownershipStatuses' => OwnershipStatus::cases(),
208+
'isEditing' => $this->isEditing(),
209+
])->layout('layouts.app');
210+
}
211+
}

0 commit comments

Comments
 (0)