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:
parent
8ba54273b7
commit
9958020776
1 changed files with 48 additions and 22 deletions
|
|
@ -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" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue