Add follow system, activity feed, notifications, sitemap, caching, Scout search
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user