Add follow system, activity feed, notifications, sitemap, caching, Scout search

This commit is contained in:
Mark
2026-05-30 11:23:24 +03:00
parent 5c6ba86e15
commit 3083071a6c
24 changed files with 1657 additions and 49 deletions
+114
View File
@@ -0,0 +1,114 @@
@extends('layouts.app')
@section('title', 'Activity Feed — GameLog')
@section('content')
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-white">Activity Feed</h1>
<div class="flex gap-2">
<a href="{{ route('activity.index') }}"
class="text-sm px-3 py-1.5 rounded-lg {{ request()->routeIs('activity.index') ? 'bg-purple-600 text-white' : 'text-slate-400 hover:text-slate-200' }} transition-colors">
Following
</a>
<a href="{{ route('activity.global') }}"
class="text-sm px-3 py-1.5 rounded-lg {{ request()->routeIs('activity.global') ? 'bg-purple-600 text-white' : 'text-slate-400 hover:text-slate-200' }} transition-colors">
Global
</a>
</div>
</div>
@if($activities->isEmpty())
<div class="text-center py-20">
<div class="w-16 h-16 rounded-full bg-[#16161d] flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h3 class="text-lg font-medium text-slate-300 mb-2">No activity yet</h3>
<p class="text-slate-500 mb-6">Follow some users to see their activity here.</p>
<a href="{{ route('games.index') }}" class="inline-flex items-center gap-2 px-5 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-semibold rounded-lg transition-colors">
Browse Games
</a>
</div>
@else
<div class="space-y-3">
@foreach($activities as $activity)
@php
$game = $activity->subject;
@endphp
<div class="bg-[#16161d] border border-white/5 rounded-xl p-4 flex items-start gap-4">
{{-- User avatar --}}
<a href="{{ $activity->user ? route('users.show', $activity->user->username) : '#' }}" class="flex-shrink-0">
@if($activity->user?->avatar)
<img src="{{ $activity->user->avatar }}" class="w-9 h-9 rounded-full object-cover border border-white/10" alt="">
@else
<div class="w-9 h-9 rounded-full bg-purple-700 flex items-center justify-center text-sm font-bold text-white">
{{ strtoupper(substr($activity->user?->name ?? '?', 0, 1)) }}
</div>
@endif
</a>
{{-- Game thumbnail --}}
@if($game && $game->cover_thumb_url)
<a href="{{ route('games.show', $game->slug) }}" class="flex-shrink-0">
<img src="{{ $game->cover_thumb_url }}" alt="{{ $game->title }}"
class="w-10 h-14 object-cover rounded border border-white/10 bg-[#0f0f13]">
</a>
@endif
{{-- Activity text --}}
<div class="flex-1 min-w-0">
<p class="text-sm text-slate-300">
<a href="{{ $activity->user ? route('users.show', $activity->user->username) : '#' }}"
class="font-semibold text-white hover:text-purple-300 transition-colors">
{{ $activity->user?->username ?? 'Unknown' }}
</a>
@if($activity->type === 'added_to_library')
added
@if($game)
<a href="{{ route('games.show', $game->slug) }}" class="text-purple-400 hover:text-purple-300 font-medium transition-colors">{{ $game->title }}</a>
@endif
to their library
@if(!empty($activity->data['status']))
<span class="text-slate-500">as <span class="text-slate-400">{{ str_replace('_', ' ', $activity->data['status']) }}</span></span>
@endif
@elseif($activity->type === 'rated_game')
rated
@if($game)
<a href="{{ route('games.show', $game->slug) }}" class="text-purple-400 hover:text-purple-300 font-medium transition-colors">{{ $game->title }}</a>
@endif
<span class="text-yellow-400 font-semibold">{{ $activity->data['rating'] ?? '' }}/10</span>
@elseif($activity->type === 'reviewed_game')
reviewed
@if($game)
<a href="{{ route('games.show', $game->slug) }}" class="text-purple-400 hover:text-purple-300 font-medium transition-colors">{{ $game->title }}</a>
@endif
@if(!empty($activity->data['rating']))
<span class="text-slate-500">— <span class="text-yellow-400">{{ $activity->data['rating'] }}/10</span></span>
@endif
@else
{{ str_replace('_', ' ', $activity->type) }}
@if($game)
<a href="{{ route('games.show', $game->slug) }}" class="text-purple-400 hover:text-purple-300 font-medium transition-colors">{{ $game->title }}</a>
@endif
@endif
</p>
<p class="text-xs text-slate-600 mt-1">{{ $activity->created_at?->diffForHumans() }}</p>
</div>
</div>
@endforeach
</div>
<div class="mt-6">
{{ $activities->links() }}
</div>
@endif
</div>
@endsection
+16
View File
@@ -41,6 +41,9 @@
<a href="{{ route('games.index') }}" class="text-sm text-slate-400 hover:text-slate-200 transition-colors">Browse</a>
<a href="{{ route('games.top-rated') }}" class="text-sm text-slate-400 hover:text-slate-200 transition-colors">Top Rated</a>
<a href="{{ route('games.new-releases') }}" class="text-sm text-slate-400 hover:text-slate-200 transition-colors">New Releases</a>
@auth
<a href="{{ route('activity.index') }}" class="text-sm text-slate-400 hover:text-slate-200 transition-colors">Activity</a>
@endauth
</div>
</div>
@@ -96,6 +99,19 @@
<!-- Right: Auth -->
<div class="flex items-center gap-3">
@auth
{{-- Notification bell --}}
@php $unreadCount = auth()->user()->notifications()->whereNull('read_at')->count(); @endphp
<a href="{{ route('notifications.index') }}" class="relative text-slate-400 hover:text-slate-200 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
@if($unreadCount > 0)
<span class="absolute -top-1 -right-1 w-4 h-4 bg-purple-600 rounded-full text-[10px] font-bold text-white flex items-center justify-center">
{{ $unreadCount > 9 ? '9+' : $unreadCount }}
</span>
@endif
</a>
<div class="relative" x-data="{ open: false }">
<button @click="open = !open" class="flex items-center gap-2 text-sm text-slate-400 hover:text-slate-200 transition-colors">
@if(auth()->user()->avatar)
@@ -0,0 +1,66 @@
@extends('layouts.app')
@section('title', 'Notifications — GameLog')
@section('content')
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<h1 class="text-2xl font-bold text-white mb-6">Notifications</h1>
@if($notifications->isEmpty())
<div class="text-center py-20">
<div class="w-16 h-16 rounded-full bg-[#16161d] flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
</div>
<h3 class="text-lg font-medium text-slate-300 mb-2">No notifications yet</h3>
<p class="text-slate-500">You'll see notifications here when someone follows you.</p>
</div>
@else
<div class="space-y-2">
@foreach($notifications as $notification)
<div class="bg-[#16161d] border border-white/5 rounded-xl p-4 flex items-center gap-4 {{ is_null($notification->read_at) ? 'border-purple-500/20 bg-purple-900/10' : '' }}">
@if($notification->type === 'new_follower')
@php $data = $notification->data ?? []; @endphp
<div class="w-10 h-10 rounded-full bg-purple-700 flex items-center justify-center text-sm font-bold text-white flex-shrink-0">
{{ strtoupper(substr($data['follower_name'] ?? '?', 0, 1)) }}
</div>
<div class="flex-1 min-w-0">
<p class="text-sm text-slate-300">
@if(!empty($data['follower_username']))
<a href="{{ route('users.show', $data['follower_username']) }}"
class="font-semibold text-white hover:text-purple-300 transition-colors">
{{ $data['follower_name'] ?? $data['follower_username'] }}
</a>
@else
<span class="font-semibold text-white">Someone</span>
@endif
started following you
</p>
<p class="text-xs text-slate-600 mt-0.5">{{ $notification->created_at?->diffForHumans() }}</p>
</div>
@if(!empty($data['follower_username']))
<a href="{{ route('users.show', $data['follower_username']) }}"
class="text-xs text-purple-400 hover:text-purple-300 transition-colors flex-shrink-0">
View profile →
</a>
@endif
@else
<div class="w-10 h-10 rounded-full bg-slate-700 flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="flex-1">
<p class="text-sm text-slate-400">{{ $notification->type }}</p>
<p class="text-xs text-slate-600 mt-0.5">{{ $notification->created_at?->diffForHumans() }}</p>
</div>
@endif
</div>
@endforeach
</div>
@endif
</div>
@endsection
+48 -5
View File
@@ -45,11 +45,54 @@
</div>
</div>
<div class="flex gap-2">
<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 class="flex items-start gap-6">
{{-- 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' }},
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;
}
}
}">
<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>
</div>