Add a public profile followers tab.

Expose follower lists on public profiles with a dedicated tab and API path instead of only showing counts.
This commit is contained in:
2026-08-29 12:25:49 +02:00
parent 1d350c9bca
commit bf9ca12469
9 changed files with 606 additions and 47 deletions
+85
View File
@@ -7,6 +7,7 @@ use App\Services\Activity\UserActivityService;
use App\Events\Achievements\AchievementCheckRequested;
use App\Services\FollowAnalyticsService;
use App\Support\AvatarUrl;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
@@ -160,6 +161,38 @@ final class FollowService
->value('followers_count');
}
/**
* People who follow $userId (user_id = the profile owner being followed).
*
* @return LengthAwarePaginator<int, array<string, mixed>>
*/
public function paginatedFollowers(int $userId, int $perPage = 24, int $page = 1): LengthAwarePaginator
{
return $this->paginateFollowRelationship(
ownerColumn: 'uf.user_id',
relatedUserColumn: 'uf.follower_id',
ownerId: $userId,
perPage: $perPage,
page: $page,
);
}
/**
* People $userId follows (follower_id = the profile owner).
*
* @return LengthAwarePaginator<int, array<string, mixed>>
*/
public function paginatedFollowing(int $userId, int $perPage = 24, int $page = 1): LengthAwarePaginator
{
return $this->paginateFollowRelationship(
ownerColumn: 'uf.follower_id',
relatedUserColumn: 'uf.user_id',
ownerId: $userId,
perPage: $perPage,
page: $page,
);
}
public function followingCount(int $userId): int
{
return (int) DB::table('user_statistics')
@@ -216,6 +249,58 @@ final class FollowService
// ─── Private helpers ─────────────────────────────────────────────────────
/**
* @return LengthAwarePaginator<int, array<string, mixed>>
*/
private function paginateFollowRelationship(
string $ownerColumn,
string $relatedUserColumn,
int $ownerId,
int $perPage,
int $page,
): LengthAwarePaginator {
$perPage = max(1, min($perPage, 100));
$page = max(1, $page);
return DB::table('user_followers as uf')
->join('users as u', 'u.id', '=', $relatedUserColumn)
->leftJoin('user_profiles as up', 'up.user_id', '=', 'u.id')
->where($ownerColumn, $ownerId)
->whereNull('u.deleted_at')
->orderByDesc('uf.created_at')
->select([
'u.id',
'u.username',
'u.name',
'u.level',
'u.rank',
'up.avatar_hash',
'uf.created_at as followed_at',
])
->paginate($perPage, ['*'], 'page', $page)
->through(fn ($row) => $this->mapPublicFollowUser($row));
}
/**
* @return array<string, mixed>
*/
private function mapPublicFollowUser(object $row): array
{
$username = (string) ($row->username ?? '');
return [
'id' => (int) $row->id,
'username' => $username,
'name' => (string) ($row->name ?? $username),
'display_name' => $username !== '' ? $username : (string) ($row->name ?? ''),
'avatar_url' => AvatarUrl::forUser((int) $row->id, $row->avatar_hash ?? null, 50),
'profile_url' => '/@' . strtolower($username !== '' ? $username : (string) $row->id),
'followed_at' => $row->followed_at,
'level' => (int) ($row->level ?? 1),
'rank' => (string) ($row->rank ?? 'Newbie'),
];
}
private function incrementCounter(int $userId, string $column): void
{
DB::table('user_statistics')->updateOrInsert(