initial commit
This commit is contained in:
commit
f67d869005
16
.dockerignore
Normal file
16
.dockerignore
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.github
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Dokumentation (optional - auskommentieren falls benötigt)
|
||||||
|
# README.md
|
||||||
112
.github/copilot-instructions.md
vendored
Normal file
112
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# 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
|
||||||
101
DOCKER.md
Normal file
101
DOCKER.md
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# Turnierplaner - Docker Deployment
|
||||||
|
|
||||||
|
## Docker Setup
|
||||||
|
|
||||||
|
Die Anwendung ist als statischer Container mit nginx konfiguriert.
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
- Docker Desktop für Windows installiert und gestartet
|
||||||
|
- Docker Compose (ist in Docker Desktop enthalten)
|
||||||
|
|
||||||
|
### Anwendung starten
|
||||||
|
|
||||||
|
**Option 1: Mit Docker Compose (empfohlen)**
|
||||||
|
```powershell
|
||||||
|
# Container bauen und starten
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Logs anzeigen
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Container stoppen
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Mit Docker direkt**
|
||||||
|
```powershell
|
||||||
|
# Image bauen
|
||||||
|
docker build -t turnierplaner:latest .
|
||||||
|
|
||||||
|
# Container starten
|
||||||
|
docker run -d -p 8080:80 --name turnierplaner turnierplaner:latest
|
||||||
|
|
||||||
|
# Container stoppen
|
||||||
|
docker stop turnierplaner
|
||||||
|
docker rm turnierplaner
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anwendung aufrufen
|
||||||
|
|
||||||
|
Nach dem Start ist die Anwendung erreichbar unter:
|
||||||
|
- **http://localhost:8080**
|
||||||
|
|
||||||
|
### Nützliche Befehle
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Container Status prüfen
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
# Container Logs anzeigen
|
||||||
|
docker logs turnierplaner
|
||||||
|
|
||||||
|
# In Container einsteigen (für Debugging)
|
||||||
|
docker exec -it turnierplaner sh
|
||||||
|
|
||||||
|
# Image neu bauen (nach Änderungen)
|
||||||
|
docker-compose build
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Alle Container/Images aufräumen
|
||||||
|
docker-compose down
|
||||||
|
docker system prune -a
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port ändern
|
||||||
|
|
||||||
|
Falls Port 8080 bereits belegt ist, ändere in [docker-compose.yml](docker-compose.yml):
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "3000:80" # Statt 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Daten-Persistenz
|
||||||
|
|
||||||
|
Die Anwendung speichert alle Daten im Browser LocalStorage. Die Daten bleiben auch nach Container-Neustart erhalten, solange du denselben Browser verwendest.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
**Port bereits belegt:**
|
||||||
|
```powershell
|
||||||
|
# Prüfe welcher Prozess Port 8080 nutzt
|
||||||
|
netstat -ano | findstr :8080
|
||||||
|
|
||||||
|
# Stoppe den Prozess oder ändere den Port in docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Container startet nicht:**
|
||||||
|
```powershell
|
||||||
|
# Prüfe Logs
|
||||||
|
docker logs turnierplaner
|
||||||
|
|
||||||
|
# Prüfe ob Docker Desktop läuft
|
||||||
|
docker ps
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nach Code-Änderungen:**
|
||||||
|
```powershell
|
||||||
|
# Container stoppen und neu bauen
|
||||||
|
docker-compose down
|
||||||
|
docker-compose build --no-cache
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Multi-stage build für minimales Image
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Kopiere alle statischen Dateien
|
||||||
|
COPY index.html /usr/share/nginx/html/
|
||||||
|
COPY planning.html /usr/share/nginx/html/
|
||||||
|
COPY script.js /usr/share/nginx/html/
|
||||||
|
COPY planning.js /usr/share/nginx/html/
|
||||||
|
COPY styles.css /usr/share/nginx/html/
|
||||||
|
|
||||||
|
# Kopiere nginx Konfiguration
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Exponiere Port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# nginx läuft automatisch im Vordergrund
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
turnierplaner:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: turnierplaner
|
||||||
|
ports:
|
||||||
|
- "18080:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- turnierplaner-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
turnierplaner-network:
|
||||||
|
driver: bridge
|
||||||
64
index.html
Normal file
64
index.html
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>Turnierplaner - Team-Eingabe</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🏐 NVJ Turnierplaner</h1>
|
||||||
|
|
||||||
|
<div class="settings">
|
||||||
|
<div class="settings-group">
|
||||||
|
<label for="fieldCount">Anzahl Felder:</label>
|
||||||
|
<input type="number" id="fieldCount" min="1" value="1" placeholder="z.B. 4">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="leagues-container">
|
||||||
|
<!-- Bundesliga -->
|
||||||
|
<div class="league">
|
||||||
|
<div class="league-header">
|
||||||
|
<div class="league-title">
|
||||||
|
<h2>Bundesliga</h2>
|
||||||
|
<div class="team-count" id="bundesliga-count">0 Teams</div>
|
||||||
|
</div>
|
||||||
|
<button class="add-team-btn" onclick="addTeam('bundesliga')">+</button>
|
||||||
|
</div>
|
||||||
|
<div class="teams-list" id="bundesliga-list">
|
||||||
|
<div class="empty-message">Keine Teams hinzugefügt. Klicke auf + um zu beginnen!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Champions League -->
|
||||||
|
<div class="league">
|
||||||
|
<div class="league-header">
|
||||||
|
<div class="league-title">
|
||||||
|
<h2>Champions League</h2>
|
||||||
|
<div class="team-count" id="champions-count">0 Teams</div>
|
||||||
|
</div>
|
||||||
|
<button class="add-team-btn" onclick="addTeam('champions')">+</button>
|
||||||
|
</div>
|
||||||
|
<div class="teams-list" id="champions-list">
|
||||||
|
<div class="empty-message">Keine Teams hinzugefügt. Klicke auf + um zu beginnen!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<div class="export-buttons">
|
||||||
|
<button class="export-btn" onclick="exportData()">📥 Daten exportieren (JSON)</button>
|
||||||
|
<button class="export-btn" onclick="importData()">📤 Daten importieren</button>
|
||||||
|
<input type="file" id="import-file" accept=".json" style="display: none;" onchange="handleFileImport(event)">
|
||||||
|
</div>
|
||||||
|
<button class="next-btn" onclick="navigateToPlanning()">Weiter →</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="export-output"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
28
nginx.conf
Normal file
28
nginx.conf
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Gzip Kompression für bessere Performance
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/css application/javascript application/json;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache-Control Header für statische Assets
|
||||||
|
location ~* \.(js|css)$ {
|
||||||
|
expires 1d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keine Cache für HTML-Dateien
|
||||||
|
location ~* \.(html)$ {
|
||||||
|
expires -1;
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||||
|
}
|
||||||
|
}
|
||||||
721
planning.html
Normal file
721
planning.html
Normal file
@ -0,0 +1,721 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>Turnierplaner - Planung</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<style>
|
||||||
|
.planning-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
min-height: 44px;
|
||||||
|
touch-action: manipulation;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
background: white;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-btn {
|
||||||
|
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95em;
|
||||||
|
min-height: 44px;
|
||||||
|
touch-action: manipulation;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-display {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2ecc71;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-control-btn {
|
||||||
|
background: #2ecc71;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
min-height: 40px;
|
||||||
|
touch-action: manipulation;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-control-btn:hover {
|
||||||
|
background: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-input {
|
||||||
|
width: 100px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 2px solid #2ecc71;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #27ae60;
|
||||||
|
box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.fields-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields-header {
|
||||||
|
padding: 15px;
|
||||||
|
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
|
||||||
|
color: white;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields-grid {
|
||||||
|
padding: 15px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 120px 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-info-box {
|
||||||
|
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
min-height: 60px;
|
||||||
|
border: 2px solid #1e8449;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-info-number {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-info-team {
|
||||||
|
display: none;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-teams-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-top: 2px solid #e0e0e0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-teams-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-teams-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-team-badge {
|
||||||
|
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-team-badge::before {
|
||||||
|
content: "⏳";
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-waiting-teams {
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 80px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-card:hover {
|
||||||
|
border-color: #2ecc71;
|
||||||
|
background: #f0fdf4;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-name {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-input {
|
||||||
|
width: 50px;
|
||||||
|
padding: 6px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
animation: fadeIn 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
margin: 5% auto;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: slideIn 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateY(-50px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 2px solid #2ecc71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.8em;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #999;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1em;
|
||||||
|
min-height: 44px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2ecc71;
|
||||||
|
box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 2px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 44px;
|
||||||
|
touch-action: manipulation;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-display-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 30px;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.league-points-section {
|
||||||
|
border: 2px solid #2ecc71;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.league-points-header {
|
||||||
|
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-table th,
|
||||||
|
.points-table td {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-table th {
|
||||||
|
background: #f0fdf4;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2ecc71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-table td:first-child,
|
||||||
|
.points-table th:first-child {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 12px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-table tr:nth-child(even) {
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-table tr:hover {
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-value {
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-value.positive {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-value.negative {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-value.neutral {
|
||||||
|
background: #e2e3e5;
|
||||||
|
color: #383d41;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-total {
|
||||||
|
font-weight: 700;
|
||||||
|
background: #2ecc71;
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-league {
|
||||||
|
border: 2px solid #3498db;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-league-header {
|
||||||
|
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-item:hover {
|
||||||
|
background: #f0f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-rank {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #3498db;
|
||||||
|
min-width: 35px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-team-name {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-points {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
min-width: 70px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-points.positive {
|
||||||
|
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-points.negative {
|
||||||
|
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="planning-header">
|
||||||
|
<button class="back-btn" onclick="goBack()">← Zurück</button>
|
||||||
|
<h1 style="flex: 1; text-align: center; margin: 0; font-size: 1.3em; color: #333;">🏐 Turnierplanung</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-bar">
|
||||||
|
<button class="menu-btn" onclick="openPointsModal()">📊 Punkte anzeigen</button>
|
||||||
|
<button class="menu-btn" onclick="openScoreboard()" style="background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);">🏆 Scoreboard</button>
|
||||||
|
<button class="menu-btn" onclick="nextRound()" style="background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%); margin-left: auto; margin-right: auto;">⚡ Nächste Runde</button>
|
||||||
|
<button class="menu-btn" onclick="confirmReset()" style="background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); margin-left: auto;">🔄 Zurücksetzen</button>
|
||||||
|
<div style="margin-left: auto; display: flex; align-items: center; gap: 10px;">
|
||||||
|
<span style="font-weight: 600; color: #333;">Spielzeit:</span>
|
||||||
|
<div class="timer-section">
|
||||||
|
<div class="timer-display" id="timerDisplay">00:00</div>
|
||||||
|
<button class="timer-control-btn" id="timerBtn" onclick="toggleTimer()">Start</button>
|
||||||
|
<input type="text" class="timer-input" id="timerInput" placeholder="Sek. oder MM:SS" title="Sekunden oder MM:SS eingeben und Enter drücken">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fields-container">
|
||||||
|
<!-- Bundesliga Felder -->
|
||||||
|
<div class="fields-section">
|
||||||
|
<div class="fields-header">
|
||||||
|
<h3>Bundesliga</h3>
|
||||||
|
</div>
|
||||||
|
<div class="fields-grid" id="bundesliga-fields">
|
||||||
|
</div>
|
||||||
|
<div class="waiting-teams-section">
|
||||||
|
<div class="waiting-teams-label">Wartende Teams:</div>
|
||||||
|
<div class="waiting-teams-list" id="bundesliga-waiting">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Champions League Felder -->
|
||||||
|
<div class="fields-section">
|
||||||
|
<div class="fields-header">
|
||||||
|
<h3>Champions League</h3>
|
||||||
|
</div>
|
||||||
|
<div class="fields-grid" id="champions-fields">
|
||||||
|
</div>
|
||||||
|
<div class="waiting-teams-section">
|
||||||
|
<div class="waiting-teams-label">Wartende Teams:</div>
|
||||||
|
<div class="waiting-teams-list" id="champions-waiting">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Punkte Modal -->
|
||||||
|
<div id="pointsModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Punkte anzeigen</h2>
|
||||||
|
<button class="close-btn" onclick="closePointsModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="points-display-content" id="pointsDisplayContent">
|
||||||
|
<!-- Wird dynamisch gefüllt -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scoreboard Modal -->
|
||||||
|
<div id="scoreboardModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>🏆 Scoreboard</h2>
|
||||||
|
<button class="close-btn" onclick="closeScoreboard()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="scoreboard-content" id="scoreboardContent">
|
||||||
|
<!-- Wird dynamisch gefüllt -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reset Confirmation Modal -->
|
||||||
|
<div id="resetConfirmModal" class="modal">
|
||||||
|
<div class="modal-content" style="max-width: 400px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>⚠️ Turnier zurücksetzen</h2>
|
||||||
|
<button class="close-btn" onclick="closeResetConfirm()">×</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 20px; text-align: center;">
|
||||||
|
<p style="color: #333; margin-bottom: 20px; font-size: 1.05em;">
|
||||||
|
Alle Runden, Scores und Punkte werden gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!
|
||||||
|
</p>
|
||||||
|
<div style="display: flex; gap: 10px; justify-content: center;">
|
||||||
|
<button class="cancel-btn" onclick="closeResetConfirm()">Abbrechen</button>
|
||||||
|
<button class="submit-btn" style="background: #e74c3c;" onclick="performReset()">Ja, Zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ergebnisse Modal (nicht mehr verwendet) -->
|
||||||
|
<div id="resultsModal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Ergebnisse eintragen</h2>
|
||||||
|
<button class="close-btn" onclick="closeResultsModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<form class="modal-form" onsubmit="saveResults(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="resultsField">Feld:</label>
|
||||||
|
<select id="resultsField" required>
|
||||||
|
<option value="">-- Feld wählen --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="resultsTeam">Team:</label>
|
||||||
|
<input type="text" id="resultsTeam" placeholder="Teamname" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="resultsSets">Sätze gewonnen:</label>
|
||||||
|
<input type="number" id="resultsSets" min="0" placeholder="z.B. 2" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="resultsOpponent">Gegnerischer Team:</label>
|
||||||
|
<input type="text" id="resultsOpponent" placeholder="Name des Gegners">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="cancel-btn" onclick="closeResultsModal()">Abbrechen</button>
|
||||||
|
<button type="submit" class="submit-btn">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="planning.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
799
planning.js
Normal file
799
planning.js
Normal file
@ -0,0 +1,799 @@
|
|||||||
|
const STORAGE_KEY = 'turnierplaner_data';
|
||||||
|
const ROTATION_STATE_KEY = 'turnierplaner_rotation';
|
||||||
|
const SCORES_KEY = 'turnierplaner_scores';
|
||||||
|
let timerInterval = null;
|
||||||
|
let timerSeconds = 0;
|
||||||
|
let isRunning = false;
|
||||||
|
let allTeams = [];
|
||||||
|
let fieldCount = 0; // Anzahl der physischen Felder (jedes Feld hat 2 Feldhälften)
|
||||||
|
let rotationState = {};
|
||||||
|
let matchScores = {}; // { "league:fieldNum": {team1Score, team2Score}, ... }
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadTournamentData();
|
||||||
|
setupTimerInput();
|
||||||
|
loadMatchScores();
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadTournamentData() {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!stored) {
|
||||||
|
console.warn('Keine Turnierdaten gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(stored);
|
||||||
|
allTeams = data;
|
||||||
|
fieldCount = parseInt(data.fieldCount) || 0;
|
||||||
|
|
||||||
|
// Lade oder initialisiere Rotations-State
|
||||||
|
loadRotationState(data);
|
||||||
|
|
||||||
|
// Populate fields mit aktuellem State
|
||||||
|
displayCurrentRound();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadRotationState(data) {
|
||||||
|
const stored = localStorage.getItem(ROTATION_STATE_KEY);
|
||||||
|
|
||||||
|
if (stored) {
|
||||||
|
rotationState = JSON.parse(stored);
|
||||||
|
} else {
|
||||||
|
// Initialisiere mit Startzustand
|
||||||
|
rotationState = {
|
||||||
|
bundesliga: {
|
||||||
|
round: 0,
|
||||||
|
teamOrder: data.bundesliga.map((t, i) => i)
|
||||||
|
},
|
||||||
|
champions: {
|
||||||
|
round: 0,
|
||||||
|
teamOrder: data.champions.map((t, i) => i)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
saveRotationState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRotationState() {
|
||||||
|
localStorage.setItem(ROTATION_STATE_KEY, JSON.stringify(rotationState));
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayCurrentRound() {
|
||||||
|
displayLeagueRound('bundesliga', allTeams.bundesliga);
|
||||||
|
displayLeagueRound('champions', allTeams.champions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayLeagueRound(league, teams) {
|
||||||
|
// Jedes Feld hat 2 Feldhälften, also maximal fieldCount * 2 Teams können gleichzeitig spielen
|
||||||
|
const maxPlayingTeams = fieldCount * 2;
|
||||||
|
const playingTeamsCount = Math.min(teams.length, maxPlayingTeams);
|
||||||
|
const fieldsPerLeague = Math.ceil(playingTeamsCount / 2); // Anzahl Felder für diese Liga
|
||||||
|
const state = rotationState[league];
|
||||||
|
|
||||||
|
// Bestimme Offset für Feldnummern - Champions League beginnt nach Bundesliga
|
||||||
|
let fieldNumberOffset = 0;
|
||||||
|
if (league === 'champions') {
|
||||||
|
const bundesligaPlayingTeams = Math.min(allTeams.bundesliga.length, fieldCount * 2);
|
||||||
|
fieldNumberOffset = Math.ceil(bundesligaPlayingTeams / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiere Felder basierend auf teamOrder
|
||||||
|
const container = document.getElementById(`${league}-fields`);
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < fieldsPerLeague; i++) {
|
||||||
|
// Info-Box für Feldnummer (nur Label)
|
||||||
|
const fieldInfoBox = document.createElement('div');
|
||||||
|
fieldInfoBox.className = 'field-info-box';
|
||||||
|
const fieldNum = fieldNumberOffset + i + 1;
|
||||||
|
fieldInfoBox.innerHTML = `<div class="field-info-number">Feld ${fieldNum}:</div>`;
|
||||||
|
container.appendChild(fieldInfoBox);
|
||||||
|
|
||||||
|
// Team 1 (mittlere Spalte) mit Score-Input
|
||||||
|
const team1Index = state.teamOrder[i * 2];
|
||||||
|
const team1 = teams[team1Index] || {};
|
||||||
|
const fieldCard1 = document.createElement('div');
|
||||||
|
fieldCard1.className = 'field-card';
|
||||||
|
const scoreKey = getScoreKey(league, fieldNum);
|
||||||
|
const existingScore = matchScores[scoreKey] || { team1Score: '', team2Score: '' };
|
||||||
|
fieldCard1.innerHTML = `
|
||||||
|
<div class="field-card-content">
|
||||||
|
<div class="team-name">${team1.name || '-'}</div>
|
||||||
|
<input type="number" class="score-input" value="${existingScore.team1Score}" placeholder="0"
|
||||||
|
onchange="updateMatchScore('${league}', ${fieldNum}, this.value, document.getElementById('score-${league}-${fieldNum}-t2').value)">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
fieldCard1.id = `score-${league}-${fieldNum}-t1-card`;
|
||||||
|
container.appendChild(fieldCard1);
|
||||||
|
|
||||||
|
// Team 2 (rechte Spalte) mit Score-Input
|
||||||
|
const team2Index = state.teamOrder[i * 2 + 1];
|
||||||
|
const team2 = teams[team2Index] || {};
|
||||||
|
const fieldCard2 = document.createElement('div');
|
||||||
|
fieldCard2.className = 'field-card';
|
||||||
|
fieldCard2.innerHTML = `
|
||||||
|
<div class="field-card-content">
|
||||||
|
<input type="number" class="score-input" value="${existingScore.team2Score}" placeholder="0"
|
||||||
|
id="score-${league}-${fieldNum}-t2"
|
||||||
|
onchange="updateMatchScore('${league}', ${fieldNum}, document.getElementById('score-${league}-${fieldNum}-t1-card').querySelector('.score-input').value, this.value)">
|
||||||
|
<div class="team-name">${team2.name || '-'}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.appendChild(fieldCard2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiere wartende Teams
|
||||||
|
updateWaitingTeams(league, teams, fieldsPerLeague, state.teamOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWaitingTeams(league, teams, fieldsPerLeague, teamOrder) {
|
||||||
|
const container = document.getElementById(`${league}-waiting`);
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Anzahl der spielenden Teams = fieldsPerLeague * 2 (2 Teams pro Feld)
|
||||||
|
const playingTeamsCount = fieldsPerLeague * 2;
|
||||||
|
const waitingIndices = teamOrder.slice(playingTeamsCount);
|
||||||
|
|
||||||
|
if (waitingIndices.length === 0) {
|
||||||
|
container.innerHTML = '<div class="no-waiting-teams">Alle Teams spielen</div>';
|
||||||
|
} else {
|
||||||
|
waitingIndices.forEach(teamIndex => {
|
||||||
|
const team = teams[teamIndex];
|
||||||
|
const badge = document.createElement('div');
|
||||||
|
badge.className = 'waiting-team-badge';
|
||||||
|
badge.textContent = team.name;
|
||||||
|
badge.title = `${team.club || ''} - ${team.motto || ''}`;
|
||||||
|
container.appendChild(badge);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateFields(league, teams, fieldCount) {
|
||||||
|
const container = document.getElementById(`${league}-fields`);
|
||||||
|
const totalFields = parseInt(fieldCount);
|
||||||
|
const fieldsPerLeague = Math.ceil(totalFields / 2);
|
||||||
|
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
for (let i = 1; i <= fieldsPerLeague; i++) {
|
||||||
|
const fieldCard = document.createElement('div');
|
||||||
|
fieldCard.className = 'field-card';
|
||||||
|
|
||||||
|
// Zuweisung von Teams zu Feldern (Round-Robin)
|
||||||
|
const teamIndex = (i - 1) % teams.length;
|
||||||
|
const team = teams[teamIndex] || {};
|
||||||
|
|
||||||
|
fieldCard.innerHTML = `
|
||||||
|
<div class="field-number">Feld ${i}</div>
|
||||||
|
<div class="field-team">${team.name || '-'}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
fieldCard.setAttribute('data-field', i);
|
||||||
|
fieldCard.setAttribute('data-league', league);
|
||||||
|
fieldCard.setAttribute('data-team', team.name || '');
|
||||||
|
|
||||||
|
container.appendChild(fieldCard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateWaitingTeams(league, teams, fieldCount) {
|
||||||
|
const container = document.getElementById(`${league}-waiting`);
|
||||||
|
const totalFields = parseInt(fieldCount);
|
||||||
|
const fieldsPerLeague = Math.ceil(totalFields / 2);
|
||||||
|
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Teams die auf Feldern spielen
|
||||||
|
const playingTeamIndices = new Set();
|
||||||
|
for (let i = 0; i < fieldsPerLeague; i++) {
|
||||||
|
playingTeamIndices.add(i % teams.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wartende Teams sind alle Teams, die nicht gerade spielen
|
||||||
|
const waitingTeams = teams.filter((team, index) => !playingTeamIndices.has(index));
|
||||||
|
|
||||||
|
if (waitingTeams.length === 0) {
|
||||||
|
container.innerHTML = '<div class="no-waiting-teams">Alle Teams spielen</div>';
|
||||||
|
} else {
|
||||||
|
waitingTeams.forEach(team => {
|
||||||
|
const badge = document.createElement('div');
|
||||||
|
badge.className = 'waiting-team-badge';
|
||||||
|
badge.textContent = team.name;
|
||||||
|
badge.title = `${team.club || ''} - ${team.motto || ''}`;
|
||||||
|
container.appendChild(badge);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer Functions
|
||||||
|
function setupTimerInput() {
|
||||||
|
const timerInput = document.getElementById('timerInput');
|
||||||
|
timerInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const value = timerInput.value.trim();
|
||||||
|
if (value) {
|
||||||
|
parseAndSetTimer(value);
|
||||||
|
timerInput.value = '';
|
||||||
|
timerInput.blur(); // Remove focus after input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAndSetTimer(timeStr) {
|
||||||
|
let seconds = 0;
|
||||||
|
|
||||||
|
if (timeStr.includes(':')) {
|
||||||
|
// Parse MM:SS format
|
||||||
|
const parts = timeStr.split(':');
|
||||||
|
if (parts.length === 2) {
|
||||||
|
const minutes = parseInt(parts[0]) || 0;
|
||||||
|
seconds = parseInt(parts[1]) || 0;
|
||||||
|
seconds = minutes * 60 + seconds;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Parse as just seconds
|
||||||
|
seconds = parseInt(timeStr) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds > 0) {
|
||||||
|
// Stop current timer if running
|
||||||
|
if (isRunning) {
|
||||||
|
pauseTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
timerSeconds = seconds;
|
||||||
|
updateTimerDisplay();
|
||||||
|
|
||||||
|
// Start timer automatically
|
||||||
|
startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTimer() {
|
||||||
|
if (isRunning) {
|
||||||
|
pauseTimer();
|
||||||
|
} else {
|
||||||
|
startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTimer() {
|
||||||
|
if (isRunning) return;
|
||||||
|
|
||||||
|
isRunning = true;
|
||||||
|
const btn = document.getElementById('timerBtn');
|
||||||
|
btn.textContent = 'Pause';
|
||||||
|
|
||||||
|
timerInterval = setInterval(() => {
|
||||||
|
if (timerSeconds > 0) {
|
||||||
|
timerSeconds--;
|
||||||
|
updateTimerDisplay();
|
||||||
|
} else {
|
||||||
|
pauseTimer();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pauseTimer() {
|
||||||
|
if (!isRunning) return;
|
||||||
|
|
||||||
|
isRunning = false;
|
||||||
|
const btn = document.getElementById('timerBtn');
|
||||||
|
btn.textContent = 'Start';
|
||||||
|
|
||||||
|
if (timerInterval) {
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
timerInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimerDisplay() {
|
||||||
|
const minutes = Math.floor(timerSeconds / 60);
|
||||||
|
const seconds = timerSeconds % 60;
|
||||||
|
|
||||||
|
const display = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||||
|
document.getElementById('timerDisplay').textContent = display;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Functions
|
||||||
|
function openPointsModal() {
|
||||||
|
document.getElementById('pointsModal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePointsModal() {
|
||||||
|
document.getElementById('pointsModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openResultsModal() {
|
||||||
|
document.getElementById('resultsModal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeResultsModal() {
|
||||||
|
document.getElementById('resultsModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePoints(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const field = document.getElementById('pointsField').value;
|
||||||
|
const team = document.getElementById('pointsTeam').value;
|
||||||
|
const points = document.getElementById('pointsValue').value;
|
||||||
|
|
||||||
|
if (!field || !points) {
|
||||||
|
alert('Bitte alle Felder ausfüllen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Punkte speichern (z.B. in localStorage oder an Server)
|
||||||
|
console.log('Punkte gespeichert:', { field, team, points });
|
||||||
|
alert(`✅ Punkte für ${team} gespeichert!`);
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
document.getElementById('pointsField').value = '';
|
||||||
|
document.getElementById('pointsTeam').value = '';
|
||||||
|
document.getElementById('pointsValue').value = '';
|
||||||
|
|
||||||
|
closePointsModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveResults(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const field = document.getElementById('resultsField').value;
|
||||||
|
const team = document.getElementById('resultsTeam').value;
|
||||||
|
const sets = document.getElementById('resultsSets').value;
|
||||||
|
const opponent = document.getElementById('resultsOpponent').value;
|
||||||
|
|
||||||
|
if (!field || !sets) {
|
||||||
|
alert('Bitte alle Pflichtfelder ausfüllen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Ergebnisse speichern (z.B. in localStorage oder an Server)
|
||||||
|
console.log('Ergebnis gespeichert:', { field, team, sets, opponent });
|
||||||
|
alert(`✅ Ergebnis für ${team} gespeichert!`);
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
document.getElementById('resultsField').value = '';
|
||||||
|
document.getElementById('resultsTeam').value = '';
|
||||||
|
document.getElementById('resultsSets').value = '';
|
||||||
|
document.getElementById('resultsOpponent').value = '';
|
||||||
|
|
||||||
|
closeResultsModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
window.addEventListener('click', (event) => {
|
||||||
|
const pointsModal = document.getElementById('pointsModal');
|
||||||
|
const resultsModal = document.getElementById('resultsModal');
|
||||||
|
|
||||||
|
if (event.target === pointsModal) {
|
||||||
|
closePointsModal();
|
||||||
|
}
|
||||||
|
if (event.target === resultsModal) {
|
||||||
|
closeResultsModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
// Stop timer before navigating
|
||||||
|
if (isRunning) {
|
||||||
|
pauseTimer();
|
||||||
|
}
|
||||||
|
window.location.href = 'index.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotation Logic
|
||||||
|
function nextRound() {
|
||||||
|
// Inkrementiere Runden-Nummer
|
||||||
|
rotationState.bundesliga.round++;
|
||||||
|
rotationState.champions.round++;
|
||||||
|
|
||||||
|
// Leere die Scores für die neue Runde
|
||||||
|
matchScores = {};
|
||||||
|
saveMatchScores();
|
||||||
|
|
||||||
|
rotateLeague('bundesliga', allTeams.bundesliga);
|
||||||
|
rotateLeague('champions', allTeams.champions);
|
||||||
|
saveRotationState();
|
||||||
|
displayCurrentRound();
|
||||||
|
console.log('Nächste Runde!', rotationState);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateLeague(league, teams) {
|
||||||
|
// Jedes Feld hat 2 Feldhälften - maximale spielende Teams = fieldCount * 2
|
||||||
|
const maxPlayingTeams = fieldCount * 2;
|
||||||
|
const playingTeamsCount = Math.min(teams.length, maxPlayingTeams);
|
||||||
|
const state = rotationState[league];
|
||||||
|
|
||||||
|
const totalTeams = teams.length;
|
||||||
|
const waitingCount = totalTeams - playingTeamsCount;
|
||||||
|
|
||||||
|
// Fall 1: Keine wartenden Teams (Anzahl Teams = fieldCount * 2)
|
||||||
|
// Team 1 bleibt stehen, alle anderen rotieren um 1 Position weiter
|
||||||
|
if (waitingCount === 0) {
|
||||||
|
// [0, 1, 2, 3, 4, 5] -> [0, 2, 3, 4, 5, 1]
|
||||||
|
// Team an Index 0 bleibt, Team an Index 1 geht ans Ende, Rest rückt auf
|
||||||
|
if (state.teamOrder.length > 1) {
|
||||||
|
const secondTeam = state.teamOrder.splice(1, 1)[0];
|
||||||
|
state.teamOrder.push(secondTeam);
|
||||||
|
}
|
||||||
|
console.log(`${league} Fall 1: Team ${teams[state.teamOrder[0]].name} bleibt stehen, andere rotieren`);
|
||||||
|
}
|
||||||
|
// Fall 2: Genau 1 wartendes Team (Anzahl Teams = fieldCount * 2 + 1)
|
||||||
|
// Alle rotieren um 1 Position, wartendes Team kommt aufs Randfeld
|
||||||
|
else if (waitingCount === 1) {
|
||||||
|
// [0, 1, 2, 3, 4, 5, 6(wartend)] -> [1, 2, 3, 4, 5, 6, 0]
|
||||||
|
// Einfach erstes Team ans Ende verschieben
|
||||||
|
const first = state.teamOrder.shift();
|
||||||
|
state.teamOrder.push(first);
|
||||||
|
console.log(`${league} Fall 2: Team ${teams[first].name} geht warten, Team ${teams[state.teamOrder[playingTeamsCount - 1]].name} kommt ins Spiel`);
|
||||||
|
}
|
||||||
|
// Fall 3: Mehrere wartende Teams (Anzahl Teams > fieldCount * 2 + 1)
|
||||||
|
// Alle wartenden sollen spielen, max 1 Runde Pause
|
||||||
|
else {
|
||||||
|
// [0, 1, 2, 3(spielend), 4, 5, 6, 7(wartend)] -> [4, 5, 6, 7(jetzt spielend), 0, 1, 2, 3(jetzt wartend)]
|
||||||
|
// Wartende Teams kommen nach vorne, spielende Teams gehen warten
|
||||||
|
const playingTeams = state.teamOrder.slice(0, playingTeamsCount);
|
||||||
|
const waitingTeams = state.teamOrder.slice(playingTeamsCount);
|
||||||
|
|
||||||
|
state.teamOrder = [...waitingTeams, ...playingTeams];
|
||||||
|
console.log(`${league} Fall 3: Wartende Teams spielen jetzt, spielende Teams warten`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== SCORE & PUNKTE-SYSTEM =====
|
||||||
|
|
||||||
|
function loadMatchScores() {
|
||||||
|
const stored = localStorage.getItem(SCORES_KEY);
|
||||||
|
if (stored) {
|
||||||
|
matchScores = JSON.parse(stored);
|
||||||
|
} else {
|
||||||
|
matchScores = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveMatchScores() {
|
||||||
|
localStorage.setItem(SCORES_KEY, JSON.stringify(matchScores));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScoreKey(league, fieldNum) {
|
||||||
|
return `${league}:${fieldNum}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMatchScore(league, fieldNum, team1Score, team2Score) {
|
||||||
|
const key = getScoreKey(league, fieldNum);
|
||||||
|
matchScores[key] = {
|
||||||
|
round: rotationState[league].round,
|
||||||
|
team1Score: parseInt(team1Score) || 0,
|
||||||
|
team2Score: parseInt(team2Score) || 0
|
||||||
|
};
|
||||||
|
saveMatchScores();
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatePoints(team1Score, team2Score) {
|
||||||
|
// Berechnet Punkte für Team 1 und Team 2 basierend auf Spielstand
|
||||||
|
const diff = Math.abs(team1Score - team2Score);
|
||||||
|
|
||||||
|
if (team1Score > team2Score) {
|
||||||
|
// Team 1 gewinnt
|
||||||
|
return {
|
||||||
|
team1Points: diff + 2,
|
||||||
|
team2Points: -diff
|
||||||
|
};
|
||||||
|
} else if (team2Score > team1Score) {
|
||||||
|
// Team 2 gewinnt
|
||||||
|
return {
|
||||||
|
team1Points: -diff,
|
||||||
|
team2Points: diff + 2
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Unentschieden
|
||||||
|
return {
|
||||||
|
team1Points: 0,
|
||||||
|
team2Points: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTeamPointsHistory() {
|
||||||
|
// Erstellt eine Punkte-Historie für alle Teams in beiden Ligen
|
||||||
|
const history = {
|
||||||
|
bundesliga: {},
|
||||||
|
champions: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialisiere leere Arrays für jedes Team
|
||||||
|
allTeams.bundesliga.forEach((team, idx) => {
|
||||||
|
history.bundesliga[idx] = [];
|
||||||
|
});
|
||||||
|
allTeams.champions.forEach((team, idx) => {
|
||||||
|
history.champions[idx] = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gehe durch alle gespeicherten Scores
|
||||||
|
Object.keys(matchScores).forEach(key => {
|
||||||
|
const [league, fieldNum] = key.split(':');
|
||||||
|
const score = matchScores[key];
|
||||||
|
const fieldNumInt = parseInt(fieldNum);
|
||||||
|
|
||||||
|
const state = rotationState[league];
|
||||||
|
const teamsInLeague = league === 'bundesliga' ? allTeams.bundesliga : allTeams.champions;
|
||||||
|
|
||||||
|
// Berechne fieldsPerLeague basierend auf fieldCount
|
||||||
|
const maxPlayingTeams = fieldCount * 2;
|
||||||
|
const playingTeamsCount = Math.min(teamsInLeague.length, maxPlayingTeams);
|
||||||
|
const fieldsPerLeague = Math.ceil(playingTeamsCount / 2);
|
||||||
|
|
||||||
|
// Bestimme Offset für Feldnummern
|
||||||
|
const bundesligaPlayingTeams = Math.min(allTeams.bundesliga.length, fieldCount * 2);
|
||||||
|
const bundesligaFields = Math.ceil(bundesligaPlayingTeams / 2);
|
||||||
|
const fieldOffset = league === 'bundesliga' ? 0 : bundesligaFields;
|
||||||
|
const i = fieldNumInt - fieldOffset - 1;
|
||||||
|
|
||||||
|
if (i >= 0 && i < fieldsPerLeague && state && state.teamOrder) {
|
||||||
|
const team1Index = state.teamOrder[i * 2];
|
||||||
|
const team2Index = state.teamOrder[i * 2 + 1];
|
||||||
|
|
||||||
|
if (team1Index !== undefined && team2Index !== undefined) {
|
||||||
|
const points = calculatePoints(score.team1Score, score.team2Score);
|
||||||
|
|
||||||
|
if (!history[league][team1Index][score.round]) {
|
||||||
|
history[league][team1Index][score.round] = 0;
|
||||||
|
}
|
||||||
|
if (!history[league][team2Index][score.round]) {
|
||||||
|
history[league][team2Index][score.round] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
history[league][team1Index][score.round] = points.team1Points;
|
||||||
|
history[league][team2Index][score.round] = points.team2Points;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== MODAL FUNCTIONS =====
|
||||||
|
|
||||||
|
function openPointsModal() {
|
||||||
|
const history = getTeamPointsHistory();
|
||||||
|
const content = document.getElementById('pointsDisplayContent');
|
||||||
|
content.innerHTML = '';
|
||||||
|
|
||||||
|
// Bundesliga
|
||||||
|
if (allTeams.bundesliga.length > 0) {
|
||||||
|
content.appendChild(createLeaguePointsSection('bundesliga', 'Bundesliga', allTeams.bundesliga, history.bundesliga));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Champions League
|
||||||
|
if (allTeams.champions.length > 0) {
|
||||||
|
content.appendChild(createLeaguePointsSection('champions', 'Champions League', allTeams.champions, history.champions));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('pointsModal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLeaguePointsSection(leagueId, leagueName, teams, history) {
|
||||||
|
const section = document.createElement('div');
|
||||||
|
section.className = 'league-points-section';
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'league-points-header';
|
||||||
|
header.innerHTML = leagueName;
|
||||||
|
section.appendChild(header);
|
||||||
|
|
||||||
|
// Table
|
||||||
|
const table = document.createElement('table');
|
||||||
|
table.className = 'points-table';
|
||||||
|
|
||||||
|
// Berechne maximale Runde
|
||||||
|
let maxRound = 0;
|
||||||
|
Object.keys(history).forEach(teamId => {
|
||||||
|
if (history[teamId] && history[teamId].length) {
|
||||||
|
maxRound = Math.max(maxRound, history[teamId].length - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Header Row
|
||||||
|
const headerRow = document.createElement('tr');
|
||||||
|
headerRow.innerHTML = '<th>Team</th>';
|
||||||
|
for (let r = 0; r <= maxRound; r++) {
|
||||||
|
const th = document.createElement('th');
|
||||||
|
th.innerHTML = `Runde ${r + 1}`;
|
||||||
|
headerRow.appendChild(th);
|
||||||
|
}
|
||||||
|
const totalTh = document.createElement('th');
|
||||||
|
totalTh.innerHTML = 'Gesamt';
|
||||||
|
headerRow.appendChild(totalTh);
|
||||||
|
table.appendChild(headerRow);
|
||||||
|
|
||||||
|
// Team Rows
|
||||||
|
teams.forEach((team, teamIdx) => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
// Team Name
|
||||||
|
const nameCell = document.createElement('td');
|
||||||
|
nameCell.style.textAlign = 'left';
|
||||||
|
nameCell.style.fontWeight = '500';
|
||||||
|
nameCell.innerHTML = team.name;
|
||||||
|
row.appendChild(nameCell);
|
||||||
|
|
||||||
|
// Points per round
|
||||||
|
let totalPoints = 0;
|
||||||
|
const teamHistory = history[teamIdx] || [];
|
||||||
|
|
||||||
|
for (let r = 0; r <= maxRound; r++) {
|
||||||
|
const cell = document.createElement('td');
|
||||||
|
const points = teamHistory[r] !== undefined ? teamHistory[r] : '-';
|
||||||
|
|
||||||
|
if (points !== '-') {
|
||||||
|
totalPoints += points;
|
||||||
|
const pointsDiv = document.createElement('div');
|
||||||
|
pointsDiv.className = 'points-value';
|
||||||
|
|
||||||
|
if (points > 0) {
|
||||||
|
pointsDiv.classList.add('positive');
|
||||||
|
pointsDiv.innerHTML = `+${points}`;
|
||||||
|
} else if (points < 0) {
|
||||||
|
pointsDiv.classList.add('negative');
|
||||||
|
pointsDiv.innerHTML = `${points}`;
|
||||||
|
} else {
|
||||||
|
pointsDiv.classList.add('neutral');
|
||||||
|
pointsDiv.innerHTML = '0';
|
||||||
|
}
|
||||||
|
cell.appendChild(pointsDiv);
|
||||||
|
} else {
|
||||||
|
cell.innerHTML = '-';
|
||||||
|
}
|
||||||
|
row.appendChild(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total Points
|
||||||
|
const totalCell = document.createElement('td');
|
||||||
|
const totalDiv = document.createElement('div');
|
||||||
|
totalDiv.className = 'points-value points-total';
|
||||||
|
totalDiv.innerHTML = totalPoints > 0 ? `+${totalPoints}` : `${totalPoints}`;
|
||||||
|
totalCell.appendChild(totalDiv);
|
||||||
|
row.appendChild(totalCell);
|
||||||
|
|
||||||
|
table.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
section.appendChild(table);
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePointsModal() {
|
||||||
|
document.getElementById('pointsModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openScoreboard() {
|
||||||
|
const history = getTeamPointsHistory();
|
||||||
|
const content = document.getElementById('scoreboardContent');
|
||||||
|
content.innerHTML = '';
|
||||||
|
|
||||||
|
// Erstelle Daten für beide Ligen mit Gesamtpunkten
|
||||||
|
const leaguesData = [
|
||||||
|
{ id: 'bundesliga', name: 'Bundesliga', teams: allTeams.bundesliga, history: history.bundesliga },
|
||||||
|
{ id: 'champions', name: 'Champions League', teams: allTeams.champions, history: history.champions }
|
||||||
|
];
|
||||||
|
|
||||||
|
leaguesData.forEach(league => {
|
||||||
|
if (league.teams.length > 0) {
|
||||||
|
// Berechne Gesamtpunkte für jedes Team
|
||||||
|
const teamScores = league.teams.map((team, idx) => {
|
||||||
|
const teamHistory = league.history[idx] || [];
|
||||||
|
const totalPoints = teamHistory.reduce((sum, pts) => sum + (pts || 0), 0);
|
||||||
|
return { idx, name: team.name, points: totalPoints };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sortiere absteigend nach Punkten
|
||||||
|
teamScores.sort((a, b) => b.points - a.points);
|
||||||
|
|
||||||
|
// Erstelle die Liga-Section
|
||||||
|
const section = document.createElement('div');
|
||||||
|
section.className = 'scoreboard-league';
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'scoreboard-league-header';
|
||||||
|
header.innerHTML = league.name;
|
||||||
|
section.appendChild(header);
|
||||||
|
|
||||||
|
const list = document.createElement('ul');
|
||||||
|
list.className = 'scoreboard-list';
|
||||||
|
|
||||||
|
teamScores.forEach((score, rank) => {
|
||||||
|
const item = document.createElement('li');
|
||||||
|
item.className = 'scoreboard-item';
|
||||||
|
|
||||||
|
const rankDiv = document.createElement('div');
|
||||||
|
rankDiv.className = 'scoreboard-rank';
|
||||||
|
rankDiv.innerHTML = `${rank + 1}.`;
|
||||||
|
item.appendChild(rankDiv);
|
||||||
|
|
||||||
|
const nameDiv = document.createElement('div');
|
||||||
|
nameDiv.className = 'scoreboard-team-name';
|
||||||
|
nameDiv.innerHTML = score.name;
|
||||||
|
item.appendChild(nameDiv);
|
||||||
|
|
||||||
|
const pointsDiv = document.createElement('div');
|
||||||
|
pointsDiv.className = 'scoreboard-points';
|
||||||
|
if (score.points > 0) {
|
||||||
|
pointsDiv.classList.add('positive');
|
||||||
|
pointsDiv.innerHTML = `+${score.points}`;
|
||||||
|
} else if (score.points < 0) {
|
||||||
|
pointsDiv.classList.add('negative');
|
||||||
|
pointsDiv.innerHTML = `${score.points}`;
|
||||||
|
} else {
|
||||||
|
pointsDiv.innerHTML = '0';
|
||||||
|
}
|
||||||
|
item.appendChild(pointsDiv);
|
||||||
|
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
section.appendChild(list);
|
||||||
|
content.appendChild(section);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('scoreboardModal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeScoreboard() {
|
||||||
|
document.getElementById('scoreboardModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmReset() {
|
||||||
|
document.getElementById('resetConfirmModal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeResetConfirm() {
|
||||||
|
document.getElementById('resetConfirmModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function performReset() {
|
||||||
|
// Lade die Rohdaten (Teams, Feldanzahl)
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!stored) return;
|
||||||
|
|
||||||
|
const data = JSON.parse(stored);
|
||||||
|
|
||||||
|
// Setze Rotationsstates zurück auf Anfangszustand
|
||||||
|
rotationState = {
|
||||||
|
bundesliga: {
|
||||||
|
round: 0,
|
||||||
|
teamOrder: data.bundesliga.map((t, i) => i)
|
||||||
|
},
|
||||||
|
champions: {
|
||||||
|
round: 0,
|
||||||
|
teamOrder: data.champions.map((t, i) => i)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Leere alle Scores
|
||||||
|
matchScores = {};
|
||||||
|
|
||||||
|
// Speichere zurückgesetzte States
|
||||||
|
saveRotationState();
|
||||||
|
saveMatchScores();
|
||||||
|
|
||||||
|
// Aktualisiere die UI
|
||||||
|
displayCurrentRound();
|
||||||
|
|
||||||
|
// Schließe das Modal
|
||||||
|
closeResetConfirm();
|
||||||
|
|
||||||
|
alert('✅ Turnier wurde zurückgesetzt! Alle Runden und Scores wurden gelöscht.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openResultsModal() {
|
||||||
|
document.getElementById('resultsModal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeResultsModal() {
|
||||||
|
document.getElementById('resultsModal').style.display = 'none';
|
||||||
|
}
|
||||||
227
script.js
Normal file
227
script.js
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
const STORAGE_KEY = 'turnierplaner_data';
|
||||||
|
|
||||||
|
// Daten laden beim Start
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
function addTeam(league) {
|
||||||
|
const list = document.getElementById(`${league}-list`);
|
||||||
|
|
||||||
|
// Leere-Nachricht entfernen wenn vorhanden
|
||||||
|
const emptyMsg = list.querySelector('.empty-message');
|
||||||
|
if (emptyMsg) {
|
||||||
|
emptyMsg.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamEntry = document.createElement('div');
|
||||||
|
teamEntry.className = 'team-entry';
|
||||||
|
teamEntry.innerHTML = `
|
||||||
|
<div class="team-info">
|
||||||
|
<input type="text" placeholder="Teamname" class="team-name">
|
||||||
|
<input type="text" placeholder="Vereinsname" class="team-club">
|
||||||
|
<input type="text" placeholder="Schlachtruf" class="team-motto">
|
||||||
|
</div>
|
||||||
|
<button class="delete-btn" onclick="deleteTeam(this)">Löschen</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
list.appendChild(teamEntry);
|
||||||
|
updateTeamCount(league);
|
||||||
|
saveData();
|
||||||
|
|
||||||
|
// Focus auf das erste Input-Feld
|
||||||
|
teamEntry.querySelector('.team-name').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTeam(button) {
|
||||||
|
const teamEntry = button.parentElement;
|
||||||
|
const list = teamEntry.parentElement;
|
||||||
|
|
||||||
|
teamEntry.remove();
|
||||||
|
|
||||||
|
// Bestimme welche Liga
|
||||||
|
const league = list.id.replace('-list', '');
|
||||||
|
updateTeamCount(league);
|
||||||
|
saveData();
|
||||||
|
|
||||||
|
// Zeige Leere-Nachricht wenn keine Teams mehr
|
||||||
|
if (list.children.length === 0) {
|
||||||
|
const emptyMsg = document.createElement('div');
|
||||||
|
emptyMsg.className = 'empty-message';
|
||||||
|
emptyMsg.textContent = 'Keine Teams hinzugefügt. Klicke auf + um zu beginnen!';
|
||||||
|
list.appendChild(emptyMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveData() {
|
||||||
|
const data = {
|
||||||
|
fieldCount: document.getElementById('fieldCount').value,
|
||||||
|
bundesliga: getTeamsFromList('bundesliga'),
|
||||||
|
champions: getTeamsFromList('champions')
|
||||||
|
};
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTeamsFromList(league) {
|
||||||
|
const list = document.getElementById(`${league}-list`);
|
||||||
|
const teams = [];
|
||||||
|
list.querySelectorAll('.team-entry').forEach(entry => {
|
||||||
|
teams.push({
|
||||||
|
name: entry.querySelector('.team-name').value,
|
||||||
|
club: entry.querySelector('.team-club').value,
|
||||||
|
motto: entry.querySelector('.team-motto').value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return teams;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadData() {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!stored) return;
|
||||||
|
|
||||||
|
const data = JSON.parse(stored);
|
||||||
|
|
||||||
|
// Lade Feldanzahl
|
||||||
|
document.getElementById('fieldCount').value = data.fieldCount || 1;
|
||||||
|
|
||||||
|
// Lade Teams
|
||||||
|
loadTeamsToList('bundesliga', data.bundesliga || []);
|
||||||
|
loadTeamsToList('champions', data.champions || []);
|
||||||
|
|
||||||
|
// Event listener für Auto-Save
|
||||||
|
document.getElementById('fieldCount').addEventListener('change', saveData);
|
||||||
|
document.addEventListener('input', (e) => {
|
||||||
|
if (e.target.classList.contains('team-name') ||
|
||||||
|
e.target.classList.contains('team-club') ||
|
||||||
|
e.target.classList.contains('team-motto')) {
|
||||||
|
saveData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update Team-Anzahl
|
||||||
|
updateTeamCount('bundesliga');
|
||||||
|
updateTeamCount('champions');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTeamsToList(league, teams) {
|
||||||
|
const list = document.getElementById(`${league}-list`);
|
||||||
|
list.innerHTML = '';
|
||||||
|
|
||||||
|
if (teams.length === 0) {
|
||||||
|
const emptyMsg = document.createElement('div');
|
||||||
|
emptyMsg.className = 'empty-message';
|
||||||
|
emptyMsg.textContent = 'Keine Teams hinzugefügt. Klicke auf + um zu beginnen!';
|
||||||
|
list.appendChild(emptyMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
teams.forEach(team => {
|
||||||
|
const teamEntry = document.createElement('div');
|
||||||
|
teamEntry.className = 'team-entry';
|
||||||
|
teamEntry.innerHTML = `
|
||||||
|
<div class="team-info">
|
||||||
|
<input type="text" placeholder="Teamname" class="team-name" value="${escapeHtml(team.name)}">
|
||||||
|
<input type="text" placeholder="Vereinsname" class="team-club" value="${escapeHtml(team.club)}">
|
||||||
|
<input type="text" placeholder="Schlachtruf" class="team-motto" value="${escapeHtml(team.motto)}">
|
||||||
|
</div>
|
||||||
|
<button class="delete-btn" onclick="deleteTeam(this)">Löschen</button>
|
||||||
|
`;
|
||||||
|
list.appendChild(teamEntry);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listener für Auto-Save
|
||||||
|
document.addEventListener('input', (e) => {
|
||||||
|
if (e.target.classList.contains('team-name') ||
|
||||||
|
e.target.classList.contains('team-club') ||
|
||||||
|
e.target.classList.contains('team-motto')) {
|
||||||
|
saveData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update Team-Anzahl
|
||||||
|
updateTeamCount(league);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTeamCount(league) {
|
||||||
|
const list = document.getElementById(`${league}-list`);
|
||||||
|
const countElement = document.getElementById(`${league}-count`);
|
||||||
|
const teamCount = list.querySelectorAll('.team-entry').length;
|
||||||
|
const teamText = teamCount === 1 ? 'Team' : 'Teams';
|
||||||
|
countElement.textContent = `${teamCount} ${teamText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportData() {
|
||||||
|
const data = {
|
||||||
|
fieldCount: document.getElementById('fieldCount').value,
|
||||||
|
bundesliga: getTeamsFromList('bundesliga'),
|
||||||
|
champions: getTeamsFromList('champions')
|
||||||
|
};
|
||||||
|
|
||||||
|
const outputDiv = document.getElementById('export-output');
|
||||||
|
outputDiv.textContent = JSON.stringify(data, null, 2);
|
||||||
|
outputDiv.style.display = 'block';
|
||||||
|
|
||||||
|
// Download als JSON-Datei
|
||||||
|
const jsonString = JSON.stringify(data, null, 2);
|
||||||
|
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `turnierplaner_${new Date().toISOString().split('T')[0]}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importData() {
|
||||||
|
document.getElementById('import-file').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileImport(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target.result);
|
||||||
|
|
||||||
|
document.getElementById('fieldCount').value = data.fieldCount || 1;
|
||||||
|
loadTeamsToList('bundesliga', data.bundesliga || []);
|
||||||
|
loadTeamsToList('champions', data.champions || []);
|
||||||
|
|
||||||
|
saveData();
|
||||||
|
alert('✅ Daten erfolgreich importiert!');
|
||||||
|
} catch (error) {
|
||||||
|
alert('❌ Fehler beim Importieren der Datei: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
|
||||||
|
// Reset file input
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const map = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return text.replace(/[&<>"']/g, m => map[m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToPlanning() {
|
||||||
|
const data = {
|
||||||
|
fieldCount: document.getElementById('fieldCount').value,
|
||||||
|
bundesliga: getTeamsFromList('bundesliga'),
|
||||||
|
champions: getTeamsFromList('champions')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Speichere in localStorage falls noch nicht gespeichert
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||||
|
|
||||||
|
// Navigiere zur Planungsseite
|
||||||
|
window.location.href = 'planning.html';
|
||||||
|
}
|
||||||
300
styles.css
Normal file
300
styles.css
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
|
||||||
|
height: 100vh;
|
||||||
|
padding: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 2em;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
background: white;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-group input {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #2ecc71;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
width: 200px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #27ae60;
|
||||||
|
box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leagues-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.leagues-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 600px) {
|
||||||
|
.leagues-container {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.league {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.league-header {
|
||||||
|
padding: 15px;
|
||||||
|
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.league-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.league-title h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-count {
|
||||||
|
font-size: 0.85em;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-team-btn {
|
||||||
|
background: white;
|
||||||
|
color: #27ae60;
|
||||||
|
border: none;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 1.8em;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-team-btn:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-list {
|
||||||
|
padding: 15px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-entry {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #2ecc71;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-entry:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-entry input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1em;
|
||||||
|
font-family: inherit;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-entry input:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-entry input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2ecc71;
|
||||||
|
box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-entry input::placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95em;
|
||||||
|
transition: background 0.3s;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-height: 44px;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: #ff5252;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-message {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
padding: 40px 20px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-section {
|
||||||
|
background: white;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-btn {
|
||||||
|
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
min-height: 44px;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn {
|
||||||
|
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
min-height: 44px;
|
||||||
|
touch-action: manipulation;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-output {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user