# StatSphere – Athlete Stats Platform ## Vibe-Coding Prompt --- ## Project Overview Build **StatSphere**, a full-featured Progressive Web App (PWA) for tracking athlete biometric and sport-specific statistics. The app supports five sports at launch with a data model designed for easy expansion. Athletes can be registered in multiple sports simultaneously. The platform features a stat leaders board, advanced filtering, athlete profiles, and a multi-step registration form. All data is stored in the browser via `localStorage`. No backend or database is required. --- ## Tech Stack | Layer | Choice | Notes | |---|---|---| | Framework | React 18 | Functional components + hooks throughout | | Routing | React Router v6 | `BrowserRouter`, `Routes`, `Route`, `Link`, `useNavigate`, `useSearchParams` | | Build tool | Vite 5 | Fast dev server + production build | | PWA | vite-plugin-pwa + Workbox | Auto service worker, manifest, offline caching | | Styling | Pure CSS (no framework) | CSS custom properties for theming, no Tailwind | | Icons | lucide-react | Minimal usage; emoji used for sport icons | | State | React Context + localStorage | `StoreProvider` wraps the whole app | | Fonts | Google Fonts | Barlow Condensed (display) + Barlow (body) | | Web server | Nginx | SPA fallback, static file serving, gzip | | Target OS | Ubuntu 24.04 LXC | Auto-installer script included | **`package.json` dependencies:** ```json { "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.27.0", "lucide-react": "^0.460.0" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.3", "vite": "^5.4.10", "vite-plugin-pwa": "^0.20.5", "workbox-window": "^7.3.0" } } ``` --- ## Visual Design System ### Theme: Dark Sports A high-contrast dark theme inspired by professional sports analytics dashboards. ```css :root { --bg-base: #0a0f1e; /* page background — near-black navy */ --bg-card: #111827; /* primary card surface */ --bg-card2: #1a2235; /* secondary card / nested surface */ --bg-input: #1e2d45; /* form input background */ --accent: #00d4ff; /* primary accent — electric cyan */ --accent2: #ff6b35; /* secondary accent — orange */ --accent3: #a855f7; /* tertiary accent — purple */ --text-primary: #f0f4ff; /* near-white body text */ --text-secondary: #8899bb;/* muted labels */ --text-muted: #4a5875; /* de-emphasised text */ --border: #1e2d45; /* card borders */ --border-bright: #2a3d5a; /* input/hover borders */ --success: #22c55e; --warning: #f59e0b; --danger: #ef4444; --font-display: 'Barlow Condensed', sans-serif; --font-body: 'Barlow', sans-serif; --nav-h: 60px; --bottom-nav-h: 64px; /* 0px on desktop, 64px on mobile */ --radius: 12px; --radius-sm: 8px; } ``` ### Typography - **Display / headings:** `Barlow Condensed` — weight 700–900, uppercase, letter-spacing 0.02–0.1em - **Body:** `Barlow` — weight 400–600 - Section titles: 28px, weight 800, uppercase, `--font-display` - Stat values: 20–24px, weight 800, `--font-display`, colored `--accent` - Stat labels: 11px, uppercase, letter-spacing 0.06em, `--text-muted`, `--font-display` - Table headers: 11px, uppercase, letter-spacing 0.1em, `--text-muted`, `--font-display` ### Sport Color Coding Each sport has a distinct badge color scheme (background / text / border): | Sport | Class | BG | Text | |---|---|---|---| | American Football | `.sport-football` | `#1a3a1a` | `#4ade80` | | Hockey | `.sport-hockey` | `#1a2e4a` | `#60a5fa` | | Baseball | `.sport-baseball` | `#3a1a1a` | `#f87171` | | Soccer | `.sport-soccer` | `#2a1a3a` | `#c084fc` | | Basketball | `.sport-basketball` | `#3a2a1a` | `#fb923c` | Badges use class `.badge` + `.sport-{id}` and are uppercase, 11px, weight 700, `--font-display`. ### Rank Badges Circular 32px badges for leaderboard ranks: - Rank 1: gold `#ffd700`, black text - Rank 2: silver `#c0c0c0`, black text - Rank 3: bronze `#cd7f32`, black text - Rank 4+: `--bg-input`, `--text-secondary` ### Reusable CSS Classes - `.page` — content wrapper: `padding: (nav-h + 20px) 16px (bottom-nav-h + 20px)`, max-width 900px, centered - `.card` — `background: --bg-card`, 1px border, radius 12px, padding 20px - `.card2` — `background: --bg-card2`, 1px border, radius 8px, padding 16px - `.btn` — inline-flex, gap 8px, padding 10px 20px, radius 8px, font-display, 14px, 600 weight, uppercase - `.btn-primary` — background `--accent`, color black - `.btn-secondary` — transparent, border `--border-bright`, hover: border + text → `--accent` - `.btn-danger` — background `--danger`, white text - `.badge` — inline-flex, padding 3px 10px, radius 20px, 11px, 700 weight, uppercase - `.label` — 12px, uppercase, letter-spacing 0.08em, `--text-secondary`, `--font-display`, display block, margin-bottom 6px - `.form-group` — margin-bottom 16px - `.tab-bar` — flex row, `--bg-card` bg, border, radius 8px, padding 4px, overflow-x auto - `.tab` — padding 8px 16px, radius 6px, 13px, 700 weight, uppercase; `.active` → background `--accent`, color black - `.grid-2` — 2-column grid, gap 16px; collapses to 1-col below 600px - `.grid-3` — 3-column grid, gap 12px; collapses to 2-col below 600px - `.section-title` — `--font-display`, 28px, 800, uppercase - `.section-sub` — 14px, `--text-secondary`, margin-bottom 24px - `.stat-val` — `--font-display`, 24px, 800, `--accent`, line-height 1 - `.stat-lbl` — 11px, `--text-muted`, uppercase, letter-spacing 0.06em, `--font-display` - `.toast` — fixed bottom (above bottom-nav), centered, success green pill, animated in/out - `.pwa-banner` — fixed bottom (above bottom-nav), full-width, `--bg-card2`, cyan border, install prompt ### Forms All `input`, `select`, `textarea` elements: - Background: `--bg-input` - Border: 1px solid `--border-bright` - Border-radius: `--radius-sm` - Padding: 10px 14px - Color: `--text-primary` - Focus: border-color → `--accent` - Width: 100% ### Tables - `border-collapse: collapse`, full width, 14px - `th`: 10px 12px padding, `--font-display`, 11px, 700, uppercase, `--text-muted`, bottom border, pointer cursor, hover → `--accent` - `td`: 12px 12px padding, bottom border - Row hover: background `rgba(0,212,255,0.03)` - Active sort column header: `--accent` colored - Active sort column values: `--accent`, 16px, weight 800 --- ## File Structure ``` statsphere/ ├── public/ │ ├── favicon.svg │ ├── icon-192.png │ ├── icon-512.png │ └── apple-touch-icon.png ├── src/ │ ├── components/ │ │ ├── Nav.jsx │ │ ├── Avatar.jsx │ │ ├── SportBadge.jsx │ │ └── PWABanner.jsx │ ├── data/ │ │ └── seedData.js │ ├── hooks/ │ │ └── useStore.jsx │ ├── pages/ │ │ ├── Home.jsx │ │ ├── Leaders.jsx │ │ ├── Filter.jsx │ │ ├── Athletes.jsx │ │ ├── AthleteDetail.jsx │ │ └── Register.jsx │ ├── App.jsx │ ├── main.jsx │ └── index.css ├── index.html ├── vite.config.js ├── package.json ├── install.sh └── README.md ``` --- ## Data Model ### Sports Registry (`SPORTS`) An object keyed by sport ID. Each entry: ```js { id: 'football', name: 'American Football', emoji: '🏈', color: '#4ade80', } ``` Sports: `football`, `hockey`, `baseball`, `soccer`, `basketball` ### Stat Definitions (`SPORT_STATS_DEFS`) An object keyed by sport ID. Each sport has an array of stat definition objects: ```js { key: 'touchdowns', label: 'TDs', type: 'number' } // type: 'text' | 'number' | 'decimal' // 'text' = select/dropdown (used for position) // 'number' = integer // 'decimal' = float, displayed with toFixed(2) ``` **Football (16 stats):** position (text), touchdowns, passing_yards, rushing_yards, receiving_yards, completions, attempts, completion_pct (decimal), interceptions, sacks (decimal), tackles, receptions, fumbles, field_goals, games_played, passer_rating (decimal) **Hockey (16 stats):** position (text), goals, assists, points, plus_minus, penalty_minutes, shots, shot_pct (decimal), games_played, save_pct (decimal), gaa (decimal), shutouts, toi (decimal), faceoff_pct (decimal), power_play_goals, shorthanded_goals **Baseball (16 stats):** position (text), batting_avg (decimal), home_runs, rbi, runs, hits, stolen_bases, obp (decimal), slg (decimal), ops (decimal), era (decimal), strikeouts_p, wins, whip (decimal), games_played, war (decimal) **Soccer (16 stats):** position (text), goals, assists, games_played, minutes_played, shots, shots_on_target, pass_accuracy (decimal), key_passes, dribbles, tackles, yellow_cards, red_cards, save_pct (decimal), clean_sheets, xg (decimal) **Basketball (16 stats):** position (text), points_per_game (decimal), rebounds_per_game (decimal), assists_per_game (decimal), steals_per_game (decimal), blocks_per_game (decimal), fg_pct (decimal), three_pt_pct (decimal), ft_pct (decimal), games_played, minutes_per_game (decimal), turnovers (decimal), plus_minus (decimal), efficiency (decimal), true_shooting (decimal), usage_rate (decimal) ### Biometric Fields (`BIOMETRIC_FIELDS`) 12 fields displayed on every athlete profile: ``` height_cm (number), weight_kg (number), age (number), reach_cm (number), dominant_hand (text), dominant_foot (text), body_fat_pct (decimal), vo2_max (decimal), vertical_jump_cm (number), 40_yard_dash (decimal), bench_press_reps (number), years_pro (number) ``` ### Athlete User Object ```js { id: String, // unique, Date.now() for new users firstName: String, lastName: String, name: String, // firstName + ' ' + lastName email: String, phone: String, city: String, country: String, bio: String, // max 300 chars socials: { facebook: String, // optional @handle instagram: String, bluesky: String, snapchat: String, }, avatarColor: String, // hex color for avatar fallback profileImage: String, // base64 data URL or empty string primarySport: String, // sport id sports: String[], // array of sport ids (1 or more) biometrics: { height_cm: Number, weight_kg: Number, age: Number, reach_cm: Number, dominant_hand: String, dominant_foot: String, body_fat_pct: Number, vo2_max: Number, vertical_jump_cm: Number, '40_yard_dash': Number, bench_press_reps: Number, years_pro: Number, }, sportStats: { football: { position, touchdowns, passing_yards, ... }, hockey: { position, goals, assists, ... }, // only sports the athlete is registered in }, joinDate: String, // ISO date string YYYY-MM-DD } ``` ### Positions Per Sport ```js football: ['QB','RB','WR','TE','OL','DE','DT','LB','CB','S','K'] hockey: ['C','LW','RW','D','G'] baseball: ['SP','RP','C','1B','2B','3B','SS','LF','CF','RF','DH'] soccer: ['GK','CB','LB','RB','CDM','CM','CAM','LW','RW','ST'] basketball: ['PG','SG','SF','PF','C'] ``` --- ## Seed Data Generation Generate **125 athlete records** (25 per sport). Approximately 30% of athletes play a second sport randomly selected from the remaining four. All data is fake but realistic. **Name pool:** - First names (30): Marcus, Tyler, Jordan, Devon, Chase, Zach, Ryan, Cole, Drew, Cody, Logan, Austin, Blake, Jalen, Myles, Trey, Dylan, Caleb, Eli, Noah, Liam, Aiden, Cameron, Hunter, Bryce, Jake, Luke, Owen, Seth, Finn - Last names (30): Thompson, Williams, Johnson, Davis, Martinez, Anderson, Taylor, Jackson, White, Harris, Martin, Garcia, Walker, Robinson, Lewis, Lee, Allen, Young, Hernandez, King, Wright, Scott, Torres, Nguyen, Hill, Flores, Green, Adams, Nelson, Baker **Biometric ranges by sport (height cm / weight kg):** - Football: h 175–205, w 85–140 - Hockey: h 175–198, w 82–105 - Baseball: h 170–200, w 82–110 - Soccer: h 165–195, w 70–90 - Basketball: h 185–220, w 85–120 **Common biometrics:** age 20–38, reach = height + 5–20, body_fat_pct 6–18%, vo2_max 48–72, vertical_jump 55–90cm, 40_yard_dash 4.30–5.20s, bench_press_reps 10–35, years_pro 1–18 **Stat generation is position-aware.** Key rules: - Football: QBs get passing_yards (2800–5200), rushers get rushing_yards, WR/TE get receiving_yards; defenders get tackles/sacks/interceptions; non-QBs get 0 for passing stats - Hockey: Goalies (G) get save_pct (0.900–0.935), gaa (1.8–3.5), shutouts (1–12); skaters get goals (5–55), assists, points; Centers get faceoff_pct (42–58%) - Baseball: Pitchers (SP/RP) get era (2.10–5.80), wins (4–22), whip (0.92–1.60), strikeouts_p; position players get batting_avg (0.220–0.350), home_runs, obp, slg, ops - Soccer: GK gets save_pct (65–82%), clean_sheets; defenders get fewer goals; attackers get more shots/xg - Basketball: Centers/PFs get higher rebounds; PGs get higher assists; all get per-game averages **Socials:** each athlete randomly gets 0–4 platforms (60% chance per platform), handle = `@firstname_lastname##` **Cities:** New York, Los Angeles, Chicago, Houston, Phoenix, Toronto, Vancouver, Dallas, Miami, Boston, Seattle, Denver, Atlanta, Minneapolis, Detroit **Countries:** 80% USA, 20% Canada/Mexico/UK/Brazil/Australia **Avatar colors pool:** `['#00d4ff','#ff6b35','#a855f7','#22c55e','#f59e0b','#ec4899','#06b6d4','#84cc16']` **Bio pool (10 rotating):** 1. "Dedicated athlete with a passion for the game and improving every day." 2. "Competitive spirit who gives 110% on and off the field." 3. "Team player first, stats second. Love the game, love my teammates." 4. "Training hard to be the best version of myself every season." 5. "Multiple-sport athlete who believes cross-training builds champions." 6. "Community advocate and sports mentor for youth programs." 7. "Playing to inspire the next generation of athletes." 8. "Fueled by competition and the pursuit of excellence." 9. "Veteran player with years of experience and a championship mindset." 10. "Rising star hungry for the next level." Export `SEED_USERS` as the merged, deduplicated array of all generated athletes. --- ## Global State (`useStore.jsx`) A React Context provider wrapping the entire app. Persists to `localStorage` under key `statsphere_users_v1`. Loads seed data on first run (when localStorage is empty). **Exposed API:** ```js { users, // User[] — full array addUser(user), // adds new user, generates id + joinDate, returns new user updateUser(id, updates), // merges updates into matching user deleteUser(id), // removes user by id getUserById(id), // returns single user or undefined getUsersBySport(sport), // returns users where sports array includes sport resetToSeed(), // restores SEED_USERS to localStorage } ``` Load order: try `JSON.parse(localStorage.getItem('statsphere_users_v1'))` → fall back to `SEED_USERS`. Save: `useEffect` on `users` changes → `localStorage.setItem(...)`. --- ## Components ### `Nav.jsx` Two navigation bars rendered simultaneously: **Desktop top bar** (visible ≥768px): - Fixed, full-width, height `--nav-h` (60px) - Background: `rgba(10,15,30,0.95)` with `backdrop-filter: blur(12px)` - Left: Logo mark (36px square div with gradient `#00d4ff → #a855f7`, 8px radius, "S" text) + wordmark "STAT**SPHERE**" where SPHERE is `--accent` colored - Right: horizontal nav links — Home, Leaders, Search, Athletes, Register - Active link: `--accent` colored text + `rgba(0,212,255,0.08)` background - Font: `--font-display`, 12px, 700, uppercase, letter-spacing 0.08em **Mobile bottom nav** (visible <768px): - Fixed, full-width, height `var(--bottom-nav-h)` (64px) - Background: `rgba(10,15,30,0.97)` with `backdrop-filter: blur(12px)` - 5 equal-width items: ⚡ Home, 🏆 Leaders, 🔍 Search, 👤 Athletes, ➕ Register - Each item: icon (20px emoji) above label (10px, uppercase, `--font-display`) - Active: `--accent` color; inactive: `--text-muted` - Accounts for `env(safe-area-inset-bottom)` padding Routes: `/` `/leaders` `/filter` `/athletes` `/register` ### `Avatar.jsx` Props: `user` object, `size` (number, default 40) If `user.profileImage` is set: render `` with `border-radius: 50%`, `object-fit: cover` Else: render div with: - `border-radius: 50%` - Background: `${user.avatarColor}22` (10% opacity) - Border: `2px solid ${user.avatarColor}44` (25% opacity) - Text: initials (`firstName[0] + lastName[0]`), `--font-display`, 800 weight, colored `user.avatarColor` - Font size: `size * 0.35` ### `SportBadge.jsx` Props: `sport` (string id) Renders: `{emoji} {name}` ### `PWABanner.jsx` Listens for the browser `beforeinstallprompt` event. If not already dismissed (check `localStorage.getItem('pwa_dismissed')`), shows a fixed banner above the bottom nav when the event fires. Banner content: - ⚡ icon - Title: "Install StatSphere" - Subtitle: "Add to home screen for the best experience" - "Install" button: calls `prompt.prompt()` on click - "×" dismiss button: sets `localStorage.setItem('pwa_dismissed','1')` and hides Styling: `.pwa-banner` class — `--bg-card2` background, 1px `--accent` border, 12px radius, box-shadow `0 4px 32px rgba(0,212,255,0.15)` --- ## Pages ### `Home.jsx` — Dashboard (`/`) **Hero section:** - Full-width, centered - Radial gradient overlay: `rgba(0,212,255,0.08)` at top, transparent by 60% - Heading (responsive font clamp 36px–72px, 900 weight): "YOUR STATS," (white) + line break + "YOUR LEGACY" (cyan) - Subtitle: 16px, `--text-secondary`, max-width 500px - Two CTA buttons: "Register Athlete" (primary) + "View Leaders" (secondary) **Stats overview:** 3-column `.grid-3` of `.card` tiles — Total Athletes, Number of Sports (5), Multi-Sport Athletes count **Sports grid:** - `repeat(auto-fill, minmax(160px, 1fr))` grid - Each sport is a card with: emoji (36px), sport name (13px, uppercase, sport's color), athlete count (12px, muted) - Links to `/leaders?sport={id}` - Hover: border-color → sport.color, translateY(-2px) **Recent Athletes:** last 5 athletes by joinDate, shown as a stacked list inside a single `.card`. Each row: Avatar (42px) + name/city + sport badges. Links to `/athletes/:id`. --- ### `Leaders.jsx` — Stat Leaders (`/leaders`) **Controls:** - Sport tab bar (5 tabs, horizontal scroll on mobile) - "Sort By" select: all numeric stats for the current sport - "Direction" select: "High → Low" / "Low → High" - "Show" select: 25 (default) / 50 / 100 **Behavior:** - Reads `?sport=` query param on mount to set initial sport - When sport changes, resets sort key to the second numeric stat for that sport (first meaningful stat after position) - Filtered list = `getUsersBySport(sport)` → only users with `sportStats[sport]` defined → sorted by selected stat → sliced to row limit **Table columns:** - `#` — rank badge (1/2/3 gold/silver/bronze, rest neutral) - Athlete — Avatar (34px) + name (link to profile) + city - Pos — position string from sportStats - 10 numeric stat columns (first 10 numeric stats for the sport, skipping `position`) **Active sort column:** header colored `--accent` with ↑/↓ arrow; cell values colored `--accent`, 16px, 800 weight **Value formatting:** `0` or null/undefined → display `—`; type `decimal` → `.toFixed(2)`; type `number` → raw value **Table is horizontally scrollable** on mobile via `overflow-x: auto` wrapper Footer: "Showing N athletes · Sorted by {stat label}" --- ### `Filter.jsx` — Search & Filter (`/filter`) Three filter card sections: **Section 1 — General Search:** - Text input: search by name, city, country, bio (case-insensitive substring) - "Sport" select: All Sports + 5 sports - "Position" select: appears only when a specific sport is selected; populated dynamically from unique positions found in that sport's data **Section 2 — Sport Stat Filter:** - "Sport" select (defaults to football) - "Stat" select: numeric stats for the selected sport - Min / Max number inputs (2-column inline grid) - Filters athletes where `sportStats[filterSport][filterKey]` is within [min, max] **Section 3 — Biometric Filter:** - "Biometric" select: all non-text biometric fields - Min / Max number inputs **Result controls:** - Sort By: Name / Age / Join Date - Order: A→Z/Low→High or Z→A/High→Low - Show: 25 / 50 / 100 - "Clear All" button resets every filter state **Results list:** flat stacked list inside `.card`, same row format as Home recent athletes (Avatar + name/city/age/height + sport badges). Links to athlete profile. Shows count above list. Empty state: "No athletes match your filters" Filtering is applied in order: text search → sport → position → stat filter → biometric filter → sort → slice. --- ### `Athletes.jsx` — Athlete List (`/athletes`) **Controls:** Text search (name or city) + sport dropdown filter, shown inline **Grid:** `repeat(auto-fill, minmax(280px, 1fr))` **Each card:** - Avatar (48px) + name + city/country - Sport badges row - 3 quick-stat tiles in a flex row: Age, Height (cm), Years Pro — each with `--font-display` value in `--accent` and tiny uppercase label - Hover: border-color → `--accent`, translateY(-2px) - Links to `/athletes/:id` "+ Add" button (primary, small) top-right links to `/register` Empty state: centered message --- ### `AthleteDetail.jsx` — Athlete Profile (`/athletes/:id`) If athlete not found: 🏟️ emoji + "Athlete not found" + back link **Header card:** - Gradient background: `linear-gradient(135deg, --bg-card 0%, --bg-card2 100%)` - Avatar (80px) + name (32px, 900 weight) + city/country + join date - Sport badges row - Bio in italic quotes - Edit link → `/register?edit={id}` (secondary button) - Delete button (danger) with `confirm()` dialog → deletes and navigates to `/athletes` Social media row (if any socials exist): - Separator line above - Icons: 📘 facebook, 📷 instagram, 🦋 bluesky, 👻 snapchat - Handle text next to each icon **Contact card:** - Email (colored `--accent`) + Phone in 2-column grid **Biometrics card:** - `repeat(auto-fill, minmax(130px, 1fr))` grid - Each field as a `.card2` with `.stat-lbl` + `.stat-val` - Decimal fields: `.toFixed(1)` — skip fields with no value **Sport Stats card:** - If athlete has >1 sport: show sport tab bar to switch between sports - Active sport stats shown as `repeat(auto-fill, minmax(130px, 1fr))` grid of `.card2` tiles - Skip stats with value `0` or undefined (avoids showing irrelevant stats for wrong position) - `text` type stats (position): show as `.stat-val` at 16px - `decimal` type: `.toFixed(2)` Back link → `/athletes` --- ### `Register.jsx` — Registration / Edit Form (`/register`) **Dual mode:** if `?edit={id}` query param is present, loads existing user data and updates on submit. Otherwise creates new user. **4-step wizard:** step indicator bar at top shows all 4 steps with completion state (✓ for completed, number for incomplete). Clicking a completed step navigates back to it. Steps: `['Personal Info', 'Biometrics', 'Sport Stats', 'Review']` --- #### Step 1 — Personal Info **Profile photo section:** - 80px circular avatar preview (shows initials or uploaded image) - File input for photo upload → reads as base64 DataURL → stored in `form.profileImage` - 6 avatar color swatches (circular buttons): `['#00d4ff','#ff6b35','#a855f7','#22c55e','#f59e0b','#ec4899']` — active swatch has 3px white border **Personal fields (2-column grid):** - First Name * (required) - Last Name * (required) - Email * (required) - Phone - City - Country **Bio field:** `