athlete page layout update
This commit is contained in:
@@ -6,89 +6,278 @@ import Avatar from '../components/Avatar.jsx';
|
||||
import SportBadge from '../components/SportBadge.jsx';
|
||||
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() {
|
||||
const { id } = useParams();
|
||||
const { getUserById, deleteUser, auth } = useStore();
|
||||
const navigate = useNavigate();
|
||||
const isLoggedIn = auth?.isLoggedIn;
|
||||
|
||||
const [activeSport, setActiveSport] = useState(null);
|
||||
const [activeTab, setActiveTab] = useState('stats');
|
||||
|
||||
const user = getUserById(id);
|
||||
|
||||
if (!user) return (
|
||||
<div className="page" style={{ textAlign: 'center', paddingTop: 80 }}>
|
||||
<div style={{ fontSize: 48, marginBottom: 16 }}>🏟️</div>
|
||||
<div style={{ color: 'var(--text-muted)' }}>Athlete not found</div>
|
||||
<Link to="/athletes" className="btn btn-secondary" style={{ marginTop: 16 }}>Back to Athletes</Link>
|
||||
<div style={{ fontSize: 64, marginBottom: 16 }}>🏟️</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: 8 }}>← Back to Athletes</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
const displaySport = activeSport || user.primarySport || user.sports?.[0];
|
||||
const sportMeta = SPORTS[displaySport];
|
||||
const sportColor = sportMeta?.color || 'var(--accent)';
|
||||
const stats = user.sportStats?.[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() {
|
||||
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 (
|
||||
<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)' }}>
|
||||
<div style={{ display: 'flex', gap: 20, alignItems: 'flex-start', flexWrap: 'wrap' }}>
|
||||
<Avatar user={user} size={80} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<h1 style={{ fontFamily: 'var(--font-display)', fontSize: 32, fontWeight: 900, letterSpacing: '0.02em', marginBottom: 4 }}>
|
||||
|
||||
{/* ── Hero ─────────────────────────────────────────────────── */}
|
||||
<div style={{
|
||||
marginBottom: 16,
|
||||
borderRadius: 'var(--radius)',
|
||||
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)}
|
||||
</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} />)}
|
||||
</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 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>
|
||||
<button className="btn btn-danger" style={{ padding: '8px 14px', fontSize: 12 }} onClick={handleDelete}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Social media — logged-in only */}
|
||||
{isLoggedIn && user.socials && Object.values(user.socials).some(Boolean) && (
|
||||
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--border)', display: 'flex', gap: 14, flexWrap: 'wrap' }}>
|
||||
{Object.entries(user.socials).filter(([, handle]) => handle).map(([platform, handle]) => (
|
||||
<div key={platform} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: 'var(--text-secondary)' }}>
|
||||
<span>{socialIcons[platform] || '🔗'}</span>
|
||||
<span>{handle}</span>
|
||||
{/* ── Key stats highlight strip ────────────────────────────── */}
|
||||
{highlights.length > 0 && (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(90px, 1fr))`,
|
||||
gap: 1,
|
||||
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>
|
||||
|
||||
{/* Contact — logged-in only */}
|
||||
{isLoggedIn && (
|
||||
<div className="card" style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 14, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-muted)', marginBottom: 14 }}>Contact</div>
|
||||
<div className="grid-2">
|
||||
<div><span className="label">Email</span><div style={{ color: 'var(--accent)', fontSize: 14 }}>{user.email}</div></div>
|
||||
<div><span className="label">Phone</span><div style={{ fontSize: 14 }}>{user.phone}</div></div>
|
||||
</div>
|
||||
{/* ── Sport selector (multi-sport athletes) ───────────────── */}
|
||||
{user.sports?.length > 1 && (
|
||||
<div className="tab-bar" style={{ marginBottom: 12 }}>
|
||||
{user.sports.map(s => (
|
||||
<button
|
||||
key={s}
|
||||
className={`tab ${displaySport === s ? 'active' : ''}`}
|
||||
onClick={() => { setActiveSport(s); setActiveTab('stats'); }}
|
||||
>
|
||||
{SPORTS[s]?.emoji} {SPORTS[s]?.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Biometrics */}
|
||||
<div className="card" style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 14, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-muted)', marginBottom: 14 }}>Biometrics</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(130px,1fr))', gap: 12 }}>
|
||||
{/* ── Content tabs ─────────────────────────────────────────── */}
|
||||
<div className="tab-bar" style={{ marginBottom: 20 }}>
|
||||
{tabs.map(t => (
|
||||
<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 => {
|
||||
const val = user.biometrics?.[f.key];
|
||||
if (!val && val !== 0) return null;
|
||||
return (
|
||||
<div key={f.key} className="card2">
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,42 +285,60 @@ export default function AthleteDetail() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sport stats */}
|
||||
<div className="card" style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 14, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-muted)', marginBottom: 14 }}>Sport Statistics</div>
|
||||
{/* ── Contact tab (logged-in only) ─────────────────────────── */}
|
||||
{activeTab === 'contact' && isLoggedIn && (
|
||||
<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 && (
|
||||
<div className="tab-bar" style={{ marginBottom: 20 }}>
|
||||
{user.sports.map(s => (
|
||||
<button key={s} className={`tab ${displaySport === s ? 'active' : ''}`} onClick={() => setActiveSport(s)}>
|
||||
{SPORTS[s]?.emoji} {SPORTS[s]?.name}
|
||||
</button>
|
||||
{user.socials && Object.values(user.socials).some(Boolean) && (
|
||||
<div className="card">
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-display)', fontSize: 12, fontWeight: 700,
|
||||
letterSpacing: '0.1em', textTransform: 'uppercase',
|
||||
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 style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(130px,1fr))', gap: 12 }}>
|
||||
{statDefs.map(def => {
|
||||
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 style={{ marginTop: 24 }}>
|
||||
<Link to="/athletes" className="btn btn-secondary">← Back to Athletes</Link>
|
||||
</div>
|
||||
|
||||
<Link to="/athletes" className="btn btn-secondary">← Back to Athletes</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user