Vielleicht kann sowas einer gebrauchen oder auch gerne erweitern.
(Beta, Fehler möglich)
Code: Alles auswählen
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pro Billiard Scoreboard</title>
<style>
:root {
--primary-blue: #50a1e4;
--danger: #e74c3c;
--bg-gray: #f4f7f9;
}
body { font-family: 'Segoe UI', sans-serif; background-color: #e9ecef; margin: 0; padding: 20px; display: flex; justify-content: center; }
#app { width: 100%; max-width: 900px; background: white; border-radius: 25px; overflow: hidden; box-shadow: 0 15px 40px rgba(0,0,0,0.1); min-height: 800px; position: relative; }
/* NAVBAR */
.nav-tabs { display: flex; background: #fff; border-bottom: 2px solid #dce0e5; }
.tab-link { flex: 1; padding: 20px; text-align: center; cursor: pointer; font-weight: 600; color: #aaa; transition: 0.3s; border-bottom: 4px solid transparent; }
.tab-link.active { color: var(--primary-blue); border-bottom: 4px solid var(--primary-blue); background-color: #f8fbff; }
.content-section { display: none; padding: 30px; }
.content-section.active { display: block; }
/* TAB 1: SPIELER LISTE */
.player-db-entry { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; background: var(--bg-gray); border-radius: 12px; margin-bottom: 8px; }
.del-btn { background: #fff; border: 1px solid #ddd; border-radius: 6px; padding: 5px 10px; cursor: pointer; color: #666; }
/* SETUP & INPUTS */
.setup-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; font-weight: bold; color: #555; }
input, select { width: 100%; padding: 14px; border-radius: 12px; border: 1px solid #ddd; font-size: 16px; box-sizing: border-box; background: #fff; }
/* SCOREBOARD */
.sb-layout { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 20px; }
.sb-grid-free { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }
.player-card { background: var(--primary-blue); color: #fff; padding: 25px; border-radius: 25px; text-align: center; border: 6px solid transparent; transition: 0.3s; flex: 1; box-shadow: 0 8px 20px rgba(0,0,0,0.1); }
.player-card.active-turn { border-color: #444; transform: scale(1.02); }
.score-num { font-size: 110px; font-weight: 800; margin: 10px 0; line-height: 0.9; }
.stats-row { font-size: 14px; opacity: 0.9; margin-top: 10px; }
/* BUTTONS */
.controls-row { display: flex; justify-content: center; align-items: center; gap: 15px; margin-top: 40px; }
.btn-icon { width: 75px; height: 75px; border-radius: 50%; border: 3px solid var(--primary-blue); background: white; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
.btn-icon svg { width: 35px; height: 35px; fill: var(--primary-blue); }
.btn-icon.balls-display { background: var(--primary-blue); color: white; border: none; font-size: 32px; font-weight: bold; }
.btn-swap { background: white; border: 1px solid #ddd; border-radius: 10px; width: 60px; height: 60px; cursor: pointer; font-size: 28px; display: flex; align-items: center; justify-content: center; }
.primary-btn { width: 100%; padding: 18px; background: var(--primary-blue); color: white; border: none; border-radius: 15px; font-size: 20px; font-weight: bold; cursor: pointer; margin-top: 20px; }
/* POPUP */
.modal-overlay { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center; border-radius: 25px; }
.modal-box { background: white; padding: 25px; border-radius: 25px; width: 340px; text-align: center; }
.foul-toggle { width: 100%; padding: 15px; background: var(--primary-blue); color: white; border: 3px solid transparent; border-radius: 12px; font-weight: bold; margin-bottom: 15px; cursor: pointer; font-size: 18px; text-transform: uppercase; }
.foul-toggle.active { border-color: #000; box-shadow: 0 0 0 2px #000; }
.kugel-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.k-btn { padding: 12px; background: var(--primary-blue); border: none; color: white; border-radius: 10px; font-weight: bold; cursor: pointer; font-size: 16px; }
.k-btn:disabled { background: #e0e0e0; color: #aaa; cursor: not-allowed; }
</style>
</head>
<body>
<div id="app">
<div class="nav-tabs">
<div class="tab-link active" id="t-settings" onclick="app.nav('settings')">1. Spieler</div>
<div class="tab-link" id="t-setup" onclick="app.nav('setup')">2. Setup</div>
<div class="tab-link" id="t-game" onclick="app.nav('game')">3. Scoreboard</div>
</div>
<div id="sec-settings" class="content-section active">
<h2>Spieler-Datenbank</h2>
<div style="display: flex; gap: 10px; margin-bottom: 25px;">
<input type="text" id="in-name" placeholder="Name eingeben...">
<button onclick="app.addPlayer()" style="background:var(--primary-blue); color:white; border:none; padding:0 30px; border-radius:12px; cursor:pointer; font-weight:bold;">Hinzufügen</button>
</div>
<div id="player-list-ui"></div>
</div>
<div id="sec-setup" class="content-section">
<h2>Partie-Einstellungen</h2>
<div class="setup-group">
<label>Disziplin:</label>
<select id="set-disc" onchange="app.onDiscChange()">
<option value="141">14/1 Endlos</option>
<option value="8ball">8-Ball</option>
<option value="9ball">9-Ball</option>
<option value="10ball">10-Ball</option>
<option value="scoreboard">Freies Scoreboard</option>
</select>
</div>
<div class="setup-group" id="race-container">
<label>Race To:</label>
<input type="number" id="set-race" value="100">
</div>
<div class="setup-group">
<label>Anzahl Spieler:</label>
<select id="set-pcount" onchange="app.renderPlayerSelects()"></select>
</div>
<div id="player-select-area"></div>
<button onclick="app.startGame()" class="primary-btn">SPIEL STARTEN</button>
</div>
<div id="sec-game" class="content-section">
<div id="game-title" style="text-align:center; font-weight:bold; color:var(--primary-blue); margin-bottom:10px; font-size:18px;"></div>
<div id="sb-container"></div>
<div id="main-controls" class="controls-row">
<button class="btn-icon" onclick="app.changeScore(-1)"><span style="font-size:40px; color:var(--primary-blue);">-</span></button>
<button class="btn-icon" onclick="app.changeScore(1)"><span style="font-size:40px; color:var(--primary-blue);">+</span></button>
<button class="btn-icon" id="btn-rack" onclick="app.rackFinish()">
<svg viewBox="0 0 24 24"><path d="M12,2L2,22H22L12,2M12,6L19.53,20H4.47L12,6Z"/></svg>
</button>
<button class="btn-icon balls-display" id="balls-display" onclick="app.openKugelPopup()">15</button>
<button class="btn-icon" id="btn-foul" onclick="app.handleBlitzFoul()">
<svg viewBox="0 0 24 24"><path d="M7,2V13H10V22L17,10H13L17,2H7Z"/></svg>
</button>
</div>
<div style="text-align:center; margin-top:40px;">
<button onclick="app.undo()" style="padding:12px 25px; border-radius:12px; border:1px solid #ccc; background:white; cursor:pointer;">Schritt Rückgängig</button>
</div>
</div>
<div id="kugel-popup" class="modal-overlay">
<div class="modal-box">
<button id="foul-toggle-btn" class="foul-toggle" onclick="app.toggleFoulFrame()">FOUL</button>
<div class="kugel-grid" id="kugel-grid"></div>
<button onclick="app.closeKugelPopup()" style="width:100%; margin-top:20px; border:none; background:none; color:#999; cursor:pointer;">Abbrechen</button>
</div>
</div>
</div>
<script>
const app = {
players: JSON.parse(localStorage.getItem('billiardPlayers')) || ["Bert", "Grobi", "Ernie", "Graf Zahl"],
game: null, history: [], foulActive: false,
init() { this.renderPlayerList(); this.onDiscChange(); },
nav(id) {
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.tab-link').forEach(t => t.classList.remove('active'));
document.getElementById('sec-' + id).classList.add('active');
document.getElementById('t-' + id).classList.add('active');
},
renderPlayerList() {
const ui = document.getElementById('player-list-ui');
ui.innerHTML = this.players.map((p, i) => `
<div class="player-db-entry">
<span>${p}</span>
<button class="del-btn" onclick="app.delPlayer(${i})">x</button>
</div>
`).join('');
},
addPlayer() {
const n = document.getElementById('in-name').value.trim();
if(n) { this.players.push(n); this.saveDB(); this.renderPlayerList(); this.renderPlayerSelects(); document.getElementById('in-name').value=""; }
},
delPlayer(i) { this.players.splice(i,1); this.saveDB(); this.renderPlayerList(); this.renderPlayerSelects(); },
saveDB() { localStorage.setItem('billiardPlayers', JSON.stringify(this.players)); },
onDiscChange() {
const d = document.getElementById('set-disc').value;
const races = { "141": 100, "8ball": 5, "9ball": 6, "10ball": 7, "scoreboard": 0 };
document.getElementById('set-race').value = races[d];
document.getElementById('race-container').style.display = d === 'scoreboard' ? 'none' : 'block';
const pSelect = document.getElementById('set-pcount');
pSelect.innerHTML = "";
const max = d === 'scoreboard' ? 6 : 2;
for(let i=1; i<=max; i++) pSelect.innerHTML += `<option value="${i}" ${i===2?'selected':''}>${i} Spieler</option>`;
this.renderPlayerSelects();
},
renderPlayerSelects() {
const count = document.getElementById('set-pcount').value;
const area = document.getElementById('player-select-area');
area.innerHTML = "";
for(let i=0; i<count; i++) {
// Logik für unterschiedliche Vorbelegung
let defaultValue = "";
if(this.players.length > i) {
defaultValue = this.players[i];
} else if (this.players.length > 0) {
defaultValue = this.players[0];
}
area.innerHTML += `
<div class="setup-group">
<label>Spieler ${i+1}:</label>
<select class="sel-player" onchange="app.validateUniquePlayers(this)">
${this.players.map(p => `<option value="${p}" ${p === defaultValue ? 'selected' : ''}>${p}</option>`).join('')}
</select>
</div>`;
}
this.validateUniquePlayers();
},
validateUniquePlayers() {
const selects = document.querySelectorAll('.sel-player');
const picked = Array.from(selects).map(s => s.value);
selects.forEach(s => {
const currentVal = s.value;
Array.from(s.options).forEach(opt => {
opt.disabled = picked.includes(opt.value) && opt.value !== currentVal;
});
});
},
startGame() {
const disc = document.getElementById('set-disc').value;
const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
this.game = {
disc, names, scores: Array(names.length).fill(0),
activeIdx: 0, balls: 15, race: document.getElementById('set-race').value,
stats: names.map(() => ({ innings: 1, series: 0, max: 0 }))
};
this.renderGame();
this.nav('game');
},
renderGame() {
const g = this.game;
document.getElementById('game-title').innerText = `${g.disc.toUpperCase()} (Race to ${g.race})`;
const container = document.getElementById('sb-container');
container.innerHTML = "";
if(g.disc === 'scoreboard') {
container.className = "sb-grid-free";
g.names.forEach((name, i) => {
container.innerHTML += `
<div class="player-card">
<div style="font-size:18px;">${name}</div>
<div class="score-num" style="font-size:70px;">${g.scores[i]}</div>
<div style="display:flex; justify-content:center; gap:10px;">
<button class="btn-swap" style="width:40px; height:40px; font-size:20px;" onclick="app.changeFreeScore(${i},-1)">-</button>
<button class="btn-swap" style="width:40px; height:40px; font-size:20px;" onclick="app.changeFreeScore(${i},1)">+</button>
</div>
</div>`;
});
} else {
container.className = "sb-layout";
g.names.forEach((name, i) => {
const gd = g.stats[i].innings > 0 ? (g.scores[i] / g.stats[i].innings).toFixed(2) : "0.00";
const cardHtml = `
<div class="player-card ${g.activeIdx === i ? 'active-turn' : ''}">
<div style="font-size:24px; font-weight:bold;">${name}</div>
<div class="score-num">${g.scores[i]}</div>
${g.disc === '141' ? `
<div class="stats-row">Aufnahme: ${g.stats[i].innings} | GD: ${gd}</div>
<div class="stats-row">Serie: ${g.stats[i].series} | Max: ${g.stats[i].max}</div>
` : ''}
</div>`;
if(i === 1) container.innerHTML += `<button class="btn-swap" onclick="app.switchPlayer()">⇄</button>`;
container.innerHTML += cardHtml;
});
}
const is141 = g.disc === '141';
document.getElementById('main-controls').style.display = g.disc === 'scoreboard' ? 'none' : 'flex';
document.getElementById('btn-rack').style.display = is141 ? 'flex' : 'none';
document.getElementById('btn-foul').style.display = is141 ? 'flex' : 'none';
document.getElementById('balls-display').style.display = is141 ? 'flex' : 'none';
document.getElementById('balls-display').innerText = g.balls;
},
switchPlayer() {
this.saveStep();
const p = this.game.activeIdx;
this.game.stats[p].series = 0;
this.game.activeIdx = (p + 1) % 2;
this.game.stats[this.game.activeIdx].innings++;
this.renderGame();
},
changeScore(v) {
this.saveStep();
const p = this.game.activeIdx;
this.game.scores[p] += v;
if(this.game.disc === '141' && v > 0) {
this.game.stats[p].series += v;
if(this.game.stats[p].series > this.game.stats[p].max) this.game.stats[p].max = this.game.stats[p].series;
this.game.balls = this.game.balls > 1 ? this.game.balls - 1 : 15;
}
this.renderGame();
},
changeFreeScore(i, v) { this.saveStep(); this.game.scores[i] += v; this.renderGame(); },
rackFinish() { this.saveStep(); this.game.scores[this.game.activeIdx] += (this.game.balls - 1); this.game.balls = 15; this.renderGame(); },
handleBlitzFoul() { this.saveStep(); this.game.scores[this.game.activeIdx] -= 1; this.switchPlayer(); },
openKugelPopup() {
this.foulActive = false;
const foulBtn = document.getElementById('foul-toggle-btn');
foulBtn.classList.remove('active');
const grid = document.getElementById('kugel-grid');
grid.innerHTML = "";
for(let i=0; i<=15; i++) {
const b = document.createElement('button');
b.className = 'k-btn'; b.innerText = i;
if(i > this.game.balls) b.disabled = true;
b.onclick = () => {
this.saveStep();
const diff = this.game.balls - i;
const points = diff - (this.foulActive ? 1 : 0);
this.game.scores[this.game.activeIdx] += points;
if(points > 0 && this.game.disc === '141') {
this.game.stats[this.game.activeIdx].series += points;
if(this.game.stats[this.game.activeIdx].series > this.game.stats[this.game.activeIdx].max)
this.game.stats[this.game.activeIdx].max = this.game.stats[this.game.activeIdx].series;
}
this.game.balls = i <= 1 ? 15 : i;
this.closeKugelPopup(); this.switchPlayer();
};
grid.appendChild(b);
}
document.getElementById('kugel-popup').style.display = 'flex';
},
toggleFoulFrame() { this.foulActive = !this.foulActive; document.getElementById('foul-toggle-btn').classList.toggle('active', this.foulActive); },
closeKugelPopup() { document.getElementById('kugel-popup').style.display = 'none'; },
saveStep() { this.history.push(JSON.stringify(this.game)); },
undo() { if(this.history.length) { this.game = JSON.parse(this.history.pop()); this.renderGame(); } }
};
app.init();
</script>
</body>
</html>

