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
@@ -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>
)
}