initial commit

This commit is contained in:
Nicola
2026-06-24 13:00:51 +02:00
commit 2d4c3864ef
11 changed files with 2555 additions and 0 deletions
+539
View File
@@ -0,0 +1,539 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>🎾 Tennis Roots Controller</title>
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
background: #0f0f1a;
color: #e0e0e0;
padding: 24px;
min-height: 100vh;
}
h1 {
font-size: 28px;
font-weight: 700;
color: #facc15;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 12px;
}
h1 small {
font-size: 14px;
font-weight: 400;
color: rgba(255,255,255,0.4);
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
max-width: 1100px;
}
.card {
background: linear-gradient(160deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
padding: 18px 20px;
}
.card.full { grid-column: 1 / -1; }
.card h2 {
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: rgba(255,255,255,0.4);
margin-bottom: 14px;
}
.field {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.field label {
min-width: 80px;
font-size: 14px;
font-weight: 500;
color: rgba(255,255,255,0.6);
}
.field input,
.field select {
flex: 1;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 6px;
padding: 8px 12px;
font-size: 15px;
color: #fff;
transition: border-color 0.2s;
}
.field input:focus,
.field select:focus {
outline: none;
border-color: #facc15;
}
.field input::placeholder { color: rgba(255,255,255,0.25); }
.field input.short { flex: 0 0 70px; text-align: center; }
.field .flag-select {
flex: 0 0 70px;
text-align: center;
font-size: 20px;
}
.btn-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 6px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn:hover { filter: brightness(1.15); transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn-primary { background: #3b82f6; color: #fff; }
.btn-success { background: #22c55e; color: #fff; }
.btn-warning { background: #facc15; color: #000; }
.btn-danger { background: #ef4444; color: #fff; }
.btn-ghost { background: rgba(255,255,255,0.08); color: #e0e0e0; }
.btn-sm { padding: 5px 10px; font-size: 12px; }
.score-display {
display: flex;
justify-content: center;
align-items: center;
gap: 24px;
padding: 12px 0;
font-size: 32px;
font-weight: 700;
}
.score-display .vs {
font-size: 18px;
font-weight: 400;
color: rgba(255,255,255,0.25);
}
.score-display .p-score {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.score-display .p-name {
font-size: 16px;
font-weight: 600;
color: rgba(255,255,255,0.7);
}
.score-display .p-points {
font-size: 36px;
font-weight: 800;
color: #facc15;
}
.score-display .p-points.adv { color: #4ade80; }
.score-display .playing-now {
font-size: 12px;
color: #facc15;
letter-spacing: 1px;
}
.sets-display {
display: flex;
justify-content: center;
gap: 32px;
font-size: 15px;
color: rgba(255,255,255,0.5);
margin-bottom: 8px;
}
.sets-display span {
display: flex;
align-items: center;
gap: 12px;
}
.sets-display .set-score {
font-weight: 700;
font-size: 18px;
color: #fff;
}
.sets-display .set-label {
font-size: 12px;
text-transform: uppercase;
color: rgba(255,255,255,0.3);
}
.status-bar {
display: flex;
align-items: center;
gap: 16px;
padding: 8px 0;
font-size: 14px;
color: rgba(255,255,255,0.5);
}
.status-bar .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
}
.status-bar .dot.disconnected { background: #ef4444; }
.status-bar .status-text {
color: #fff;
font-weight: 600;
}
.connection-status {
position: fixed;
top: 24px;
right: 24px;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: rgba(255,255,255,0.5);
}
.connection-status .led {
width: 10px;
height: 10px;
border-radius: 50%;
background: #ef4444;
transition: background 0.3s;
}
.connection-status .led.connected { background: #22c55e; box-shadow: 0 0 8px rgba(34,197,94,0.5); }
@media (max-width: 700px) {
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="connection-status" id="connStatus">
<span class="led" id="connLed"></span>
<span id="connLabel">Disconnesso</span>
</div>
<h1>
🎾 Tennis Roots
<small>Controller</small>
</h1>
<div class="grid">
<!-- STATO IN TEMPO REALE -->
<div class="card full">
<h2>📊 Stato corrente</h2>
<div class="sets-display" id="setsDisplay"></div>
<div class="score-display">
<div class="p-score" id="p1score">
<span class="p-name" id="liveP1Name">P1</span>
<span class="playing-now" id="liveP1Serving"></span>
<span class="p-points" id="liveP1Points">0</span>
</div>
<span class="vs">VS</span>
<div class="p-score" id="p2score">
<span class="p-name" id="liveP2Name">P2</span>
<span class="playing-now" id="liveP2Serving"></span>
<span class="p-points" id="liveP2Points">0</span>
</div>
</div>
<div class="status-bar">
<span class="dot" id="statusDot"></span>
<span class="status-text" id="liveStatus">In attesa...</span>
</div>
</div>
<!-- PUNTEGGIO -->
<div class="card">
<h2>🎾 Punteggio</h2>
<div class="btn-group">
<button class="btn btn-success" onclick="addPoint(1)">🎾 Punto P1</button>
<button class="btn btn-success" onclick="addPoint(2)">🎾 Punto P2</button>
</div>
<div class="btn-group" style="margin-top:8px">
<button class="btn btn-warning" onclick="setDeuce()">⚖️ Deuce</button>
<button class="btn btn-ghost" onclick="resetGame()">🔄 Reset Game</button>
</div>
<div class="btn-group" style="margin-top:8px">
<button class="btn btn-danger" onclick="resetMatch()">🗑️ Reset Match</button>
</div>
</div>
<!-- SERVIZIO -->
<div class="card">
<h2>🎾 Servizio</h2>
<div class="field">
<label>Battuta:</label>
<select id="serverSelect">
<option value="1">P1</option>
<option value="2">P2</option>
</select>
<button class="btn btn-primary btn-sm" onclick="setServer()">Imposta</button>
</div>
</div>
<!-- GIOCATORI -->
<div class="card">
<h2>👤 Giocatori</h2>
<div class="field">
<label>P1 Nome:</label>
<input type="text" id="p1Name" value="J. Sinner" />
</div>
<div class="field">
<label>P1 Bandiera:</label>
<select class="flag-select" id="p1Flag">
<option value="it">🇮🇹</option>
<option value="es">🇪🇸</option>
<option value="us">🇺🇸</option>
<option value="fr">🇫🇷</option>
<option value="gb">🇬🇧</option>
<option value="de">🇩🇪</option>
<option value="au">🇦🇺</option>
<option value="ch">🇨🇭</option>
<option value="rs">🇷🇸</option>
<option value="gr">🇬🇷</option>
<option value="br">🇧🇷</option>
<option value="ar">🇦🇷</option>
</select>
</div>
<div class="field">
<label>P2 Nome:</label>
<input type="text" id="p2Name" value="C. Alcaraz" />
</div>
<div class="field">
<label>P2 Bandiera:</label>
<select class="flag-select" id="p2Flag">
<option value="it">🇮🇹</option>
<option value="es" selected>🇪🇸</option>
<option value="us">🇺🇸</option>
<option value="fr">🇫🇷</option>
<option value="gb">🇬🇧</option>
<option value="de">🇩🇪</option>
<option value="au">🇦🇺</option>
<option value="ch">🇨🇭</option>
<option value="rs">🇷🇸</option>
<option value="gr">🇬🇷</option>
<option value="br">🇧🇷</option>
<option value="ar">🇦🇷</option>
</select>
</div>
<div class="btn-group" style="margin-top:6px">
<button class="btn btn-primary" onclick="updatePlayers()">💾 Salva Giocatori</button>
</div>
</div>
<!-- MATCH INFO -->
<div class="card">
<h2>📋 Info Match</h2>
<div class="field">
<label>Torneo:</label>
<input type="text" id="tournament" value="Wimbledon" />
</div>
<div class="field">
<label>Round:</label>
<input type="text" id="round" value="Finale · Uomini · Singolare" />
</div>
<div class="btn-group" style="margin-top:6px">
<button class="btn btn-primary" onclick="updateMatchInfo()">💾 Salva Info</button>
</div>
</div>
<!-- SCORCIATOIE -->
<div class="card">
<h2>⌨️ Scorciatoie</h2>
<div style="font-size:13px; color:rgba(255,255,255,0.5); line-height:1.8">
<kbd style="background:rgba(255,255,255,0.1); padding:2px 6px; border-radius:3px;">1</kbd> Punto P1 &nbsp;
<kbd style="background:rgba(255,255,255,0.1); padding:2px 6px; border-radius:3px;">2</kbd> Punto P2 &nbsp;
<kbd style="background:rgba(255,255,255,0.1); padding:2px 6px; border-radius:3px;">D</kbd> Deuce &nbsp;
<kbd style="background:rgba(255,255,255,0.1); padding:2px 6px; border-radius:3px;">R</kbd> Reset Game &nbsp;
<kbd style="background:rgba(255,255,255,0.1); padding:2px 6px; border-radius:3px;">M</kbd> Reset Match
</div>
</div>
</div>
<script>
// ---- WebSocket ----
const WS_URL = `ws://${location.host}/ws`;
let ws = null;
let currentState = {};
let reconnectTimer = null;
function connect() {
if (ws && (ws.readyState === 0 || ws.readyState === 1)) return;
ws = new WebSocket(WS_URL);
ws.onopen = () => {
document.getElementById('connLed').className = 'led connected';
document.getElementById('connLabel').textContent = 'Connesso';
};
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === 'STATE_UPDATE') {
currentState = msg.state;
updateUI();
}
} catch (e) {}
};
ws.onclose = () => {
document.getElementById('connLed').className = 'led';
document.getElementById('connLabel').textContent = 'Disconnesso';
scheduleReconnect();
};
ws.onerror = () => ws.close();
}
function scheduleReconnect() {
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(connect, 2000);
}
function send(msg) {
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify(msg));
}
}
// ---- UI Update ----
function pointLabel(state) {
if (state.tiebreak) {
return [String(state.tiebreakPoints[0]), String(state.tiebreakPoints[1])];
}
const p1 = state.points[0];
const p2 = state.points[1];
if (state.deuce) {
if (state.advantage === 1) return ['AD', '40'];
if (state.advantage === 2) return ['40', 'AD'];
return ['40', '40'];
}
const map = ['0', '15', '30', '40'];
return [map[p1] || '40', map[p2] || '40'];
}
function updateUI() {
const s = currentState;
if (!s || !s.player1) return;
// Giocatori
document.getElementById('liveP1Name').textContent = s.player1;
document.getElementById('liveP2Name').textContent = s.player2;
document.getElementById('p1Name').value = s.player1;
document.getElementById('p2Name').value = s.player2;
document.getElementById('p1Flag').value = s.flag1 || 'it';
document.getElementById('p2Flag').value = s.flag2 || 'es';
document.getElementById('serverSelect').value = s.server || 1;
// Punti
const labels = pointLabel(s);
const p1el = document.getElementById('liveP1Points');
const p2el = document.getElementById('liveP2Points');
p1el.textContent = labels[0];
p2el.textContent = labels[1];
p1el.className = 'p-points' + (s.advantage === 1 ? ' adv' : '');
p2el.className = 'p-points' + (s.advantage === 2 ? ' adv' : '');
// Servizio
document.getElementById('liveP1Serving').textContent = s.server === 1 ? '● Serve' : '';
document.getElementById('liveP2Serving').textContent = s.server === 2 ? '● Serve' : '';
// Set
const setsEl = document.getElementById('setsDisplay');
let setsHtml = '';
const setLabels = ['S1', 'S2', 'S3'];
for (let i = 0; i < 3; i++) {
const ss = s.sets[i] || [0, 0];
const active = i === s.currentSet ? ' style="color:#facc15"' : '';
setsHtml += `<span><span class="set-label">${setLabels[i]}</span><span class="set-score"${active}>${ss[0]}${ss[1]}</span></span>`;
}
setsEl.innerHTML = setsHtml;
// Stato
document.getElementById('liveStatus').textContent = s.matchStatus || 'In corso';
document.getElementById('statusDot').style.background = s.matchOver ? '#22c55e' : '#facc15';
// Info match
document.getElementById('tournament').value = s.tournament || '';
document.getElementById('round').value = s.round || '';
}
// ---- Comandi ----
function addPoint(player) {
send({ type: 'COMMAND', command: 'ADD_POINT', player });
}
function resetGame() {
send({ type: 'COMMAND', command: 'RESET_GAME' });
}
function resetMatch() {
if (confirm('Reset totale del match?')) {
send({ type: 'COMMAND', command: 'RESET_MATCH' });
}
}
function setDeuce() {
send({ type: 'COMMAND', command: 'SET_DEUCE' });
}
function setServer() {
const val = parseInt(document.getElementById('serverSelect').value);
send({ type: 'COMMAND', command: 'SET_SERVER', player: val });
}
function updatePlayers() {
send({
type: 'STATE_UPDATE',
state: {
player1: document.getElementById('p1Name').value || 'P1',
player2: document.getElementById('p2Name').value || 'P2',
flag1: document.getElementById('p1Flag').value,
flag2: document.getElementById('p2Flag').value,
}
});
}
function updateMatchInfo() {
send({
type: 'STATE_UPDATE',
state: {
tournament: document.getElementById('tournament').value || 'Torneo',
round: document.getElementById('round').value || '',
}
});
}
// ---- Keyboard shortcuts ----
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
if (e.key === '1') addPoint(1);
if (e.key === '2') addPoint(2);
if (e.key === 'd' || e.key === 'D') setDeuce();
if (e.key === 'r') resetGame();
if (e.key === 'm' || e.key === 'M') resetMatch();
});
// ---- Init ----
connect();
</script>
</body>
</html>