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/hooks/ — useStore only (for now)
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
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) |
| 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 |
**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
# StatSphere - Ubuntu 24.04 LXC Install Script
# PlayersEdge - Ubuntu 24.04 LXC Install Script
# Run as root or with sudo
set -e
APP_DIR="/opt/statsphere"
APP_USER="statsphere"
APP_DIR="/opt/playersedge"
APP_USER="playersedge"
NODE_VERSION="20"
echo "============================================"
echo " StatSphere Athlete Stats Platform Installer"
echo " PlayersEdge Athlete Stats Platform Installer"
echo "============================================"
# 1. System update
@@ -26,7 +26,7 @@ echo " NPM: $(npm --version)"
# 3. Create 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
echo "[4/8] Setting up application directory..."
@@ -44,13 +44,13 @@ sudo -u $APP_USER npm run build
# 6. Configure Nginx
echo "[7/8] Configuring Nginx..."
cat > /etc/nginx/sites-available/statsphere <<'NGINX'
cat > /etc/nginx/sites-available/playersedge <<'NGINX'
server {
listen 80;
listen [::]:80;
server_name _;
root /opt/statsphere/dist;
root /opt/playersedge/dist;
index index.html;
# Gzip
@@ -77,7 +77,7 @@ server {
NGINX
# 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
nginx -t
systemctl restart nginx
@@ -85,15 +85,15 @@ systemctl enable nginx
# 7. Setup systemd service for dev server (optional)
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]
Description=StatSphere Development Server
Description=PlayersEdge Development Server
After=network.target
[Service]
Type=simple
User=statsphere
WorkingDirectory=/opt/statsphere
User=playersedge
WorkingDirectory=/opt/playersedge
ExecStart=/usr/bin/npm run preview
Restart=on-failure
RestartSec=5
@@ -123,6 +123,6 @@ echo " npm run build"
echo " systemctl restart nginx"
echo ""
echo " Dev server (optional, port 4173):"
echo " systemctl start statsphere-dev"
echo " systemctl enable statsphere-dev"
echo " systemctl start playersedge-dev"
echo " systemctl enable playersedge-dev"
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 Avatar from '../components/Avatar.jsx';
import SportBadge from '../components/SportBadge.jsx';
import { fmtName } from '../utils/formatName.js';
export default function AthleteDetail() {
const { id } = useParams();
const { getUserById, deleteUser } = useStore();
const { getUserById, deleteUser, auth } = useStore();
const navigate = useNavigate();
const isLoggedIn = auth?.isLoggedIn;
const [activeSport, setActiveSport] = useState(null);
const user = getUserById(id);
@@ -37,7 +39,9 @@ export default function AthleteDetail() {
<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 }}>{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={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 10 }}>
{user.sports?.map(s => <SportBadge key={s} sport={s} />)}
@@ -50,10 +54,10 @@ export default function AthleteDetail() {
</div>
</div>
{/* Social media */}
{user.socials && Object.keys(user.socials).length > 0 && (
{/* 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).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)' }}>
<span>{socialIcons[platform] || '🔗'}</span>
<span>{handle}</span>
@@ -63,7 +67,8 @@ export default function AthleteDetail() {
)}
</div>
{/* Contact */}
{/* 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">
@@ -71,6 +76,7 @@ export default function AthleteDetail() {
<div><span className="label">Phone</span><div style={{ fontSize: 14 }}>{user.phone}</div></div>
</div>
</div>
)}
{/* Biometrics */}
<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 Avatar from '../components/Avatar.jsx';
import SportBadge from '../components/SportBadge.jsx';
import { fmtName } from '../utils/formatName.js';
export default function Athletes() {
const { users } = useStore();
@@ -48,7 +49,7 @@ export default function Athletes() {
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
<Avatar user={u} size={48} />
<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>
</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 Avatar from '../components/Avatar.jsx';
import SportBadge from '../components/SportBadge.jsx';
import { fmtName } from '../utils/formatName.js';
export default function Filter() {
const { users } = useStore();
@@ -226,7 +227,7 @@ export default function Filter() {
>
<Avatar user={u} size={44} />
<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.biometrics?.age} yrs · {u.biometrics?.height_cm}cm · {u.city}, {u.country}
</div>

View File

@@ -2,6 +2,7 @@ import { Link } from 'react-router-dom';
import { useStore } from '../hooks/useStore.jsx';
import { SPORTS } from '../data/seedData.js';
import Avatar from '../components/Avatar.jsx';
import { fmtName } from '../utils/formatName.js';
export default function Home() {
const { users } = useStore();
@@ -96,7 +97,7 @@ export default function Home() {
>
<Avatar user={u} size={42} />
<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>
<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 { SPORTS, SPORT_STATS_DEFS } from '../data/seedData.js';
import Avatar from '../components/Avatar.jsx';
import { fmtName } from '../utils/formatName.js';
export default function Leaders() {
const { getUsersBySport } = useStore();
@@ -120,7 +121,7 @@ export default function Leaders() {
<Link to={`/athletes/${u.id}`} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<Avatar user={u} size={34} />
<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>
</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;
}