minor rule changes for viewing athletes

This commit is contained in:
2026-04-08 12:02:25 -04:00
parent da7ca20228
commit 6e88a2a19a
8 changed files with 62 additions and 31 deletions

View File

@@ -180,8 +180,17 @@ src/components/ — shared, reusable, no page logic
src/pages/ — one file per route, owns that route's state src/pages/ — one file per route, owns that route's state
src/hooks/ — useStore only (for now) src/hooks/ — useStore only (for now)
src/data/ — seedData only src/data/ — seedData only
src/utils/ — pure helper functions shared across pages
``` ```
### Athlete Name Display
**Always use `fmtName(user)` from `src/utils/formatName.js` in all lists, tables, and cards.** This displays `"Lastname, F."` (e.g. `"Thompson, M."`).
The only place full names appear is `AthleteDetail.jsx`, and only when `auth.isLoggedIn` is true. Non-logged-in visitors see the formatted name even on the profile page.
Contact details (email, phone) and social media handles are also hidden on `AthleteDetail.jsx` unless `auth.isLoggedIn`.
### Import Style ### Import Style
All imports use explicit `.jsx` extensions. No index barrel files. Example: All imports use explicit `.jsx` extensions. No index barrel files. Example:
@@ -368,7 +377,7 @@ npm run build
|---|---| |---|---|
| New package added to `dependencies` or `devDependencies` in `package.json` | `npm install` on the server (or full `install.sh` rerun) | | New package added to `dependencies` or `devDependencies` in `package.json` | `npm install` on the server (or full `install.sh` rerun) |
| Node.js version requirement changes | Full `install.sh` rerun | | Node.js version requirement changes | Full `install.sh` rerun |
| Nginx config changes | Full `install.sh` rerun (or manually edit `/etc/nginx/sites-available/statsphere`) | | Nginx config changes | Full `install.sh` rerun (or manually edit `/etc/nginx/sites-available/playersedge`) |
| New system-level dependency (e.g. ImageMagick, a native Node addon) | Full `install.sh` rerun | | New system-level dependency (e.g. ImageMagick, a native Node addon) | Full `install.sh` rerun |
**Claude will flag this:** Any time a task adds or removes a package from `package.json`, Claude will explicitly note that `npm install` (or `install.sh`) must be rerun on the server before the next deployment. **Claude will flag this:** Any time a task adds or removes a package from `package.json`, Claude will explicitly note that `npm install` (or `install.sh`) must be rerun on the server before the next deployment.

View File

@@ -1,15 +1,15 @@
#!/bin/bash #!/bin/bash
# StatSphere - Ubuntu 24.04 LXC Install Script # PlayersEdge - Ubuntu 24.04 LXC Install Script
# Run as root or with sudo # Run as root or with sudo
set -e set -e
APP_DIR="/opt/statsphere" APP_DIR="/opt/playersedge"
APP_USER="statsphere" APP_USER="playersedge"
NODE_VERSION="20" NODE_VERSION="20"
echo "============================================" echo "============================================"
echo " StatSphere Athlete Stats Platform Installer" echo " PlayersEdge Athlete Stats Platform Installer"
echo "============================================" echo "============================================"
# 1. System update # 1. System update
@@ -26,7 +26,7 @@ echo " NPM: $(npm --version)"
# 3. Create app user # 3. Create app user
echo "[3/8] Creating app user..." echo "[3/8] Creating app user..."
id -u $APP_USER &>/dev/null || useradd -r -s /bin/false -d $APP_DIR $APP_USER id -u $APP_USER &>/dev/null || useradd -r -s /bin/false -d $APP_DIR playersedge
# 4. Copy app files # 4. Copy app files
echo "[4/8] Setting up application directory..." echo "[4/8] Setting up application directory..."
@@ -44,13 +44,13 @@ sudo -u $APP_USER npm run build
# 6. Configure Nginx # 6. Configure Nginx
echo "[7/8] Configuring Nginx..." echo "[7/8] Configuring Nginx..."
cat > /etc/nginx/sites-available/statsphere <<'NGINX' cat > /etc/nginx/sites-available/playersedge <<'NGINX'
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
server_name _; server_name _;
root /opt/statsphere/dist; root /opt/playersedge/dist;
index index.html; index index.html;
# Gzip # Gzip
@@ -77,7 +77,7 @@ server {
NGINX NGINX
# Enable site # Enable site
ln -sf /etc/nginx/sites-available/statsphere /etc/nginx/sites-enabled/ ln -sf /etc/nginx/sites-available/playersedge /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default rm -f /etc/nginx/sites-enabled/default
nginx -t nginx -t
systemctl restart nginx systemctl restart nginx
@@ -85,15 +85,15 @@ systemctl enable nginx
# 7. Setup systemd service for dev server (optional) # 7. Setup systemd service for dev server (optional)
echo "[8/8] Setting up systemd service (dev preview mode)..." echo "[8/8] Setting up systemd service (dev preview mode)..."
cat > /etc/systemd/system/statsphere-dev.service <<'SERVICE' cat > /etc/systemd/system/playersedge-dev.service <<'SERVICE'
[Unit] [Unit]
Description=StatSphere Development Server Description=PlayersEdge Development Server
After=network.target After=network.target
[Service] [Service]
Type=simple Type=simple
User=statsphere User=playersedge
WorkingDirectory=/opt/statsphere WorkingDirectory=/opt/playersedge
ExecStart=/usr/bin/npm run preview ExecStart=/usr/bin/npm run preview
Restart=on-failure Restart=on-failure
RestartSec=5 RestartSec=5
@@ -123,6 +123,6 @@ echo " npm run build"
echo " systemctl restart nginx" echo " systemctl restart nginx"
echo "" echo ""
echo " Dev server (optional, port 4173):" echo " Dev server (optional, port 4173):"
echo " systemctl start statsphere-dev" echo " systemctl start playersedge-dev"
echo " systemctl enable statsphere-dev" echo " systemctl enable playersedge-dev"
echo "" echo ""

View File

@@ -4,11 +4,13 @@ import { useStore } from '../hooks/useStore.jsx';
import { SPORTS, SPORT_STATS_DEFS, BIOMETRIC_FIELDS } from '../data/seedData.js'; import { SPORTS, SPORT_STATS_DEFS, BIOMETRIC_FIELDS } from '../data/seedData.js';
import Avatar from '../components/Avatar.jsx'; import Avatar from '../components/Avatar.jsx';
import SportBadge from '../components/SportBadge.jsx'; import SportBadge from '../components/SportBadge.jsx';
import { fmtName } from '../utils/formatName.js';
export default function AthleteDetail() { export default function AthleteDetail() {
const { id } = useParams(); const { id } = useParams();
const { getUserById, deleteUser } = useStore(); const { getUserById, deleteUser, auth } = useStore();
const navigate = useNavigate(); const navigate = useNavigate();
const isLoggedIn = auth?.isLoggedIn;
const [activeSport, setActiveSport] = useState(null); const [activeSport, setActiveSport] = useState(null);
const user = getUserById(id); const user = getUserById(id);
@@ -37,7 +39,9 @@ export default function AthleteDetail() {
<div style={{ display: 'flex', gap: 20, alignItems: 'flex-start', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 20, alignItems: 'flex-start', flexWrap: 'wrap' }}>
<Avatar user={user} size={80} /> <Avatar user={user} size={80} />
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<h1 style={{ fontFamily: 'var(--font-display)', fontSize: 32, fontWeight: 900, letterSpacing: '0.02em', marginBottom: 4 }}>{user.name}</h1> <h1 style={{ fontFamily: 'var(--font-display)', fontSize: 32, fontWeight: 900, letterSpacing: '0.02em', marginBottom: 4 }}>
{isLoggedIn ? user.name : fmtName(user)}
</h1>
<div style={{ color: 'var(--text-secondary)', marginBottom: 10 }}>{user.city}, {user.country} · Joined {user.joinDate}</div> <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 }}> <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 10 }}>
{user.sports?.map(s => <SportBadge key={s} sport={s} />)} {user.sports?.map(s => <SportBadge key={s} sport={s} />)}
@@ -50,10 +54,10 @@ export default function AthleteDetail() {
</div> </div>
</div> </div>
{/* Social media */} {/* Social media — logged-in only */}
{user.socials && Object.keys(user.socials).length > 0 && ( {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' }}> <div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--border)', display: 'flex', gap: 14, flexWrap: 'wrap' }}>
{Object.entries(user.socials).map(([platform, handle]) => ( {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)' }}> <div key={platform} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: 'var(--text-secondary)' }}>
<span>{socialIcons[platform] || '🔗'}</span> <span>{socialIcons[platform] || '🔗'}</span>
<span>{handle}</span> <span>{handle}</span>
@@ -63,14 +67,16 @@ export default function AthleteDetail() {
)} )}
</div> </div>
{/* Contact */} {/* Contact — logged-in only */}
<div className="card" style={{ marginBottom: 16 }}> {isLoggedIn && (
<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="card" style={{ marginBottom: 16 }}>
<div className="grid-2"> <div style={{ fontFamily: 'var(--font-display)', fontSize: 14, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-muted)', marginBottom: 14 }}>Contact</div>
<div><span className="label">Email</span><div style={{ color: 'var(--accent)', fontSize: 14 }}>{user.email}</div></div> <div className="grid-2">
<div><span className="label">Phone</span><div style={{ fontSize: 14 }}>{user.phone}</div></div> <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>
</div> </div>
</div> )}
{/* Biometrics */} {/* Biometrics */}
<div className="card" style={{ marginBottom: 16 }}> <div className="card" style={{ marginBottom: 16 }}>

View File

@@ -4,6 +4,7 @@ import { useStore } from '../hooks/useStore.jsx';
import { SPORTS } from '../data/seedData.js'; import { SPORTS } from '../data/seedData.js';
import Avatar from '../components/Avatar.jsx'; import Avatar from '../components/Avatar.jsx';
import SportBadge from '../components/SportBadge.jsx'; import SportBadge from '../components/SportBadge.jsx';
import { fmtName } from '../utils/formatName.js';
export default function Athletes() { export default function Athletes() {
const { users } = useStore(); const { users } = useStore();
@@ -48,7 +49,7 @@ export default function Athletes() {
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
<Avatar user={u} size={48} /> <Avatar user={u} size={48} />
<div> <div>
<div style={{ fontWeight: 700, fontSize: 16 }}>{u.name}</div> <div style={{ fontWeight: 700, fontSize: 16 }}>{fmtName(u)}</div>
<div style={{ color: 'var(--text-muted)', fontSize: 12 }}>{u.city}, {u.country}</div> <div style={{ color: 'var(--text-muted)', fontSize: 12 }}>{u.city}, {u.country}</div>
</div> </div>
</div> </div>

View File

@@ -4,6 +4,7 @@ import { useStore } from '../hooks/useStore.jsx';
import { SPORTS, SPORT_STATS_DEFS, BIOMETRIC_FIELDS } from '../data/seedData.js'; import { SPORTS, SPORT_STATS_DEFS, BIOMETRIC_FIELDS } from '../data/seedData.js';
import Avatar from '../components/Avatar.jsx'; import Avatar from '../components/Avatar.jsx';
import SportBadge from '../components/SportBadge.jsx'; import SportBadge from '../components/SportBadge.jsx';
import { fmtName } from '../utils/formatName.js';
export default function Filter() { export default function Filter() {
const { users } = useStore(); const { users } = useStore();
@@ -226,7 +227,7 @@ export default function Filter() {
> >
<Avatar user={u} size={44} /> <Avatar user={u} size={44} />
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: 15 }}>{u.name}</div> <div style={{ fontWeight: 600, fontSize: 15 }}>{fmtName(u)}</div>
<div style={{ color: 'var(--text-muted)', fontSize: 12 }}> <div style={{ color: 'var(--text-muted)', fontSize: 12 }}>
{u.biometrics?.age} yrs · {u.biometrics?.height_cm}cm · {u.city}, {u.country} {u.biometrics?.age} yrs · {u.biometrics?.height_cm}cm · {u.city}, {u.country}
</div> </div>

View File

@@ -2,6 +2,7 @@ import { Link } from 'react-router-dom';
import { useStore } from '../hooks/useStore.jsx'; import { useStore } from '../hooks/useStore.jsx';
import { SPORTS } from '../data/seedData.js'; import { SPORTS } from '../data/seedData.js';
import Avatar from '../components/Avatar.jsx'; import Avatar from '../components/Avatar.jsx';
import { fmtName } from '../utils/formatName.js';
export default function Home() { export default function Home() {
const { users } = useStore(); const { users } = useStore();
@@ -96,7 +97,7 @@ export default function Home() {
> >
<Avatar user={u} size={42} /> <Avatar user={u} size={42} />
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: 15 }}>{u.name}</div> <div style={{ fontWeight: 600, fontSize: 15 }}>{fmtName(u)}</div>
<div style={{ color: 'var(--text-muted)', fontSize: 12 }}>{u.city}, {u.country}</div> <div style={{ color: 'var(--text-muted)', fontSize: 12 }}>{u.city}, {u.country}</div>
</div> </div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', justifyContent: 'flex-end' }}> <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', justifyContent: 'flex-end' }}>

View File

@@ -3,6 +3,7 @@ import { useSearchParams, Link } from 'react-router-dom';
import { useStore } from '../hooks/useStore.jsx'; import { useStore } from '../hooks/useStore.jsx';
import { SPORTS, SPORT_STATS_DEFS } from '../data/seedData.js'; import { SPORTS, SPORT_STATS_DEFS } from '../data/seedData.js';
import Avatar from '../components/Avatar.jsx'; import Avatar from '../components/Avatar.jsx';
import { fmtName } from '../utils/formatName.js';
export default function Leaders() { export default function Leaders() {
const { getUsersBySport } = useStore(); const { getUsersBySport } = useStore();
@@ -120,7 +121,7 @@ export default function Leaders() {
<Link to={`/athletes/${u.id}`} style={{ display: 'flex', alignItems: 'center', gap: 10 }}> <Link to={`/athletes/${u.id}`} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<Avatar user={u} size={34} /> <Avatar user={u} size={34} />
<div> <div>
<div style={{ fontWeight: 600, fontSize: 14, whiteSpace: 'nowrap' }}>{u.name}</div> <div style={{ fontWeight: 600, fontSize: 14, whiteSpace: 'nowrap' }}>{fmtName(u)}</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)' }}>{u.city}</div> <div style={{ fontSize: 11, color: 'var(--text-muted)' }}>{u.city}</div>
</div> </div>
</Link> </Link>

12
src/utils/formatName.js Normal file
View File

@@ -0,0 +1,12 @@
/**
* Returns "Lastname, F." for public display.
* Use on all lists, tables, and cards.
* Only show the full name (user.name) on AthleteDetail when the viewer is logged in.
*/
export function fmtName(user) {
if (!user) return '';
const last = user.lastName || '';
const first = user.firstName || '';
const initial = first ? `${first[0]}.` : '';
return initial ? `${last}, ${initial}` : last;
}