Add follow system, activity feed, notifications, sitemap, caching, Scout search
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Game;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Spatie\Sitemap\Sitemap;
|
||||
use Spatie\Sitemap\Tags\Url;
|
||||
|
||||
class GenerateSitemapCommand extends Command
|
||||
{
|
||||
protected $signature = 'sitemap:generate';
|
||||
protected $description = 'Generate the sitemap.xml file';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$sitemap = Sitemap::create()
|
||||
->add(Url::create('/')->setPriority(1.0)->setChangeFrequency(Url::CHANGE_FREQUENCY_DAILY))
|
||||
->add(Url::create('/games')->setPriority(0.9)->setChangeFrequency(Url::CHANGE_FREQUENCY_DAILY))
|
||||
->add(Url::create('/top-rated')->setPriority(0.8)->setChangeFrequency(Url::CHANGE_FREQUENCY_WEEKLY))
|
||||
->add(Url::create('/new-releases')->setPriority(0.8)->setChangeFrequency(Url::CHANGE_FREQUENCY_DAILY));
|
||||
|
||||
Game::select('slug', 'updated_at')->chunk(500, function ($games) use ($sitemap) {
|
||||
foreach ($games as $game) {
|
||||
$sitemap->add(
|
||||
Url::create("/games/{$game->slug}")
|
||||
->setLastModificationDate($game->updated_at)
|
||||
->setPriority(0.7)
|
||||
->setChangeFrequency(Url::CHANGE_FREQUENCY_WEEKLY)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
User::where('is_private', false)
|
||||
->select('username', 'updated_at')
|
||||
->chunk(500, function ($users) use ($sitemap) {
|
||||
foreach ($users as $user) {
|
||||
$sitemap->add(
|
||||
Url::create("/users/{$user->username}")
|
||||
->setLastModificationDate($user->updated_at)
|
||||
->setPriority(0.5)
|
||||
->setChangeFrequency(Url::CHANGE_FREQUENCY_WEEKLY)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$sitemap->writeToFile(public_path('sitemap.xml'));
|
||||
|
||||
$this->info('Sitemap generated at public/sitemap.xml');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Activity;
|
||||
|
||||
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'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
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' => Str::uuid(),
|
||||
'type' => 'new_follower',
|
||||
'data' => json_encode([
|
||||
'follower_id' => $me->id,
|
||||
'follower_username' => $me->username,
|
||||
'follower_name' => $me->name,
|
||||
]),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Models\Game;
|
||||
use App\Models\Genre;
|
||||
use App\Models\Platform;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class GameController extends Controller
|
||||
{
|
||||
@@ -53,7 +54,10 @@ class GameController extends Controller
|
||||
$userLists = auth()->user()->lists()->get(['id', 'title']);
|
||||
}
|
||||
|
||||
return view('games.show', compact('game', 'userGame', 'userReview', 'userLists'));
|
||||
$response = view('games.show', compact('game', 'userGame', 'userReview', 'userLists'));
|
||||
return auth()->check()
|
||||
? $response
|
||||
: response($response)->header('Cache-Control', 'public, max-age=300');
|
||||
}
|
||||
|
||||
public function search(Request $request)
|
||||
@@ -62,23 +66,19 @@ class GameController extends Controller
|
||||
$games = collect();
|
||||
|
||||
if (strlen($q) >= 2) {
|
||||
$games = Game::published()
|
||||
->where('title', 'like', "%{$q}%")
|
||||
->orderByDesc('rating_count')
|
||||
->limit(20)
|
||||
->get();
|
||||
$games = Game::search($q)->get()->filter(fn($g) => $g->status === 'published');
|
||||
}
|
||||
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json($games->map(fn($g) => [
|
||||
'id' => $g->id,
|
||||
'title' => $g->title,
|
||||
'slug' => $g->slug,
|
||||
'cover_thumb' => $g->cover_thumb_url,
|
||||
'release_year' => $g->release_year,
|
||||
'rating_avg' => $g->rating_avg,
|
||||
'url' => route('games.show', $g),
|
||||
]));
|
||||
return response()->json($games->take(6)->map(fn($g) => [
|
||||
'id' => $g->id,
|
||||
'title' => $g->title,
|
||||
'slug' => $g->slug,
|
||||
'cover_thumb' => $g->cover_thumb_url,
|
||||
'release_year' => $g->release_year,
|
||||
'rating_avg' => $g->rating_avg,
|
||||
'url' => route('games.show', $g),
|
||||
])->values());
|
||||
}
|
||||
|
||||
return view('games.search', compact('games', 'q'));
|
||||
@@ -86,24 +86,30 @@ class GameController extends Controller
|
||||
|
||||
public function topRated(Request $request)
|
||||
{
|
||||
$games = Game::published()
|
||||
->where('rating_count', '>=', 1)
|
||||
->orderByDesc('rating_avg')
|
||||
->orderByDesc('rating_count')
|
||||
->paginate(24)
|
||||
->withQueryString();
|
||||
$page = $request->get('page', 1);
|
||||
$games = Cache::remember("games.top_rated.{$page}", 300, fn() =>
|
||||
Game::published()
|
||||
->where('rating_count', '>=', 1)
|
||||
->orderByDesc('rating_avg')
|
||||
->orderByDesc('rating_count')
|
||||
->paginate(24)
|
||||
->withQueryString()
|
||||
);
|
||||
|
||||
return view('games.top-rated', compact('games'));
|
||||
}
|
||||
|
||||
public function newReleases(Request $request)
|
||||
{
|
||||
$games = Game::published()
|
||||
->whereNotNull('release_date')
|
||||
->where('release_date', '<=', now())
|
||||
->orderByDesc('release_date')
|
||||
->paginate(24)
|
||||
->withQueryString();
|
||||
$page = $request->get('page', 1);
|
||||
$games = Cache::remember("games.new_releases.{$page}", 300, fn() =>
|
||||
Game::published()
|
||||
->whereNotNull('release_date')
|
||||
->where('release_date', '<=', now())
|
||||
->orderByDesc('release_date')
|
||||
->paginate(24)
|
||||
->withQueryString()
|
||||
);
|
||||
|
||||
return view('games.new-releases', compact('games'));
|
||||
}
|
||||
|
||||
@@ -3,24 +3,29 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Game;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$topRated = Game::published()
|
||||
->where('rating_count', '>=', 1)
|
||||
->orderByDesc('rating_avg')
|
||||
->orderByDesc('rating_count')
|
||||
->limit(12)
|
||||
->get();
|
||||
$topRated = Cache::remember('home.top_rated', 300, fn() =>
|
||||
Game::published()
|
||||
->where('rating_count', '>=', 1)
|
||||
->orderByDesc('rating_avg')
|
||||
->orderByDesc('rating_count')
|
||||
->limit(12)
|
||||
->get()
|
||||
);
|
||||
|
||||
$newReleases = Game::published()
|
||||
->whereNotNull('release_date')
|
||||
->where('release_date', '<=', now())
|
||||
->orderByDesc('release_date')
|
||||
->limit(8)
|
||||
->get();
|
||||
$newReleases = Cache::remember('home.new_releases', 300, fn() =>
|
||||
Game::published()
|
||||
->whereNotNull('release_date')
|
||||
->where('release_date', '<=', now())
|
||||
->orderByDesc('release_date')
|
||||
->limit(8)
|
||||
->get()
|
||||
);
|
||||
|
||||
return view('home', compact('topRated', 'newReleases'));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Notification;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$notifications = auth()->user()
|
||||
->notifications()
|
||||
->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]);
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -5,12 +5,14 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Spatie\Sluggable\HasSlug;
|
||||
use Spatie\Sluggable\SlugOptions;
|
||||
|
||||
class Game extends Model
|
||||
{
|
||||
use HasSlug;
|
||||
use HasSlug, Searchable;
|
||||
|
||||
protected $fillable = [
|
||||
'igdb_id', 'slug', 'title', 'summary', 'cover_url', 'cover_thumb_url',
|
||||
@@ -60,6 +62,15 @@ class Game extends Model
|
||||
return $this->hasMany(Review::class)->latest();
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'summary' => $this->summary,
|
||||
];
|
||||
}
|
||||
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('status', 'published');
|
||||
@@ -72,5 +83,8 @@ class Game extends Model
|
||||
'rating_avg' => $stats->avg ? round($stats->avg, 2) : null,
|
||||
'rating_count' => $stats->cnt ?? 0,
|
||||
]);
|
||||
|
||||
Cache::forget('home.top_rated');
|
||||
Cache::forget('games.top_rated.1');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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', 'created_at'];
|
||||
protected $casts = [
|
||||
'data' => 'array',
|
||||
'read_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\ActivityService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
@@ -23,6 +24,10 @@ class Review extends Model
|
||||
static::deleted(function (Review $review) {
|
||||
$review->game->recalculateRating();
|
||||
});
|
||||
|
||||
static::created(function (Review $review) {
|
||||
ActivityService::log($review->user, 'reviewed_game', $review->game, ['rating' => $review->rating]);
|
||||
});
|
||||
}
|
||||
|
||||
public function user(): BelongsTo { return $this->belongsTo(User::class); }
|
||||
|
||||
@@ -20,6 +20,7 @@ class User extends Authenticatable
|
||||
public function reviews(): HasMany { return $this->hasMany(Review::class); }
|
||||
public function lists(): HasMany { return $this->hasMany(UserList::class); }
|
||||
public function activities(): HasMany { return $this->hasMany(Activity::class)->latest(); }
|
||||
public function notifications(): HasMany { return $this->hasMany(Notification::class)->latest('created_at'); }
|
||||
|
||||
public function following(): BelongsToMany
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
use App\Services\ActivityService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
@@ -10,6 +11,21 @@ class UserGame extends Model
|
||||
|
||||
const STATUSES = ['playing','played','want_to_play','on_hold','dropped'];
|
||||
|
||||
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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function user(): BelongsTo { return $this->belongsTo(User::class); }
|
||||
public function game(): BelongsTo { return $this->belongsTo(Game::class); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user