From 99580207769acb6026dac9865439f35e480c5c59 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Wed, 2 Sep 2026 22:23:22 +0700 Subject: [PATCH] =?UTF-8?q?Make=20profile=20stat=20pills=20clickable=20?= =?UTF-8?q?=E2=80=94=20jump=20to=20their=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/pages/NIP19Page.tsx | 70 ++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/src/pages/NIP19Page.tsx b/src/pages/NIP19Page.tsx index e68036d..f12bb99 100644 --- a/src/pages/NIP19Page.tsx +++ b/src/pages/NIP19Page.tsx @@ -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 ( -
-
- {loading ? ( - - ) : ( -

- {icon} - {value} -

- )} -

{label}

-
+ const interactive = !!onClick && value > 0; + const inner = ( +
+ {loading ? ( + + ) : ( +

+ {icon} + {value} +

+ )} +

+ {label} +

); + + 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 ( + + ); + } + + return
{inner}
; } 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 ── */}
- - - - + scrollToSection('section-spots')} /> + scrollToSection('section-shops')} /> + scrollToSection('section-crews')} /> + scrollToSection('section-stash')} /> {(riderStats?.zapsReceived ?? 0) > 0 && ( } /> )} @@ -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) && ( -
+

@@ -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)) && ( -
+

@@ -547,7 +573,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) { {/* Public Stash — gear this skater chose to show publicly */} {(publicStashLoading || (publicStash && publicStash.length > 0)) && ( -
+

@@ -763,7 +789,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) { {/* Crews Card */} {(crewsLoading || crews.length > 0) && ( - +