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:
@@ -13,9 +13,10 @@ import TabActivity from '../../components/profile/tabs/TabActivity'
|
||||
import TabPosts from '../../components/profile/tabs/TabPosts'
|
||||
import TabStories from '../../components/profile/tabs/TabStories'
|
||||
import TabWorlds from '../../components/profile/tabs/TabWorlds'
|
||||
import TabFollowers from '../../components/profile/tabs/TabFollowers'
|
||||
import GroupProfileSummary from '../../components/groups/GroupProfileSummary'
|
||||
|
||||
const VALID_TABS = ['posts', 'artworks', 'stories', 'achievements', 'worlds', 'collections', 'about', 'stats', 'favourites', 'activity']
|
||||
const VALID_TABS = ['posts', 'artworks', 'stories', 'achievements', 'worlds', 'collections', 'about', 'stats', 'favourites', 'activity', 'followers']
|
||||
|
||||
function getInitialTab(initialTab = 'posts') {
|
||||
if (typeof window === 'undefined') {
|
||||
@@ -55,6 +56,7 @@ export default function ProfileShow() {
|
||||
socialLinks,
|
||||
followerCount,
|
||||
recentFollowers,
|
||||
followersListing,
|
||||
followContext,
|
||||
followAnalytics,
|
||||
suggestedUsers,
|
||||
@@ -87,6 +89,15 @@ export default function ProfileShow() {
|
||||
|
||||
const handleTabChange = useCallback((tab) => {
|
||||
if (!VALID_TABS.includes(tab)) return
|
||||
|
||||
// Followers listing is loaded only on the followers tab GET. A client-only
|
||||
// pushState would render an empty list, so navigate for a real page load.
|
||||
if (tab === 'followers') {
|
||||
const targetBase = profileTabUrls?.followers || `${profileUrl || ''}/followers`
|
||||
window.location.assign(targetBase)
|
||||
return
|
||||
}
|
||||
|
||||
setActiveTab(tab)
|
||||
|
||||
try {
|
||||
@@ -274,6 +285,13 @@ export default function ProfileShow() {
|
||||
creatorStories={creatorStories}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'followers' && (
|
||||
<TabFollowers
|
||||
listing={followersListing}
|
||||
username={user.username || user.name}
|
||||
followerCount={followerCount}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -11,6 +11,7 @@ export const TABS = [
|
||||
{ id: 'stats', label: 'Stats', icon: 'fa-chart-bar' },
|
||||
{ id: 'favourites', label: 'Favourites', icon: 'fa-heart' },
|
||||
{ id: 'activity', label: 'Activity', icon: 'fa-bolt' },
|
||||
{ id: 'followers', label: 'Followers', icon: 'fa-users' },
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from 'react'
|
||||
|
||||
function formatFollowedAt(value) {
|
||||
if (!value) return null
|
||||
|
||||
try {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return null
|
||||
|
||||
return date.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default function TabFollowers({ listing, username, followerCount }) {
|
||||
const items = Array.isArray(listing?.data) ? listing.data : []
|
||||
const total = Number(listing?.total ?? followerCount ?? 0)
|
||||
const currentPage = Number(listing?.current_page ?? 1)
|
||||
const lastPage = Number(listing?.last_page ?? 1)
|
||||
const prevUrl = listing?.prev_page_url ?? null
|
||||
const nextUrl = listing?.next_page_url ?? null
|
||||
|
||||
return (
|
||||
<div
|
||||
id="tabpanel-followers"
|
||||
role="tabpanel"
|
||||
aria-labelledby="tab-followers"
|
||||
className="pt-6"
|
||||
>
|
||||
<h2 className="mb-4 flex items-center gap-2 text-xs font-semibold uppercase tracking-widest text-slate-500">
|
||||
<i className="fa-solid fa-users text-sky-400 fa-fw" />
|
||||
Followers
|
||||
<span className="rounded bg-white/5 px-1.5 py-0.5 text-[10px] font-medium normal-case tracking-normal text-slate-400">
|
||||
{total.toLocaleString()}
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<p className="rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-8 text-center text-sm text-slate-400">
|
||||
{username ? `${username} has no public followers yet.` : 'No public followers yet.'}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="grid gap-3 sm:grid-cols-2">
|
||||
{items.map((follower) => {
|
||||
const handle = follower.username || follower.display_name || follower.name
|
||||
const followedAt = formatFollowedAt(follower.followed_at)
|
||||
|
||||
return (
|
||||
<li key={follower.id}>
|
||||
<a
|
||||
href={follower.profile_url}
|
||||
className="flex items-center gap-3 rounded-2xl border border-white/8 bg-white/[0.03] px-3 py-3 transition-colors hover:border-sky-300/25 hover:bg-white/[0.06]"
|
||||
>
|
||||
{follower.avatar_url ? (
|
||||
<img
|
||||
src={follower.avatar_url}
|
||||
alt=""
|
||||
className="h-12 w-12 rounded-xl object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-xl bg-white/5 text-slate-500">
|
||||
<i className="fa-solid fa-user" />
|
||||
</span>
|
||||
)}
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-sm font-semibold text-white">
|
||||
{handle}
|
||||
</span>
|
||||
{follower.username ? (
|
||||
<span className="block truncate text-xs text-slate-400">@{follower.username}</span>
|
||||
) : null}
|
||||
<span className="mt-1 block text-[11px] text-slate-500">
|
||||
{[follower.rank, followedAt ? `Followed ${followedAt}` : null]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{lastPage > 1 ? (
|
||||
<nav className="mt-6 flex items-center justify-center gap-3 text-sm" aria-label="Followers pagination">
|
||||
{prevUrl ? (
|
||||
<a href={prevUrl} className="rounded-xl border border-white/10 px-3 py-2 text-slate-300 hover:bg-white/5">
|
||||
Previous
|
||||
</a>
|
||||
) : (
|
||||
<span className="rounded-xl border border-white/5 px-3 py-2 text-slate-600">Previous</span>
|
||||
)}
|
||||
<span className="text-slate-500">
|
||||
Page {currentPage} of {lastPage}
|
||||
</span>
|
||||
{nextUrl ? (
|
||||
<a href={nextUrl} className="rounded-xl border border-white/10 px-3 py-2 text-slate-300 hover:bg-white/5">
|
||||
Next
|
||||
</a>
|
||||
) : (
|
||||
<span className="rounded-xl border border-white/5 px-3 py-2 text-slate-600">Next</span>
|
||||
)}
|
||||
</nav>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user