minor rule changes for viewing athletes
This commit is contained in:
11
CLAUDE.md
11
CLAUDE.md
@@ -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.
|
||||||
|
|||||||
28
install.sh
28
install.sh
@@ -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 ""
|
||||||
|
|||||||
@@ -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,7 +67,8 @@ export default function AthleteDetail() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contact */}
|
{/* Contact — logged-in only */}
|
||||||
|
{isLoggedIn && (
|
||||||
<div className="card" style={{ marginBottom: 16 }}>
|
<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 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 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><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 }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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' }}>
|
||||||
|
|||||||
@@ -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
12
src/utils/formatName.js
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user