13 KiB
GameLog Feature Batch Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add follow system, activity feed, notifications, sitemap, Scout search, and caching to the GameLog Laravel 11 app.
Architecture: All features hook into existing User/Game/UserGame/Review models. Migrations for follows/activities already ran. The nav (layouts/app.blade.php) handles all global UI additions. Scout uses the database driver for dev (SQLite).
Tech Stack: Laravel 13 + Tailwind + Alpine.js, SQLite dev, spatie/laravel-sitemap, laravel/scout
Task 1: FollowController + Routes
Files:
-
Create:
app/Http/Controllers/FollowController.php -
Modify:
routes/web.php -
Create FollowController with store/destroy returning JSON
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\JsonResponse;
class FollowController extends Controller
{
public function store(User $user): JsonResponse
{
$me = auth()->user();
if ($me->id === $user->id) {
return response()->json(['error' => 'Cannot follow yourself'], 422);
}
if (!$me->following()->where('following_id', $user->id)->exists()) {
$me->following()->attach($user->id);
$user->notifications()->create([
'id' => \Illuminate\Support\Str::uuid(),
'type' => 'new_follower',
'data' => json_encode([
'follower_id' => $me->id,
'follower_username' => $me->username,
'follower_name' => $me->name,
]),
]);
}
return response()->json([
'following' => true,
'count' => $user->followers()->count(),
]);
}
public function destroy(User $user): JsonResponse
{
auth()->user()->following()->detach($user->id);
return response()->json([
'following' => false,
'count' => $user->followers()->count(),
]);
}
}
- Add routes to web.php (auth group)
Route::middleware('auth')->group(function () {
Route::post('/users/{user:username}/follow', [\App\Http\Controllers\FollowController::class, 'store'])->name('users.follow');
Route::delete('/users/{user:username}/follow', [\App\Http\Controllers\FollowController::class, 'destroy'])->name('users.unfollow');
});
- Commit:
git commit -m "feat: add FollowController and routes"
Task 2: Notifications Migration + Model
Files:
-
Create:
database/migrations/2026_05_30_000001_create_notifications_table.php -
Create:
app/Models/Notification.php -
Create notifications migration
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('type', 100);
$table->json('data');
$table->timestamp('read_at')->nullable();
$table->timestamp('created_at')->nullable();
});
}
public function down(): void { Schema::dropIfExists('notifications'); }
};
- Create Notification model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Notification extends Model
{
public $incrementing = false;
protected $keyType = 'string';
public $timestamps = false;
protected $fillable = ['id', 'user_id', 'type', 'data', 'read_at'];
protected $casts = ['data' => 'array', 'read_at' => 'datetime', 'created_at' => 'datetime'];
public function user(): BelongsTo { return $this->belongsTo(User::class); }
}
-
Run migration:
php artisan migrate -
Commit:
git commit -m "feat: add notifications table and model"
Task 3: Follow Button on users/show.blade.php
Files:
-
Modify:
resources/views/users/show.blade.php -
Add Alpine.js follow button block after
<div class="flex gap-2">(replaces the lone "View Library" button container)
Replace the <div class="flex gap-2"> block with:
<div class="flex items-center gap-4">
{{-- Follower / Following counts --}}
<div class="flex gap-4 text-center">
<div>
<div class="text-lg font-bold text-white">{{ $user->followers()->count() }}</div>
<div class="text-xs text-slate-500 uppercase tracking-wide">Followers</div>
</div>
<div>
<div class="text-lg font-bold text-white">{{ $user->following()->count() }}</div>
<div class="text-xs text-slate-500 uppercase tracking-wide">Following</div>
</div>
</div>
<div class="flex gap-2">
@auth
@if(auth()->id() !== $user->id)
<div x-data="{
following: {{ auth()->user()->isFollowing($user) ? 'true' : 'false' }},
count: {{ $user->followers()->count() }},
async toggle() {
const method = this.following ? 'DELETE' : 'POST';
const res = await fetch('{{ route('users.follow', $user->username) }}', {
method,
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content, 'Accept': 'application/json' }
});
if (res.ok) {
const d = await res.json();
this.following = d.following;
this.count = d.count;
}
}
}">
<button @click="toggle()"
:class="following ? 'bg-white/10 text-slate-300 hover:bg-red-900/30 hover:text-red-400 border border-white/10' : 'bg-purple-600 hover:bg-purple-700 text-white'"
class="px-4 py-2 text-sm font-medium rounded-lg transition-colors">
<span x-text="following ? 'Following' : 'Follow'"></span>
</button>
</div>
@endif
@endauth
<a href="{{ route('users.library', $user->username) }}"
class="px-4 py-2 border border-white/10 hover:border-white/20 text-slate-300 hover:text-white text-sm font-medium rounded-lg transition-colors">
View Library
</a>
</div>
</div>
- Commit:
git commit -m "feat: add follow button and follower counts to user profile"
Task 4: ActivityService + Hook into Models
Files:
-
Create:
app/Services/ActivityService.php -
Modify:
app/Models/UserGame.php -
Modify:
app/Models/Review.php -
Create ActivityService (thin wrapper around Activity::log)
<?php
namespace App\Services;
use App\Models\Activity;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
class ActivityService
{
public static function log(User $user, string $type, Model $subject, array $data = []): void
{
Activity::log($user->id, $type, $subject, $data);
}
}
- Add boot() hooks to UserGame model
use App\Services\ActivityService;
protected static function boot(): void
{
parent::boot();
static::created(function (UserGame $ug) {
ActivityService::log($ug->user, 'added_to_library', $ug->game, ['status' => $ug->status]);
});
static::updated(function (UserGame $ug) {
if ($ug->isDirty('rating') && $ug->rating) {
ActivityService::log($ug->user, 'rated_game', $ug->game, ['rating' => $ug->rating]);
}
});
}
- Add to Review model boot() after the existing saved/deleted callbacks
use App\Services\ActivityService;
// Inside the static::created callback (add this inside the boot method after existing code):
static::created(function (Review $review) {
ActivityService::log($review->user, 'reviewed_game', $review->game, ['rating' => $review->rating]);
});
- Commit:
git commit -m "feat: add ActivityService and log hooks on UserGame/Review"
Task 5: ActivityController + Routes + View
Files:
-
Create:
app/Http/Controllers/ActivityController.php -
Modify:
routes/web.php -
Create:
resources/views/activity/index.blade.php -
Create ActivityController
<?php
namespace App\Http\Controllers;
use App\Models\Activity;
use Illuminate\Support\Facades\DB;
class ActivityController extends Controller
{
public function index()
{
$user = auth()->user();
$followingIds = $user->following()->pluck('users.id')->push($user->id);
$activities = Activity::with(['user', 'subject'])
->whereIn('user_id', $followingIds)
->latest('created_at')
->paginate(40);
return view('activity.index', compact('activities'));
}
public function global()
{
$activities = Activity::with(['user', 'subject'])
->latest('created_at')
->paginate(40);
return view('activity.index', compact('activities'));
}
}
- Add activity routes to web.php
Route::get('/activity', [\App\Http\Controllers\ActivityController::class, 'index'])->name('activity.index')->middleware('auth');
Route::get('/activity/global', [\App\Http\Controllers\ActivityController::class, 'global'])->name('activity.global');
-
Create activity/index.blade.php view (see full content below)
-
Commit:
git commit -m "feat: add activity feed controller, routes, and view"
Task 6: NotificationController + Routes + View
Files:
-
Create:
app/Http/Controllers/NotificationController.php -
Modify:
routes/web.php -
Create:
resources/views/notifications/index.blade.php -
Create NotificationController
<?php
namespace App\Http\Controllers;
use App\Models\Notification;
class NotificationController extends Controller
{
public function index()
{
$notifications = auth()->user()
->notifications()
->latest('created_at')
->limit(20)
->get();
auth()->user()->notifications()->whereNull('read_at')->update(['read_at' => now()]);
return view('notifications.index', compact('notifications'));
}
public function markRead(Notification $notification)
{
abort_if($notification->user_id !== auth()->id(), 403);
$notification->update(['read_at' => now()]);
return response()->json(['ok' => true]);
}
}
- Add notification routes to web.php (auth group)
Route::get('/notifications', [\App\Http\Controllers\NotificationController::class, 'index'])->name('notifications.index')->middleware('auth');
Route::post('/notifications/{notification}/read', [\App\Http\Controllers\NotificationController::class, 'markRead'])->name('notifications.read')->middleware('auth');
-
Add to User model:
public function notifications() { return $this->hasMany(Notification::class)->latest('created_at'); } -
Create notifications/index.blade.php view
-
Add bell icon to nav in layouts/app.blade.php
-
Commit:
git commit -m "feat: add notifications controller, routes, view, and nav bell"
Task 7: Sitemap
Files:
-
Run composer require
-
Create:
app/Console/Commands/GenerateSitemapCommand.php -
Modify:
routes/console.php(Laravel 11 schedules here) -
Modify:
routes/web.php -
Modify:
public/robots.txt -
Install spatie/laravel-sitemap:
composer require spatie/laravel-sitemap -
Create GenerateSitemapCommand
-
Add weekly schedule in routes/console.php
-
Add sitemap route in web.php
-
Update robots.txt
-
Commit:
git commit -m "feat: add sitemap generation command and route"
Task 8: Scout Full-Text Search
Files:
-
Run composer require + vendor:publish
-
Modify:
app/Models/Game.php -
Modify:
app/Http/Controllers/GameController.php -
Modify:
.env -
Install Scout:
composer require laravel/scout && php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider" -
Add SCOUT_DRIVER=database to .env
-
Add Searchable trait and toSearchableArray to Game model
-
Update GameController@search to use Scout
-
Commit:
git commit -m "feat: add Laravel Scout search to Game model"
Task 9: Performance Caching
Files:
-
Modify:
app/Http/Controllers/HomeController.php -
Modify:
app/Http/Controllers/GameController.php -
Modify:
app/Models/Game.php -
Wrap HomeController queries in Cache::remember
-
Wrap GameController topRated/newReleases in Cache::remember with page key
-
Flush caches in Game::recalculateRating()
-
Add cache header to GameController@show response
-
Commit:
git commit -m "feat: add file cache to home/top-rated/new-releases queries"