Make profile stat pills clickable — jump to their section

- StatPill now accepts an onClick and renders as a hover-lift button when it has
  a non-zero value (inert div when empty), with clear affordance
- Spots / Shops / Crews / Gear pills smooth-scroll to their matching section
  (added ids + scroll-mt-24 offset so headings aren't hidden under sticky nav)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-02 22:23:22 +07:00
parent 8ba54273b7
commit 9958020776

View file

@ -60,35 +60,55 @@ function profileEventCreatedAt(createdAt: number | undefined): string | undefine
return new Date(createdAt * 1000).toLocaleDateString(undefined, { year: 'numeric', month: 'short' });
}
/** A compact stat pill for the at-a-glance strip. */
/** A compact stat pill for the at-a-glance strip. Clickable when `onClick` is
* provided (jumps to the matching section); shows disabled/inert when empty. */
function StatPill({
value,
label,
color,
loading,
icon,
onClick,
}: {
value: number;
label: string;
color: string;
loading?: boolean;
icon?: ReactNode;
onClick?: () => void;
}) {
return (
<div className="flex items-center gap-2.5 rounded-xl border border-white/10 bg-white/5 px-3.5 py-2.5 min-w-[84px]">
<div className="leading-none">
{loading ? (
<Skeleton className="h-6 w-8" />
) : (
<p className="text-xl sm:text-2xl font-black flex items-center gap-1" style={{ color }}>
{icon}
{value}
</p>
)}
<p className="text-[10px] uppercase tracking-wide text-gray-500 font-bold mt-0.5">{label}</p>
</div>
const interactive = !!onClick && value > 0;
const inner = (
<div className="leading-none text-left">
{loading ? (
<Skeleton className="h-6 w-8" />
) : (
<p className="text-xl sm:text-2xl font-black flex items-center gap-1" style={{ color }}>
{icon}
{value}
</p>
)}
<p className="text-[10px] uppercase tracking-wide text-gray-500 font-bold mt-0.5 group-hover:text-gray-300 transition-colors">
{label}
</p>
</div>
);
const base = 'flex items-center gap-2.5 rounded-xl border px-3.5 py-2.5 min-w-[84px] transition-all';
if (interactive) {
return (
<button
type="button"
onClick={onClick}
className={`group ${base} border-white/10 bg-white/5 hover:bg-white/10 hover:border-white/25 hover:-translate-y-0.5 cursor-pointer`}
>
{inner}
</button>
);
}
return <div className={`group ${base} border-white/10 bg-white/5`}>{inner}</div>;
}
function ProfilePage({ pubkey }: { pubkey: string }) {
@ -150,6 +170,12 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
setTimeout(() => setCopied(false), 2000);
};
// Smooth-scroll a stat pill to its matching section on the page.
const scrollToSection = (id: string) => {
const el = document.getElementById(id);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
// Create a mock event for zapping
const profileEvent = author.data?.event;
@ -298,10 +324,10 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
{/* ── At-a-glance stats strip ── */}
<div className="flex flex-wrap items-stretch gap-2 sm:gap-3 mb-6">
<StatPill value={spots?.length ?? 0} label="Spots" color="#ff0040" loading={spotsLoading} />
<StatPill value={shops?.length ?? 0} label="Shops" color="#ccff00" loading={shopsLoading} />
<StatPill value={crews.length} label="Crews" color="#2dd4bf" loading={crewsLoading} />
<StatPill value={publicStash?.length ?? 0} label="Gear" color="#ff6600" loading={publicStashLoading} />
<StatPill value={spots?.length ?? 0} label="Spots" color="#ff0040" loading={spotsLoading} onClick={() => scrollToSection('section-spots')} />
<StatPill value={shops?.length ?? 0} label="Shops" color="#ccff00" loading={shopsLoading} onClick={() => scrollToSection('section-shops')} />
<StatPill value={crews.length} label="Crews" color="#2dd4bf" loading={crewsLoading} onClick={() => scrollToSection('section-crews')} />
<StatPill value={publicStash?.length ?? 0} label="Gear" color="#ff6600" loading={publicStashLoading} onClick={() => scrollToSection('section-stash')} />
{(riderStats?.zapsReceived ?? 0) > 0 && (
<StatPill value={riderStats!.zapsReceived} label="Zaps" color="#facc15" icon={<Zap className="h-3.5 w-3.5" />} />
)}
@ -459,7 +485,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
{/* Spots by this user — hidden entirely for others when empty */}
{(spotsLoading || (spots && spots.length > 0) || isOwnProfile) && (
<div>
<div id="section-spots" className="scroll-mt-24">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-black uppercase flex items-center gap-2">
<MapPin className="h-5 w-5 text-[#ff0040]" />
@ -502,7 +528,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
{/* Shops by this user — only render when there's something, or it's you */}
{(shopsLoading || (shops && shops.length > 0)) && (
<div>
<div id="section-shops" className="scroll-mt-24">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-black uppercase flex items-center gap-2">
<Store className="h-5 w-5 text-[#ccff00]" />
@ -547,7 +573,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
{/* Public Stash — gear this skater chose to show publicly */}
{(publicStashLoading || (publicStash && publicStash.length > 0)) && (
<div>
<div id="section-stash" className="scroll-mt-24">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-black uppercase flex items-center gap-2">
<Package className="h-5 w-5 text-[#ff6600]" />
@ -763,7 +789,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
{/* Crews Card */}
{(crewsLoading || crews.length > 0) && (
<Card className="bg-black/90 border-2 border-teal-500/30">
<Card id="section-crews" className="scroll-mt-24 bg-black/90 border-2 border-teal-500/30">
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<Users className="h-5 w-5 text-teal-400" />