5.2 KiB
Copilot Instructions for Turnierplaner
Project Overview
Turnierplaner (Tournament Planner) is a German-language, client-side volleyball tournament management app for the "NVJ" organization. It handles team registration, round-robin rotation logic, live scoring, and leaderboards for two leagues: "Bundesliga" and "Champions League".
Tech Stack
- Frontend: Vanilla HTML, CSS, JavaScript (ES6+)
- Persistence: LocalStorage for all data (no backend)
- UI Pattern: Multi-page app with shared state via localStorage
- Build: None - runs directly in browser via file:// or static server
Architecture & Key Components
Two-Page Structure
-
index.html + script.js: Team entry interface
- Add/remove teams with name, club, and motto fields
- Set field count (used to calculate fields per league)
- Export/import tournament configuration as JSON
- Navigate to planning view with "Weiter →"
-
planning.html + planning.js: Live tournament management
- Display current round matchups on fields (grid layout: field number | team 1 | team 2)
- Show waiting teams below the playing fields
- Live score input per match with automatic point calculation
- Round rotation system with "Nächste Runde" button
- Timer with custom input (MM:SS or seconds)
- Modals for: Points History, Scoreboard (rankings), Reset confirmation
Data Flow & State Management
Three localStorage Keys (planning.js):
turnierplaner_data: Teams + field count (shared between pages)turnierplaner_rotation: Current round number + team order per leagueturnierplaner_scores: Match results keyed by"league:fieldNum"
Critical Logic: Field allocation in planning.js
// fieldCount = number of physical fields (from user input)
// Each field has 2 halves, so max playing teams = fieldCount * 2
const maxPlayingTeams = fieldCount * 2;
const playingTeamsCount = Math.min(teams.length, maxPlayingTeams);
const fieldsPerLeague = Math.ceil(playingTeamsCount / 2);
Round Rotation (planning.js): Three rotation strategies based on waiting team count:
- Fall 1 (0 waiting, teams === fieldCount * 2): Team at index 0 stays, team at index 1 moves to end
- Fall 2 (1 waiting, teams === fieldCount * 2 + 1): All rotate by 1, first team goes to end
- Fall 3 (2+ waiting, teams > fieldCount * 2 + 1): Swap playing/waiting blocks entirely
Point Calculation (planning.js): Winner gets |score1 - score2| + 2, loser gets -|score1 - score2|, tie = 0
Development Workflows
Running Locally
# No build step - open directly in browser
start index.html # Windows
# Or use a local server for better CORS handling
npx serve .
Testing Changes
- Open browser DevTools → Application → Local Storage
- Clear
turnierplaner_*keys to reset state - Use Export/Import JSON to preserve test data
Debugging Rotation Logic
Add breakpoints in planning.js rotateLeague(). Check rotationState.teamOrder vs actual team indices.
Code Conventions & Patterns
Naming & Structure
- German UI text (labels, buttons, alerts), English code (variables, functions)
- Global functions (no modules):
onclick="functionName()"in HTML - Modal pattern:
open{Name}Modal()/close{Name}Modal()withdisplay: block/none - Auto-save: Input event listeners trigger
saveData()(script.js)
Score Input Pattern (planning.js)
// Inline onchange in field card HTML
onchange="updateMatchScore('bundesliga', 1, this.value, otherInput.value)"
XSS Prevention
Use escapeHtml() (script.js) for user input in team names/clubs/mottos
Mobile-First CSS (styles.css)
viewport-fit=coverfor iOS safe areas- Touch-optimized:
min-height: 44px,touch-action: manipulation - Responsive grid:
grid-template-columns: 1fr 1fr→1fron mobile
Integration Points & Dependencies
External Dependencies
None - pure vanilla JavaScript
Browser APIs
- LocalStorage: All persistence (script.js, planning.js)
- File API: JSON import via
<input type="file">(script.js) - Blob API: JSON export download (script.js)
Cross-Page Communication
Exclusively via localStorage keys. index.html writes turnierplaner_data, planning.html reads it on load (planning.js).
Important Gotchas
- Field Count Logic:
fieldCountfrom user input = physical fields. Each field has 2 halves, so maxplayingTeamsCount = fieldCount * 2 - Field Numbering Offset: Champions League fields start after Bundesliga fields (planning.js)
- Round State Mismatch: If
rotationStatedeleted butmatchScoresexists, scores reference wrong teams - Timer Doesn't Persist: Timer state (
timerSeconds,isRunning) not saved to localStorage - Deletion Side Effects: Deleting teams in index.html doesn't invalidate planning.html rotation state