A web app for netball coaches and managers to track player court time, plan match lineups, and manage team rosters. Built to solve the common challenge of ensuring fair playing time distribution and managing substitutions during games and tournaments.
Live site: netball.forgesync.co.nz
Create and manage team rosters with customizable player lists. Rosters are reusable across multiple games, tournaments, and match plans, providing a single source of truth for your squad.
Use cases:
Run a live game with real-time tracking that stays accurate even when you switch browser tabs. Features include:
Design insight: The timer uses timestamp-based calculation rather than interval counters to prevent drift when the browser tab is backgroundedâa common issue when coaches check game schedules or messages mid-match.
Pre-plan lineups for each period before a game to visualize rotation strategies:
Use cases:
Group multiple matches into tournaments with intelligent auto-generation:
Design insight: Tournament Auto-Generation
The tournament scheduler solves a complex optimization problem: given N players and M matches, assign positions to maximize fairness across two dimensions:
The algorithm uses a greedy weighted scoring system with per-game constraints:
Player Score = (Court Time Weight Ă Minutes Played) +
(Position Zone Weight Ă Appearances in Zone)
For each position in each period, the scheduler:
Why greedy vs. exhaustive search? A tournament with 12 players and 5 matches has roughly 10^50 possible assignments. The greedy approach produces near-optimal results in milliseconds rather than hours, with diminishing returns from perfect optimization (a 2-minute difference across 200 minutes is negligible for grassroots netball).
Per-game fairness: Without per-game limits, the algorithm could create âbench-heavyâ matches where some players barely play. The per-game cap (e.g., 75% of periods) ensures every match has reasonable rotation, preventing frustration for players and parents.
Flexible sign-in options powered by Firebase Auth:
main)npm install -g wrangler)git clone https://github.com/s3w3ll/NetballRosterTracker.git
cd NetballRosterTracker
npm install
.env.local file with your Firebase config:
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
NEXT_PUBLIC_FIREBASE_APP_ID=your-app-id
NEXT_PUBLIC_FIREBASE_API_KEY=your-api-key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your-sender-id
NEXT_PUBLIC_WORKER_URL=http://localhost:8787
npm run dev
cd worker
npm install
npm run db:apply
npm run dev
Frontend:
npm run dev # Dev server on :9002 (turbopack)
npm run build # Static export â out/
npm run typecheck # TypeScript validation (3 known pre-existing errors â ignore unless you changed types)
npm run test # Run Vitest tests
npm run test:watch # Watch mode for tests
Worker:
cd worker
npm run dev # Local dev server
npm run deploy # Deploy to Cloudflare Workers
npm run db:apply # Apply schema to local D1
npm run db:apply:remote # Apply schema to remote D1
npm run typecheck # TypeScript validation (1 known pre-existing error in auth.ts â ignore unless you changed auth)
Frontend:
npm run build
Static output is generated in the out/ directory, ready for GitHub Pages or any static hosting.
Worker:
cd worker
npm run deploy
Deploys to Cloudflare Workers. Configure ALLOWED_ORIGINS environment variable in Cloudflare dashboard to control CORS.
The app deploys automatically on push to main:
Cost: Firestore pricing scales with read/write operations. A tournament with 12 players and 5 matches generates thousands of reads during planning. Cloudflare D1 has a generous free tier (100k reads/day) and predictable pricing.
Performance: D1 SQL queries with JOINs are more efficient than Firestoreâs document-based reads. Tournament aggregation (sum court time across matches) is a single SQL query vs. multiple document reads + client-side aggregation.
Offline resilience: SQLiteâs relational model is easier to reason about for complex queries and data integrity (cascading deletes, foreign keys).
Static export (output: 'export') doesnât support true dynamic routes. Initially used generateStaticParams() with dummy params, but this created confusing UX (bookmarking /games/[gameId] breaks on refresh).
The localStorage approach (setNavId('gameId', id) â navigate to /games/play) makes URLs stable and bookmarkable while keeping the app fully static.
The scheduler runs client-side in the browser. A greedy algorithm produces excellent results in <100ms. ML approaches (genetic algorithms, constraint solvers) would require:
Greedy is transparent, fast, and âgood enoughâ for grassroots sports.
This is a personal project built for local netball clubs, but contributions are welcome! If you have ideas or find bugs, feel free to open an issue or submit a PR.
MIT License - see LICENSE file for details.