🥏 Disc Agenda — Frisbee Tournament Platform

A self-hosted tournament management web app for ultimate frisbee, featuring live multiplayer scoring via WebSockets, a mobile-friendly questionnaire with QR codes, and a clean athletic visual design.

Architecture

┌──────────────────────────────────────────┐
│  Go 1.26 Backend (single binary)         │
│  ├─ REST API  (gorilla/mux)              │
│  ├─ WebSocket hub (gorilla/websocket)    │
│  ├─ SPA file server                      │
│  └─ File-based JSON storage              │
├──────────────────────────────────────────┤
│  React 19 + Vite 8 Frontend              │
│  ├─ react-router-dom v7 (SPA)            │
│  ├─ qrcode.react (QR generation)         │
│  └─ Custom CSS (Bebas Neue + Barlow)     │
├──────────────────────────────────────────┤
│  Data: flat JSON files + JSONL audit logs │
│  (no database required)                  │
└──────────────────────────────────────────┘

Features

  • Tournament hub — home page with location, dates, teams, rules
  • Schedule — round-grouped game schedule with status badges
  • Live scoring — WebSocket-powered multiplayer scoreboard with +/- and SET controls
  • Audit log — every score change persisted as JSONL per game
  • Questionnaire — mobile-friendly survey with QR code, team selectors, spirit voting, custom questions
  • Results — final standings table with spirit award highlight
  • Past tournaments — archive of completed events

Quick Start

docker compose up --build
# → http://localhost:8080

Data persists in a Docker volume. Seed data auto-copies on first run.

Local Development

Prerequisites: Go 1.26+, Node 22+

Terminal 1 — backend:

cd backend
go mod tidy
go run ./cmd/server -data ../data -static ../frontend/dist -port 8080

Terminal 2 — frontend (with hot reload + API proxy):

cd frontend
npm install
npm run dev
# → http://localhost:5173 (proxies /api and /ws to :8080)

Project Structure

├── backend/
│   ├── cmd/server/main.go          # Entry point, router, SPA handler
│   └── internal/
│       ├── handlers/handlers.go    # REST + WS HTTP handlers
│       ├── models/models.go        # Domain types
│       ├── storage/storage.go      # File-based persistence
│       └── websocket/hub.go        # Per-game WS hub + broadcast
├── frontend/
│   ├── src/
│   │   ├── api.js                  # API client + WS factory
│   │   ├── App.jsx                 # Router
│   │   ├── main.jsx                # Entry
│   │   ├── components/             # Header, Footer, Icons
│   │   ├── pages/                  # All page components
│   │   └── styles/global.css       # Full stylesheet
│   ├── vite.config.js              # Dev proxy config
│   └── index.html
├── data/                           # Seed data (JSON files)
│   ├── tournaments.json
│   └── tournaments/{id}/
│       ├── schedule.json
│       ├── questionnaire_config.json
│       ├── results.json
│       └── games/                  # Score state + audit logs
├── Dockerfile                      # Multi-stage build
├── docker-compose.yml
└── Makefile

API Reference

Method Endpoint Description
GET /api/tournaments List all tournaments
GET /api/tournaments/{id} Tournament details
GET /api/tournaments/{id}/schedule Game schedule
GET /api/tournaments/{id}/games/{gid}/score Current score
POST /api/tournaments/{id}/games/{gid}/score Update score (REST)
GET /api/tournaments/{id}/games/{gid}/audit Audit log
WS /ws/game/{id}/{gid}?user_id=x Live score WebSocket
GET /api/tournaments/{id}/questionnaire Questionnaire config + teams
POST /api/tournaments/{id}/questionnaire Submit response
GET /api/tournaments/{id}/questionnaire/results All responses (admin)
GET /api/tournaments/{id}/results Final standings

WebSocket Protocol

Connect: ws://host/ws/game/{tourneyId}/{gameId}?user_id=alice

Send (client → server):

{"action": "increment", "team": "home"}
{"action": "decrement", "team": "away"}
{"action": "set", "team": "home", "value": 12}

Receive (server → all clients):

{
  "type": "score_update",
  "state": {"game_id": "g01", "home_score": 8, "away_score": 6, ...},
  "audit": {"action": "increment", "team": "home", "old_home": 7, ...}
}

Configuration

All via flags or env vars:

Flag Env Default Description
-port PORT 8080 Listen port
-data DATA_DIR ./data Data directory
-static ./static Frontend files

Adding Tournament Data

Edit JSON files directly in data/:

  • tournaments.json — add/edit tournament objects
  • tournaments/{id}/schedule.json — game schedule
  • tournaments/{id}/questionnaire_config.json — custom survey questions
  • tournaments/{id}/results.json — final standings

No admin UI yet — data is managed via files. Future: add admin panel.

Tech Stack

  • Backend: Go 1.26, gorilla/mux 1.8.1, gorilla/websocket 1.5.3, rs/cors
  • Frontend: React 19, Vite 8, react-router-dom 7, qrcode.react
  • Storage: Flat JSON files + JSONL audit logs
  • Container: Alpine 3.21, multi-stage Docker build

Documentation

  • Technical Documentation — detailed API reference, data models, WebSocket protocol, deployment guides, troubleshooting
  • Project Description — project overview, motivation, features, design philosophy, roadmap
Description
No description provided
Readme 99 KiB
Languages
JavaScript 48.3%
Go 28.5%
CSS 19.8%
Makefile 1.4%
Dockerfile 1.3%
Other 0.7%