113 lines
5.2 KiB
Markdown
113 lines
5.2 KiB
Markdown
# 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
|
|
1. **[index.html](index.html)** + [script.js](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 →"
|
|
|
|
2. **[planning.html](planning.html)** + [planning.js](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](planning.js#L1-L3)):
|
|
- `turnierplaner_data`: Teams + field count (shared between pages)
|
|
- `turnierplaner_rotation`: Current round number + team order per league
|
|
- `turnierplaner_scores`: Match results keyed by `"league:fieldNum"`
|
|
|
|
**Critical Logic**: Field allocation in [planning.js](planning.js#L63-L76)
|
|
```javascript
|
|
// 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](planning.js#L574-L611)): 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](planning.js#L544-L565)): Winner gets `|score1 - score2| + 2`, loser gets `-|score1 - score2|`, tie = 0
|
|
|
|
## Development Workflows
|
|
|
|
### Running Locally
|
|
```bash
|
|
# No build step - open directly in browser
|
|
start index.html # Windows
|
|
# Or use a local server for better CORS handling
|
|
npx serve .
|
|
```
|
|
|
|
### Testing Changes
|
|
1. Open browser DevTools → Application → Local Storage
|
|
2. Clear `turnierplaner_*` keys to reset state
|
|
3. Use Export/Import JSON to preserve test data
|
|
|
|
### Debugging Rotation Logic
|
|
Add breakpoints in [planning.js](planning.js#L574) `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()` with `display: block/none`
|
|
- Auto-save: Input event listeners trigger `saveData()` ([script.js](script.js#L94-L99))
|
|
|
|
### Score Input Pattern ([planning.js](planning.js#L116-L120))
|
|
```javascript
|
|
// Inline onchange in field card HTML
|
|
onchange="updateMatchScore('bundesliga', 1, this.value, otherInput.value)"
|
|
```
|
|
|
|
### XSS Prevention
|
|
Use `escapeHtml()` ([script.js](script.js#L203-L210)) for user input in team names/clubs/mottos
|
|
|
|
### Mobile-First CSS ([styles.css](styles.css))
|
|
- `viewport-fit=cover` for iOS safe areas
|
|
- Touch-optimized: `min-height: 44px`, `touch-action: manipulation`
|
|
- Responsive grid: `grid-template-columns: 1fr 1fr` → `1fr` on mobile
|
|
|
|
## Integration Points & Dependencies
|
|
|
|
### External Dependencies
|
|
None - pure vanilla JavaScript
|
|
|
|
### Browser APIs
|
|
- **LocalStorage**: All persistence ([script.js](script.js#L1), [planning.js](planning.js#L1-L3))
|
|
- **File API**: JSON import via `<input type="file">` ([script.js](script.js#L180-L200))
|
|
- **Blob API**: JSON export download ([script.js](script.js#L166-L177))
|
|
|
|
### Cross-Page Communication
|
|
Exclusively via localStorage keys. [index.html](index.html) writes `turnierplaner_data`, [planning.html](planning.html) reads it on load ([planning.js](planning.js#L12-L14)).
|
|
|
|
## Important Gotchas
|
|
|
|
1. **Field Count Logic**: `fieldCount` from user input = physical fields. Each field has 2 halves, so max `playingTeamsCount = fieldCount * 2`
|
|
2. **Field Numbering Offset**: Champions League fields start after Bundesliga fields ([planning.js](planning.js#L65-L69))
|
|
3. **Round State Mismatch**: If `rotationState` deleted but `matchScores` exists, scores reference wrong teams
|
|
4. **Timer Doesn't Persist**: Timer state (`timerSeconds`, `isRunning`) not saved to localStorage
|
|
5. **Deletion Side Effects**: Deleting teams in [index.html](index.html) doesn't invalidate [planning.html](planning.html) rotation state
|