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
@@ -74,6 +74,7 @@ class ProfileController extends Controller
'stats',
'favourites',
'activity',
'followers',
];
public function __construct(
@@ -200,6 +201,28 @@ class ProfileController extends Controller
return redirect()->route('profile.show', ['username' => UsernamePolicy::normalize($username)], 301);
}
/**
* Legacy "All followers" URL from old profile templates:
* GET /following/{id}/{slug?} → /@{canonical-username}/followers
*/
public function legacyFollowingById(Request $request, int $id, ?string $slug = null)
{
$user = User::query()->find($id);
if (! $user) {
abort(404);
}
$url = url('/@' . strtolower((string) $user->username) . '/followers');
$query = $request->query();
if ($query !== []) {
$url .= '?' . http_build_query($query);
}
return redirect()->to($url, 301);
}
/** Toggle follow/unfollow for the profile of $username (auth required). */
public function toggleFollow(Request $request, string $username): JsonResponse
{
@@ -1293,9 +1316,37 @@ class ProfileController extends Controller
? ($profileTabUrls[$resolvedInitialTab] ?? $canonical)
: $canonical;
$tabMetaLabel = $resolvedInitialTab !== null
? ucfirst($resolvedInitialTab)
? ($resolvedInitialTab === 'followers' ? 'Followers' : ucfirst($resolvedInitialTab))
: null;
$followersListing = null;
if (! $galleryOnly && $resolvedInitialTab === 'followers') {
$followersPage = max(1, (int) $request->query('page', 1));
$followersListing = [
'data' => [],
'current_page' => $followersPage,
'last_page' => 1,
'per_page' => 24,
'total' => 0,
'next_page_url' => null,
'prev_page_url' => null,
];
if (Schema::hasTable('user_followers')) {
$followersPaginator = $this->followService->paginatedFollowers((int) $user->id, 24, $followersPage);
$followersListing = [
'data' => array_values($followersPaginator->items()),
'current_page' => $followersPaginator->currentPage(),
'last_page' => $followersPaginator->lastPage(),
'per_page' => $followersPaginator->perPage(),
'total' => $followersPaginator->total(),
'next_page_url' => $followersPaginator->nextPageUrl(),
'prev_page_url' => $followersPaginator->previousPageUrl(),
];
}
}
$pageTitle = $galleryOnly
? (($user->username ?? $user->name ?? 'User') . ' Gallery on Skinbase')
: ($isTabLanding
@@ -1303,9 +1354,11 @@ class ProfileController extends Controller
: (($user->username ?? $user->name ?? 'User') . ' on Skinbase'));
$pageDescription = $galleryOnly
? ('Browse the public gallery of ' . ($user->username ?? $user->name) . ' on Skinbase.')
: ($isTabLanding
? ('Explore the ' . strtolower((string) $tabMetaLabel) . ' section for ' . ($user->username ?? $user->name) . ' on Skinbase.')
: ('View the profile of ' . ($user->username ?? $user->name) . ' on Skinbase — artworks, favourites and more.'));
: ($resolvedInitialTab === 'followers'
? ('People who follow ' . ($user->username ?? $user->name) . ' on Skinbase.')
: ($isTabLanding
? ('Explore the ' . strtolower((string) $tabMetaLabel) . ' section for ' . ($user->username ?? $user->name) . ' on Skinbase.')
: ('View the profile of ' . ($user->username ?? $user->name) . ' on Skinbase — artworks, favourites and more.')));
$profileSeo = app(SeoFactory::class)->profilePage(
$pageTitle,
$galleryOnly ? $galleryUrl : $activeProfileUrl,
@@ -1350,6 +1403,7 @@ class ProfileController extends Controller
'socialLinks' => $socialLinks,
'followerCount' => $followerCount,
'recentFollowers' => $recentFollowers->values(),
'followersListing' => $followersListing,
'followContext' => $followContext,
'followAnalytics' => $followAnalytics,
'suggestedUsers' => $suggestedUsers,