Add follow system, activity feed, notifications, sitemap, caching, Scout search
This commit is contained in:
@@ -0,0 +1,429 @@
|
||||
# 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
|
||||
<?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)
|
||||
|
||||
```php
|
||||
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
|
||||
<?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
|
||||
<?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:
|
||||
|
||||
```html
|
||||
<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
|
||||
<?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
|
||||
|
||||
```php
|
||||
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
|
||||
|
||||
```php
|
||||
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
|
||||
<?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
|
||||
|
||||
```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
|
||||
<?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)
|
||||
|
||||
```php
|
||||
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"`
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user