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:
parent
9958020776
commit
2bb0f98f45
1 changed files with 53 additions and 34 deletions
|
|
@ -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 */}
|
||||
|
|
|
|||
Loading…
Reference in a new issue