From 2bb0f98f45d26d837c2eb5dbe598d4a99488f3f7 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Wed, 2 Sep 2026 22:29:10 +0700 Subject: [PATCH] Redesign profile stats strip into a balanced, scannable grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Even 2x2 (mobile) / 1x4 (desktop) grid instead of a ragged flex-wrap where "Gear" dropped to its own row and "Member since" floated awkwardly - Each stat now has an icon badge + prominent count + label, so the number reads as a number (previously the tiny colored count looked like a status dot) - Zero-value stats render in a muted, inert state — a brand-new profile now reads as "nothing yet" rather than a wall of dead grey zeros - Moved "Zaps received" + "Member since" into a clean secondary meta line below, keeping the primary grid focused on the four core contributions Co-authored-by: shakespeare.diy --- src/pages/NIP19Page.tsx | 87 +++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/src/pages/NIP19Page.tsx b/src/pages/NIP19Page.tsx index f12bb99..cfe4ee7 100644 --- a/src/pages/NIP19Page.tsx +++ b/src/pages/NIP19Page.tsx @@ -60,8 +60,9 @@ 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. Clickable when `onClick` is - * provided (jumps to the matching section); shows disabled/inert when empty. */ +/** A stat cell for the at-a-glance grid. Clickable (jumps to its section) when + * it has a non-zero value; shows an inert, muted state at zero so a brand-new + * profile reads as "nothing yet" rather than broken. */ function StatPill({ value, label, @@ -78,37 +79,49 @@ function StatPill({ onClick?: () => void; }) { const interactive = !!onClick && value > 0; + const empty = !loading && value === 0; + const inner = ( -
- {loading ? ( - - ) : ( -

- {icon} - {value} + <> + + {icon} + +

+ {loading ? ( + + ) : ( +

+ {value} +

+ )} +

+ {label}

- )} -

- {label} -

-
+
+ ); - const base = 'flex items-center gap-2.5 rounded-xl border px-3.5 py-2.5 min-w-[84px] transition-all'; + const base = 'group flex items-center gap-3 rounded-2xl border p-3 transition-all'; if (interactive) { return ( ); } - return
{inner}
; + return
{inner}
; } function ProfilePage({ pubkey }: { pubkey: string }) { @@ -323,25 +336,31 @@ 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 && ( - } /> - )} - {memberSince && ( -
- -
-

Member since

-

{memberSince}

-
-
- )} +
+ } loading={spotsLoading} onClick={() => scrollToSection('section-spots')} /> + } loading={shopsLoading} onClick={() => scrollToSection('section-shops')} /> + } loading={crewsLoading} onClick={() => scrollToSection('section-crews')} /> + } loading={publicStashLoading} onClick={() => scrollToSection('section-stash')} />
+ {/* Secondary meta line: zaps received + member since */} + {(memberSince || (riderStats?.zapsReceived ?? 0) > 0) && ( +
+ {(riderStats?.zapsReceived ?? 0) > 0 && ( + + + {riderStats!.zapsReceived} zaps received + + )} + {memberSince && ( + + + Member since {memberSince} + + )} +
+ )} + {/* Profile Info */}
{/* Main Content */}