Redesign profile stats strip into a balanced, scannable grid

- 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 <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-02 22:29:10 +07:00
parent 9958020776
commit 2bb0f98f45

View file

@ -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 = (
<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}
<>
<span
className="flex-shrink-0 h-9 w-9 rounded-xl flex items-center justify-center transition-colors"
style={{
backgroundColor: empty ? 'rgba(255,255,255,0.05)' : `${color}22`,
color: empty ? '#6b7280' : color,
}}
>
{icon}
</span>
<div className="leading-none text-left min-w-0">
{loading ? (
<Skeleton className="h-6 w-8" />
) : (
<p className="text-xl sm:text-2xl font-black" style={{ color: empty ? '#6b7280' : color }}>
{value}
</p>
)}
<p className={`text-[10px] uppercase tracking-wide font-bold mt-0.5 transition-colors ${empty ? 'text-gray-600' : 'text-gray-400 group-hover:text-gray-200'}`}>
{label}
</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>
</div>
</>
);
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 (
<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`}
className={`${base} border-white/10 bg-white/[0.04] 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>;
return <div className={`${base} border-white/5 bg-white/[0.02]`}>{inner}</div>;
}
function ProfilePage({ pubkey }: { pubkey: string }) {
@ -323,25 +336,31 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
</div>
{/* ── 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} 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" />} />
)}
{memberSince && (
<div className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3.5 py-2.5 ml-auto">
<Clock className="h-4 w-4 text-gray-500 flex-shrink-0" />
<div className="leading-tight">
<p className="text-[10px] uppercase tracking-wide text-gray-500 font-bold">Member since</p>
<p className="text-sm font-black text-white">{memberSince}</p>
</div>
</div>
)}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3 mb-3">
<StatPill value={spots?.length ?? 0} label="Spots" color="#ff0040" icon={<MapPin className="h-4 w-4" />} loading={spotsLoading} onClick={() => scrollToSection('section-spots')} />
<StatPill value={shops?.length ?? 0} label="Shops" color="#ccff00" icon={<Store className="h-4 w-4" />} loading={shopsLoading} onClick={() => scrollToSection('section-shops')} />
<StatPill value={crews.length} label="Crews" color="#2dd4bf" icon={<Users className="h-4 w-4" />} loading={crewsLoading} onClick={() => scrollToSection('section-crews')} />
<StatPill value={publicStash?.length ?? 0} label="Gear" color="#ff6600" icon={<Package className="h-4 w-4" />} loading={publicStashLoading} onClick={() => scrollToSection('section-stash')} />
</div>
{/* Secondary meta line: zaps received + member since */}
{(memberSince || (riderStats?.zapsReceived ?? 0) > 0) && (
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 mb-6 px-1 text-xs text-gray-500">
{(riderStats?.zapsReceived ?? 0) > 0 && (
<span className="flex items-center gap-1.5">
<Zap className="h-3.5 w-3.5 text-yellow-400" />
<span className="font-black text-white">{riderStats!.zapsReceived}</span> zaps received
</span>
)}
{memberSince && (
<span className="flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5" />
Member since <span className="font-bold text-gray-300">{memberSince}</span>
</span>
)}
</div>
)}
{/* Profile Info */}
<div className="grid lg:grid-cols-3 gap-6">
{/* Main Content */}