Add friend library comparison and game comments features

- CompareController: side-by-side library diff with stats (common games, rating diff)
- GameCommentController + GameCommentPolicy: threaded comments with replies, auth-gated
- GameComment model with top-level scope and self-referential replies relation
- Migration: game_comments table with parent_id for threading
- games/show: Discussion section with Alpine.js (post, reply, delete)
- compare/show: tabbed view (in common, they have, you have) with game cards
- users/show: "Compare Libraries" button visible to other authenticated users

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mark
2026-05-30 12:04:24 +03:00
co-authored by Claude Sonnet 4.6
parent 3083071a6c
commit 14047dfe37
12 changed files with 608 additions and 2 deletions
@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
class CompareController extends Controller
{
public function show(User $user)
{
if (auth()->id() === $user->id) {
return redirect()->route('users.show', $user->username);
}
$myGames = auth()->user()->library()->with('game')->get()->keyBy('game_id');
$theirGames = $user->library()->with('game')->get()->keyBy('game_id');
$myIds = $myGames->keys()->values();
$theirIds = $theirGames->keys()->values();
$bothIds = $myIds->intersect($theirIds)->values();
$theyHaveNotYouIds = $theirIds->diff($myIds)->values();
$youHaveNotThemIds = $myIds->diff($theirIds)->values();
$both = $bothIds->map(fn($id) => [
'game' => $myGames[$id]->game,
'myEntry' => $myGames[$id],
'theirEntry' => $theirGames[$id],
])->filter(fn($item) => $item['game'] !== null)->values();
$theyHaveNotYou = $theyHaveNotYouIds
->map(fn($id) => $theirGames[$id])
->filter(fn($ug) => $ug->game !== null)
->values();
$youHaveNotThem = $youHaveNotThemIds
->map(fn($id) => $myGames[$id])
->filter(fn($ug) => $ug->game !== null)
->values();
// Average rating diff on games both rated
$diffSum = 0;
$diffCount = 0;
foreach ($bothIds as $id) {
$myRating = $myGames[$id]->rating;
$theirRating = $theirGames[$id]->rating;
if ($myRating !== null && $theirRating !== null) {
$diffSum += abs($myRating - $theirRating);
$diffCount++;
}
}
$stats = [
'both_count' => $bothIds->count(),
'they_not_you' => $theyHaveNotYouIds->count(),
'you_not_them' => $youHaveNotThemIds->count(),
'avg_rating_diff' => $diffCount > 0 ? round($diffSum / $diffCount, 1) : null,
];
return view('compare.show', compact('user', 'both', 'theyHaveNotYou', 'youHaveNotThem', 'stats'));
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers;
use App\Models\Game;
use App\Models\GameComment;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class GameCommentController extends Controller
{
public function store(Request $request, Game $game): JsonResponse
{
$data = $request->validate([
'body' => 'required|string|max:1000',
'parent_id' => 'nullable|integer|exists:game_comments,id',
]);
if (isset($data['parent_id'])) {
$parent = GameComment::findOrFail($data['parent_id']);
abort_if($parent->game_id !== $game->id, 422, 'Parent comment does not belong to this game.');
}
$comment = $game->comments()->create([
'user_id' => auth()->id(),
'body' => $data['body'],
'parent_id' => $data['parent_id'] ?? null,
]);
$comment->load('user');
return response()->json([
'id' => $comment->id,
'body' => $comment->body,
'user' => ['name' => $comment->user->name, 'username' => $comment->user->username],
'user_id' => $comment->user_id,
'created_at' => $comment->created_at->diffForHumans(),
'replies_count' => 0,
], 201);
}
public function destroy(GameComment $comment): JsonResponse
{
$this->authorize('delete', $comment);
$comment->delete();
return response()->json(['deleted' => true]);
}
}
+8 -1
View File
@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\Game;
use App\Models\GameComment;
use App\Models\Genre;
use App\Models\Platform;
use Illuminate\Http\Request;
@@ -54,7 +55,13 @@ class GameController extends Controller
$userLists = auth()->user()->lists()->get(['id', 'title']);
}
$response = view('games.show', compact('game', 'userGame', 'userReview', 'userLists'));
$comments = $game->comments()->whereNull('parent_id')
->with(['user', 'replies.user'])
->latest()
->limit(20)
->get();
$response = view('games.show', compact('game', 'userGame', 'userReview', 'userLists', 'comments'));
return auth()->check()
? $response
: response($response)->header('Cache-Control', 'public, max-age=300');