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
+206
View File
@@ -0,0 +1,206 @@
@extends('layouts.app')
@section('title', 'Your Library vs ' . ($user->username ?? $user->name) . ' — GameLog')
@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10"
x-data="{ tab: 'both' }">
{{-- Header --}}
<div class="mb-8">
<a href="{{ route('users.show', $user->username) }}"
class="text-sm text-slate-500 hover:text-slate-300 transition-colors mb-2 inline-block">
← {{ $user->username ?? $user->name }}'s profile
</a>
<h1 class="text-2xl font-bold text-white">
Your Library vs <span class="text-purple-400">{{ $user->username ?? $user->name }}</span>'s Library
</h1>
</div>
{{-- Stats Row --}}
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8">
<div class="bg-[#16161d] border border-white/10 rounded-xl p-4 text-center">
<div class="text-2xl font-bold text-white">{{ $stats['both_count'] }}</div>
<div class="text-xs text-slate-500 mt-1 uppercase tracking-wide">In Common</div>
</div>
<div class="bg-[#16161d] border border-white/10 rounded-xl p-4 text-center">
<div class="text-2xl font-bold text-white">{{ $stats['they_not_you'] }}</div>
<div class="text-xs text-slate-500 mt-1 uppercase tracking-wide">They Have, You Don't</div>
</div>
<div class="bg-[#16161d] border border-white/10 rounded-xl p-4 text-center">
<div class="text-2xl font-bold text-white">{{ $stats['you_not_them'] }}</div>
<div class="text-xs text-slate-500 mt-1 uppercase tracking-wide">You Have, They Don't</div>
</div>
<div class="bg-[#16161d] border border-white/10 rounded-xl p-4 text-center">
@if($stats['avg_rating_diff'] !== null)
<div class="text-2xl font-bold text-white">{{ $stats['avg_rating_diff'] }}</div>
<div class="text-xs text-slate-500 mt-1 uppercase tracking-wide">Avg Rating Diff</div>
@else
<div class="text-2xl font-bold text-slate-600">—</div>
<div class="text-xs text-slate-500 mt-1 uppercase tracking-wide">Avg Rating Diff</div>
@endif
</div>
</div>
{{-- Tabs --}}
<div class="flex gap-1 mb-6 bg-[#16161d] border border-white/10 rounded-xl p-1 w-fit">
<button @click="tab = 'both'"
:class="tab === 'both' ? 'bg-purple-600 text-white' : 'text-slate-400 hover:text-white'"
class="px-4 py-2 text-sm font-medium rounded-lg transition-colors">
In Common ({{ $stats['both_count'] }})
</button>
<button @click="tab = 'they'"
:class="tab === 'they' ? 'bg-purple-600 text-white' : 'text-slate-400 hover:text-white'"
class="px-4 py-2 text-sm font-medium rounded-lg transition-colors">
They Played, You Haven't ({{ $stats['they_not_you'] }})
</button>
<button @click="tab = 'you'"
:class="tab === 'you' ? 'bg-purple-600 text-white' : 'text-slate-400 hover:text-white'"
class="px-4 py-2 text-sm font-medium rounded-lg transition-colors">
You Played, They Haven't ({{ $stats['you_not_them'] }})
</button>
</div>
{{-- Tab: In Common --}}
<div x-show="tab === 'both'" x-transition>
@if($both->isEmpty())
<div class="text-center py-16 text-slate-500">No games in common yet.</div>
@else
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
@foreach($both as $item)
@php $game = $item['game']; @endphp
<div class="bg-[#16161d] border border-white/5 rounded-xl overflow-hidden hover:border-white/20 transition-colors">
<a href="{{ route('games.show', $game) }}">
@if($game->cover_url)
<img src="{{ $game->cover_url }}" alt="{{ $game->title }}" class="w-full aspect-[3/4] object-cover">
@else
<div class="w-full aspect-[3/4] bg-[#0f0f13] flex items-center justify-center">
<svg class="w-10 h-10 text-slate-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"/>
</svg>
</div>
@endif
</a>
<div class="p-3">
<a href="{{ route('games.show', $game) }}"
class="text-sm font-medium text-slate-200 hover:text-white line-clamp-2 block mb-2 transition-colors">
{{ $game->title }}
</a>
<div class="flex items-center justify-between text-xs text-slate-500">
<div class="flex items-center gap-1">
<span class="text-slate-600">You</span>
@if($item['myEntry']->rating)
<span class="text-yellow-400 font-bold">{{ $item['myEntry']->rating }}</span>
@else
<span class="text-slate-600">—</span>
@endif
</div>
<div class="w-px h-3 bg-white/10"></div>
<div class="flex items-center gap-1">
<span class="text-slate-600">Them</span>
@if($item['theirEntry']->rating)
<span class="text-purple-400 font-bold">{{ $item['theirEntry']->rating }}</span>
@else
<span class="text-slate-600">—</span>
@endif
</div>
</div>
</div>
</div>
@endforeach
</div>
@endif
</div>
{{-- Tab: They Played, You Haven't --}}
<div x-show="tab === 'they'" x-transition>
@if($theyHaveNotYou->isEmpty())
<div class="text-center py-16 text-slate-500">No games here.</div>
@else
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
@foreach($theyHaveNotYou as $entry)
<div class="bg-[#16161d] border border-white/5 rounded-xl overflow-hidden hover:border-white/20 transition-colors">
<a href="{{ route('games.show', $entry->game) }}">
@if($entry->game->cover_url)
<img src="{{ $entry->game->cover_url }}" alt="{{ $entry->game->title }}" class="w-full aspect-[3/4] object-cover">
@else
<div class="w-full aspect-[3/4] bg-[#0f0f13] flex items-center justify-center">
<svg class="w-10 h-10 text-slate-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"/>
</svg>
</div>
@endif
</a>
<div class="p-3">
<a href="{{ route('games.show', $entry->game) }}"
class="text-sm font-medium text-slate-200 hover:text-white line-clamp-2 block mb-2 transition-colors">
{{ $entry->game->title }}
</a>
@if($entry->rating)
<div class="flex items-center gap-1 text-xs text-slate-500 mb-2">
<svg class="w-3 h-3 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
<span class="font-semibold text-white">{{ $entry->rating }}</span><span>/10</span>
</div>
@endif
<form method="POST" action="{{ route('library.store') }}">
@csrf
<input type="hidden" name="game_id" value="{{ $entry->game->id }}">
<input type="hidden" name="status" value="want_to_play">
<button type="submit"
class="w-full text-xs px-2 py-1.5 bg-purple-600/20 hover:bg-purple-600/40 text-purple-300 hover:text-white border border-purple-600/30 rounded-lg transition-colors">
+ Add to Library
</button>
</form>
</div>
</div>
@endforeach
</div>
@endif
</div>
{{-- Tab: You Played, They Haven't --}}
<div x-show="tab === 'you'" x-transition>
@if($youHaveNotThem->isEmpty())
<div class="text-center py-16 text-slate-500">No games here.</div>
@else
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
@foreach($youHaveNotThem as $entry)
<div class="bg-[#16161d] border border-white/5 rounded-xl overflow-hidden hover:border-white/20 transition-colors">
<a href="{{ route('games.show', $entry->game) }}">
@if($entry->game->cover_url)
<img src="{{ $entry->game->cover_url }}" alt="{{ $entry->game->title }}" class="w-full aspect-[3/4] object-cover">
@else
<div class="w-full aspect-[3/4] bg-[#0f0f13] flex items-center justify-center">
<svg class="w-10 h-10 text-slate-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"/>
</svg>
</div>
@endif
</a>
<div class="p-3">
<a href="{{ route('games.show', $entry->game) }}"
class="text-sm font-medium text-slate-200 hover:text-white line-clamp-2 block mb-2 transition-colors">
{{ $entry->game->title }}
</a>
@if($entry->rating)
<div class="flex items-center gap-1 text-xs text-slate-500">
<svg class="w-3 h-3 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
<span class="font-semibold text-white">{{ $entry->rating }}</span><span>/10</span>
</div>
@endif
</div>
</div>
@endforeach
</div>
@endif
</div>
</div>
@endsection