Files
SkinbaseNova/app/Services/Studio/CreatorStudioFollowersService.php

115 lines
5.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Studio;
use App\Models\User;
use App\Support\AvatarUrl;
use Illuminate\Support\Facades\DB;
final class CreatorStudioFollowersService
{
public function list(User $user, array $filters = []): array
{
$perPage = 30;
$search = trim((string) ($filters['q'] ?? ''));
$sort = (string) ($filters['sort'] ?? 'recent');
$relationship = (string) ($filters['relationship'] ?? 'all');
$page = max(1, (int) ($filters['page'] ?? 1));
$allowedSorts = ['recent', 'oldest', 'name', 'uploads', 'followers'];
$allowedRelationships = ['all', 'following-back', 'not-followed'];
if (! in_array($sort, $allowedSorts, true)) {
$sort = 'recent';
}
if (! in_array($relationship, $allowedRelationships, true)) {
$relationship = 'all';
}
$baseQuery = DB::table('user_followers as uf')
->join('users as u', 'u.id', '=', 'uf.follower_id')
->leftJoin('user_profiles as up', 'up.user_id', '=', 'u.id')
->leftJoin('user_statistics as us', 'us.user_id', '=', 'u.id')
->leftJoin('user_followers as mutual', function ($join) use ($user): void {
$join->on('mutual.user_id', '=', 'uf.follower_id')
->where('mutual.follower_id', '=', $user->id);
})
->where('uf.user_id', $user->id)
->whereNull('u.deleted_at')
->when($search !== '', function ($query) use ($search): void {
$query->where(function ($inner) use ($search): void {
$inner->where('u.username', 'like', '%' . $search . '%')
->orWhere('u.name', 'like', '%' . $search . '%');
});
})
->when($relationship === 'following-back', fn ($query) => $query->whereNotNull('mutual.created_at'))
->when($relationship === 'not-followed', fn ($query) => $query->whereNull('mutual.created_at'));
$summaryBaseQuery = clone $baseQuery;
$followers = $baseQuery
->when($sort === 'recent', fn ($query) => $query->orderByDesc('uf.created_at'))
->when($sort === 'oldest', fn ($query) => $query->orderBy('uf.created_at'))
->when($sort === 'name', fn ($query) => $query->orderByRaw('COALESCE(u.username, u.name) asc'))
->when($sort === 'uploads', fn ($query) => $query->orderByDesc('us.uploads_count')->orderByRaw('COALESCE(u.username, u.name) asc'))
->when($sort === 'followers', fn ($query) => $query->orderByDesc('us.followers_count')->orderByRaw('COALESCE(u.username, u.name) asc'))
->select([
'u.id',
'u.username',
'u.name',
'up.avatar_hash',
'us.uploads_count',
'us.followers_count',
'uf.created_at as followed_at',
'mutual.created_at as followed_back_at',
])
->paginate($perPage, ['*'], 'page', $page)
->withQueryString();
return [
'items' => collect($followers->items())->map(fn ($row): array => [
'id' => (int) $row->id,
'name' => $row->name ?: '@' . $row->username,
'username' => $row->username,
'avatar_url' => AvatarUrl::forUser((int) $row->id, $row->avatar_hash, 64),
'profile_url' => '/@' . strtolower((string) ($row->username ?? $row->id)),
'uploads_count' => (int) ($row->uploads_count ?? 0),
'followers_count' => (int) ($row->followers_count ?? 0),
'is_following_back' => $row->followed_back_at !== null,
'followed_back_at' => $row->followed_back_at,
'followed_at' => $row->followed_at,
])->values()->all(),
'meta' => [
'current_page' => $followers->currentPage(),
'last_page' => $followers->lastPage(),
'per_page' => $followers->perPage(),
'total' => $followers->total(),
],
'filters' => [
'q' => $search,
'sort' => $sort,
'relationship' => $relationship,
],
'summary' => [
'total_followers' => (clone $summaryBaseQuery)->count(),
'following_back' => (clone $summaryBaseQuery)->whereNotNull('mutual.created_at')->count(),
'not_followed' => (clone $summaryBaseQuery)->whereNull('mutual.created_at')->count(),
],
'sort_options' => [
['value' => 'recent', 'label' => 'Most recent'],
['value' => 'oldest', 'label' => 'Oldest first'],
['value' => 'name', 'label' => 'Name A-Z'],
['value' => 'uploads', 'label' => 'Most uploads'],
['value' => 'followers', 'label' => 'Most followers'],
],
'relationship_options' => [
['value' => 'all', 'label' => 'All followers'],
['value' => 'following-back', 'label' => 'Following back'],
['value' => 'not-followed', 'label' => 'Not followed yet'],
],
];
}
}