athlete page layout update

This commit is contained in:
2026-04-08 12:16:18 -04:00
parent 6e88a2a19a
commit cda94b7621

View File

@@ -6,89 +6,278 @@ import Avatar from '../components/Avatar.jsx';
import SportBadge from '../components/SportBadge.jsx'; import SportBadge from '../components/SportBadge.jsx';
import { fmtName } from '../utils/formatName.js'; import { fmtName } from '../utils/formatName.js';
const SOCIAL_ICONS = { facebook: '📘', instagram: '📷', bluesky: '🦋', snapchat: '👻' };
// Pick the most display-worthy highlight stats for the top strip.
// Skips 0-value stats so irrelevant positional stats don't show dashes.
function getHighlights(statDefs, stats, max = 6) {
return statDefs
.filter(d => d.type !== 'text' && stats[d.key] && Number(stats[d.key]) !== 0)
.slice(0, max);
}
export default function AthleteDetail() { export default function AthleteDetail() {
const { id } = useParams(); const { id } = useParams();
const { getUserById, deleteUser, auth } = useStore(); const { getUserById, deleteUser, auth } = useStore();
const navigate = useNavigate(); const navigate = useNavigate();
const isLoggedIn = auth?.isLoggedIn; const isLoggedIn = auth?.isLoggedIn;
const [activeSport, setActiveSport] = useState(null); const [activeSport, setActiveSport] = useState(null);
const [activeTab, setActiveTab] = useState('stats');
const user = getUserById(id); const user = getUserById(id);
if (!user) return ( if (!user) return (
<div className="page" style={{ textAlign: 'center', paddingTop: 80 }}> <div className="page" style={{ textAlign: 'center', paddingTop: 80 }}>
<div style={{ fontSize: 48, marginBottom: 16 }}>🏟</div> <div style={{ fontSize: 64, marginBottom: 16 }}>🏟</div>
<div style={{ color: 'var(--text-muted)' }}>Athlete not found</div> <h2 style={{ fontFamily: 'var(--font-display)', fontSize: 24, fontWeight: 700, marginBottom: 8 }}>Athlete not found</h2>
<Link to="/athletes" className="btn btn-secondary" style={{ marginTop: 16 }}>Back to Athletes</Link> <Link to="/athletes" className="btn btn-secondary" style={{ marginTop: 8 }}> Back to Athletes</Link>
</div> </div>
); );
const displaySport = activeSport || user.primarySport || user.sports?.[0]; const displaySport = activeSport || user.primarySport || user.sports?.[0];
const sportMeta = SPORTS[displaySport];
const sportColor = sportMeta?.color || 'var(--accent)';
const stats = user.sportStats?.[displaySport] || {}; const stats = user.sportStats?.[displaySport] || {};
const statDefs = SPORT_STATS_DEFS[displaySport] || []; const statDefs = SPORT_STATS_DEFS[displaySport] || [];
const numericDefs = statDefs.filter(d => d.type !== 'text');
const position = stats.position;
const highlights = getHighlights(statDefs, stats);
const tabs = ['stats', 'biometrics', ...(isLoggedIn ? ['contact'] : [])];
const tabLabels = { stats: 'Stats', biometrics: 'Biometrics', contact: 'Contact' };
function handleDelete() { function handleDelete() {
if (confirm(`Delete ${user.name}?`)) { deleteUser(id); navigate('/athletes'); } if (confirm(`Delete ${user.name}?`)) { deleteUser(id); navigate('/athletes'); }
} }
const socialIcons = { facebook: '📘', instagram: '📷', bluesky: '🦋', snapchat: '👻' }; function fmtStat(val, type) {
if (val === undefined || val === null || val === 0 || val === '') return '—';
if (type === 'decimal') return Number(val).toFixed(2);
return val;
}
return ( return (
<div className="page"> <div className="page">
{/* Header card */}
<div className="card" style={{ marginBottom: 16, background: 'linear-gradient(135deg, var(--bg-card) 0%, var(--bg-card2) 100%)', borderColor: 'var(--border-bright)' }}> {/* ── Hero ─────────────────────────────────────────────────── */}
<div style={{ display: 'flex', gap: 20, alignItems: 'flex-start', flexWrap: 'wrap' }}> <div style={{
<Avatar user={user} size={80} /> marginBottom: 16,
<div style={{ flex: 1 }}> borderRadius: 'var(--radius)',
<h1 style={{ fontFamily: 'var(--font-display)', fontSize: 32, fontWeight: 900, letterSpacing: '0.02em', marginBottom: 4 }}> overflow: 'hidden',
border: '1px solid var(--border-bright)',
}}>
{/* Color accent bar across the top */}
<div style={{ height: 6, background: `linear-gradient(90deg, ${sportColor}, ${sportColor}88)` }} />
<div style={{
background: `linear-gradient(135deg, var(--bg-card) 0%, var(--bg-card2) 60%, ${sportColor}12 100%)`,
padding: 24,
}}>
<div style={{ display: 'flex', gap: 24, alignItems: 'flex-start', flexWrap: 'wrap' }}>
{/* Avatar */}
<div style={{ flexShrink: 0 }}>
<Avatar user={user} size={110} />
</div>
{/* Player info */}
<div style={{ flex: 1, minWidth: 200 }}>
{/* Position chip */}
{position && (
<div style={{
display: 'inline-block', marginBottom: 8,
fontFamily: 'var(--font-display)', fontSize: 11, fontWeight: 800,
letterSpacing: '0.12em', textTransform: 'uppercase',
background: `${sportColor}22`, color: sportColor,
border: `1px solid ${sportColor}55`,
padding: '3px 10px', borderRadius: 20,
}}>
{position}
</div>
)}
{/* Name */}
<h1 style={{
fontFamily: 'var(--font-display)', fontSize: 'clamp(26px, 5vw, 42px)',
fontWeight: 900, letterSpacing: '0.01em', lineHeight: 1.05,
marginBottom: 10,
}}>
{isLoggedIn ? user.name : fmtName(user)} {isLoggedIn ? user.name : fmtName(user)}
</h1> </h1>
<div style={{ color: 'var(--text-secondary)', marginBottom: 10 }}>{user.city}, {user.country} · Joined {user.joinDate}</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 10 }}> {/* Sport badges */}
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
{user.sports?.map(s => <SportBadge key={s} sport={s} />)} {user.sports?.map(s => <SportBadge key={s} sport={s} />)}
</div> </div>
{user.bio && <p style={{ color: 'var(--text-secondary)', fontSize: 14, fontStyle: 'italic', maxWidth: 500 }}>"{user.bio}"</p>}
{/* Location + join date */}
<div style={{ color: 'var(--text-muted)', fontSize: 13, marginBottom: 10 }}>
{[user.city, user.country].filter(Boolean).join(', ')}
{user.joinDate && <span> · Member since {user.joinDate}</span>}
</div> </div>
<div style={{ display: 'flex', gap: 8 }}>
{/* Bio */}
{user.bio && (
<p style={{ color: 'var(--text-secondary)', fontSize: 14, fontStyle: 'italic', maxWidth: 520, margin: 0, lineHeight: 1.5 }}>
"{user.bio}"
</p>
)}
</div>
{/* Action buttons */}
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
<Link to={`/register?edit=${id}`} className="btn btn-secondary" style={{ padding: '8px 14px', fontSize: 12 }}>Edit</Link> <Link to={`/register?edit=${id}`} className="btn btn-secondary" style={{ padding: '8px 14px', fontSize: 12 }}>Edit</Link>
<button className="btn btn-danger" style={{ padding: '8px 14px', fontSize: 12 }} onClick={handleDelete}>Delete</button> <button className="btn btn-danger" style={{ padding: '8px 14px', fontSize: 12 }} onClick={handleDelete}>Delete</button>
</div> </div>
</div> </div>
</div>
</div>
{/* Social media — logged-in only */} {/* ── Key stats highlight strip ────────────────────────────── */}
{isLoggedIn && user.socials && Object.values(user.socials).some(Boolean) && ( {highlights.length > 0 && (
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--border)', display: 'flex', gap: 14, flexWrap: 'wrap' }}> <div style={{
{Object.entries(user.socials).filter(([, handle]) => handle).map(([platform, handle]) => ( display: 'grid',
<div key={platform} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: 'var(--text-secondary)' }}> gridTemplateColumns: `repeat(auto-fit, minmax(90px, 1fr))`,
<span>{socialIcons[platform] || '🔗'}</span> gap: 1,
<span>{handle}</span> background: 'var(--border)',
border: '1px solid var(--border-bright)',
borderRadius: 'var(--radius)',
overflow: 'hidden',
marginBottom: 16,
}}>
{highlights.map(def => (
<div key={def.key} style={{
background: 'var(--bg-card)',
padding: '18px 12px',
textAlign: 'center',
}}>
<div style={{
fontFamily: 'var(--font-display)', fontWeight: 900,
fontSize: 'clamp(22px, 3vw, 32px)',
color: sportColor,
lineHeight: 1,
}}>
{def.type === 'decimal' ? Number(stats[def.key]).toFixed(2) : stats[def.key]}
</div>
<div style={{
fontSize: 10, fontFamily: 'var(--font-display)', fontWeight: 700,
letterSpacing: '0.1em', textTransform: 'uppercase',
color: 'var(--text-muted)', marginTop: 6,
}}>
{def.label}
</div>
</div> </div>
))} ))}
</div> </div>
)} )}
</div>
{/* Contact — logged-in only */} {/* ── Sport selector (multi-sport athletes) ───────────────── */}
{isLoggedIn && ( {user.sports?.length > 1 && (
<div className="card" style={{ marginBottom: 16 }}> <div className="tab-bar" style={{ marginBottom: 12 }}>
<div style={{ fontFamily: 'var(--font-display)', fontSize: 14, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-muted)', marginBottom: 14 }}>Contact</div> {user.sports.map(s => (
<div className="grid-2"> <button
<div><span className="label">Email</span><div style={{ color: 'var(--accent)', fontSize: 14 }}>{user.email}</div></div> key={s}
<div><span className="label">Phone</span><div style={{ fontSize: 14 }}>{user.phone}</div></div> className={`tab ${displaySport === s ? 'active' : ''}`}
</div> onClick={() => { setActiveSport(s); setActiveTab('stats'); }}
>
{SPORTS[s]?.emoji} {SPORTS[s]?.name}
</button>
))}
</div> </div>
)} )}
{/* Biometrics */} {/* ── Content tabs ─────────────────────────────────────────── */}
<div className="card" style={{ marginBottom: 16 }}> <div className="tab-bar" style={{ marginBottom: 20 }}>
<div style={{ fontFamily: 'var(--font-display)', fontSize: 14, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-muted)', marginBottom: 14 }}>Biometrics</div> {tabs.map(t => (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(130px,1fr))', gap: 12 }}> <button key={t} className={`tab ${activeTab === t ? 'active' : ''}`} onClick={() => setActiveTab(t)}>
{tabLabels[t]}
</button>
))}
</div>
{/* ── Stats tab ────────────────────────────────────────────── */}
{activeTab === 'stats' && (
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
{/* Table header row */}
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '14px 20px',
borderBottom: '1px solid var(--border)',
background: 'var(--bg-card2)',
}}>
<span style={{
fontFamily: 'var(--font-display)', fontSize: 12, fontWeight: 700,
letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--text-muted)',
}}>
{sportMeta?.emoji} {sportMeta?.name} Season Stats
</span>
{position && (
<span style={{
fontFamily: 'var(--font-display)', fontSize: 12, fontWeight: 800,
letterSpacing: '0.08em', textTransform: 'uppercase', color: sportColor,
}}>
{position}
</span>
)}
</div>
{numericDefs.length > 0 ? (
<div style={{ overflowX: 'auto' }}>
<table style={{ minWidth: 600 }}>
<thead>
<tr>
{numericDefs.map(def => (
<th key={def.key} style={{
color: highlights.some(h => h.key === def.key) ? sportColor : undefined,
}}>
{def.label}
</th>
))}
</tr>
</thead>
<tbody>
<tr>
{numericDefs.map(def => (
<td key={def.key} style={{
fontFamily: 'var(--font-display)',
fontWeight: highlights.some(h => h.key === def.key) ? 800 : 500,
fontSize: highlights.some(h => h.key === def.key) ? 16 : 14,
color: highlights.some(h => h.key === def.key) ? sportColor : 'var(--text-primary)',
}}>
{fmtStat(stats[def.key], def.type)}
</td>
))}
</tr>
</tbody>
</table>
</div>
) : (
<div style={{ textAlign: 'center', color: 'var(--text-muted)', padding: '40px 20px', fontSize: 14 }}>
No stats recorded yet
</div>
)}
</div>
)}
{/* ── Biometrics tab ───────────────────────────────────────── */}
{activeTab === 'biometrics' && (
<div className="card">
<div style={{
fontFamily: 'var(--font-display)', fontSize: 12, fontWeight: 700,
letterSpacing: '0.1em', textTransform: 'uppercase',
color: 'var(--text-muted)', marginBottom: 16,
}}>
Physical Measurements
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 12 }}>
{BIOMETRIC_FIELDS.map(f => { {BIOMETRIC_FIELDS.map(f => {
const val = user.biometrics?.[f.key]; const val = user.biometrics?.[f.key];
if (!val && val !== 0) return null; if (!val && val !== 0) return null;
return ( return (
<div key={f.key} className="card2"> <div key={f.key} className="card2">
<div className="stat-lbl">{f.label}</div> <div className="stat-lbl">{f.label}</div>
<div className="stat-val" style={{ fontSize: 20, marginTop: 4 }}> <div className="stat-val" style={{ fontSize: 20, marginTop: 6 }}>
{f.type === 'decimal' ? Number(val).toFixed(1) : val} {f.type === 'decimal' ? Number(val).toFixed(1) : val}
</div> </div>
</div> </div>
@@ -96,42 +285,60 @@ export default function AthleteDetail() {
})} })}
</div> </div>
</div> </div>
)}
{/* Sport stats */} {/* ── Contact tab (logged-in only) ─────────────────────────── */}
<div className="card" style={{ marginBottom: 16 }}> {activeTab === 'contact' && isLoggedIn && (
<div style={{ fontFamily: 'var(--font-display)', fontSize: 14, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-muted)', marginBottom: 14 }}>Sport Statistics</div> <div>
<div className="card" style={{ marginBottom: 12 }}>
<div style={{
fontFamily: 'var(--font-display)', fontSize: 12, fontWeight: 700,
letterSpacing: '0.1em', textTransform: 'uppercase',
color: 'var(--text-muted)', marginBottom: 16,
}}>
Contact Details
</div>
<div className="grid-2">
<div>
<span className="label">Email</span>
<div style={{ color: 'var(--accent)', fontSize: 14, marginTop: 4 }}>{user.email || '—'}</div>
</div>
<div>
<span className="label">Phone</span>
<div style={{ fontSize: 14, marginTop: 4 }}>{user.phone || '—'}</div>
</div>
</div>
</div>
{user.sports?.length > 1 && ( {user.socials && Object.values(user.socials).some(Boolean) && (
<div className="tab-bar" style={{ marginBottom: 20 }}> <div className="card">
{user.sports.map(s => ( <div style={{
<button key={s} className={`tab ${displaySport === s ? 'active' : ''}`} onClick={() => setActiveSport(s)}> fontFamily: 'var(--font-display)', fontSize: 12, fontWeight: 700,
{SPORTS[s]?.emoji} {SPORTS[s]?.name} letterSpacing: '0.1em', textTransform: 'uppercase',
</button> color: 'var(--text-muted)', marginBottom: 16,
}}>
Social Media
</div>
<div className="grid-2">
{Object.entries(user.socials).filter(([, handle]) => handle).map(([platform, handle]) => (
<div key={platform} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 20 }}>{SOCIAL_ICONS[platform] || '🔗'}</span>
<div>
<div className="stat-lbl">{platform}</div>
<div style={{ fontSize: 14, marginTop: 2 }}>{handle}</div>
</div>
</div>
))} ))}
</div> </div>
</div>
)}
</div>
)} )}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(130px,1fr))', gap: 12 }}> <div style={{ marginTop: 24 }}>
{statDefs.map(def => { <Link to="/athletes" className="btn btn-secondary"> Back to Athletes</Link>
const val = stats[def.key];
if (val === undefined || val === null) return null;
const display = def.type === 'decimal' ? Number(val).toFixed(2) : String(val);
if (display === '0' && def.type !== 'text') return null;
return (
<div key={def.key} className="card2">
<div className="stat-lbl">{def.label}</div>
<div style={{
fontFamily: 'var(--font-display)', fontWeight: 800,
fontSize: def.type === 'text' ? 16 : 20,
color: 'var(--accent)', marginTop: 4
}}>{display}</div>
</div>
);
})}
</div>
</div> </div>
<Link to="/athletes" className="btn btn-secondary"> Back to Athletes</Link>
</div> </div>
); );
} }