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:
@@ -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