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' });
|
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
|
/** A stat cell for the at-a-glance grid. Clickable (jumps to its section) when
|
||||||
* provided (jumps to the matching section); shows disabled/inert when empty. */
|
* 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({
|
function StatPill({
|
||||||
value,
|
value,
|
||||||
label,
|
label,
|
||||||
|
|
@ -78,37 +79,49 @@ function StatPill({
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const interactive = !!onClick && value > 0;
|
const interactive = !!onClick && value > 0;
|
||||||
|
const empty = !loading && value === 0;
|
||||||
|
|
||||||
const inner = (
|
const inner = (
|
||||||
<div className="leading-none text-left">
|
<>
|
||||||
|
<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 ? (
|
{loading ? (
|
||||||
<Skeleton className="h-6 w-8" />
|
<Skeleton className="h-6 w-8" />
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xl sm:text-2xl font-black flex items-center gap-1" style={{ color }}>
|
<p className="text-xl sm:text-2xl font-black" style={{ color: empty ? '#6b7280' : color }}>
|
||||||
{icon}
|
|
||||||
{value}
|
{value}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-[10px] uppercase tracking-wide text-gray-500 font-bold mt-0.5 group-hover:text-gray-300 transition-colors">
|
<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}
|
{label}
|
||||||
</p>
|
</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) {
|
if (interactive) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
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}
|
{inner}
|
||||||
</button>
|
</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 }) {
|
function ProfilePage({ pubkey }: { pubkey: string }) {
|
||||||
|
|
@ -323,24 +336,30 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── At-a-glance stats strip ── */}
|
{/* ── At-a-glance stats strip ── */}
|
||||||
<div className="flex flex-wrap items-stretch gap-2 sm:gap-3 mb-6">
|
<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" loading={spotsLoading} onClick={() => scrollToSection('section-spots')} />
|
<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" loading={shopsLoading} onClick={() => scrollToSection('section-shops')} />
|
<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" loading={crewsLoading} onClick={() => scrollToSection('section-crews')} />
|
<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" loading={publicStashLoading} onClick={() => scrollToSection('section-stash')} />
|
<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 && (
|
{(riderStats?.zapsReceived ?? 0) > 0 && (
|
||||||
<StatPill value={riderStats!.zapsReceived} label="Zaps" color="#facc15" icon={<Zap className="h-3.5 w-3.5" />} />
|
<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 && (
|
{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">
|
<span className="flex items-center gap-1.5">
|
||||||
<Clock className="h-4 w-4 text-gray-500 flex-shrink-0" />
|
<Clock className="h-3.5 w-3.5" />
|
||||||
<div className="leading-tight">
|
Member since <span className="font-bold text-gray-300">{memberSince}</span>
|
||||||
<p className="text-[10px] uppercase tracking-wide text-gray-500 font-bold">Member since</p>
|
</span>
|
||||||
<p className="text-sm font-black text-white">{memberSince}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Profile Info */}
|
{/* Profile Info */}
|
||||||
<div className="grid lg:grid-cols-3 gap-6">
|
<div className="grid lg:grid-cols-3 gap-6">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue