initial commit
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Clock</title>
|
||||
<style>
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clock {
|
||||
background: rgba(0,0,0,0.5);
|
||||
backdrop-filter: blur(6px);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: calc(min(8px, 0.6vw));
|
||||
padding: 0.4em 1em;
|
||||
font-size: clamp(16px, 2vw, 32px);
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
.clock { animation: fadeIn 0.8s ease-out; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="widget">
|
||||
<div class="clock" id="clock">00:00:00</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function updateClock() {
|
||||
const now = new Date();
|
||||
document.getElementById('clock').textContent =
|
||||
now.toLocaleTimeString('it-IT', { hour12: false });
|
||||
}
|
||||
setInterval(updateClock, 1000);
|
||||
updateClock();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* common.js – Stato condiviso tra i widget overlay via WebSocket
|
||||
*
|
||||
* Ogni widget si connette al server Express+WS e ascolta gli aggiornamenti.
|
||||
* Il pannello di controllo invia comandi/aggiornamenti, il server li
|
||||
* propaga a tutti i widget connessi.
|
||||
*/
|
||||
|
||||
// ---- Config ----
|
||||
const WS_URL = (() => {
|
||||
const loc = window.location;
|
||||
const protocol = loc.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return `${protocol}//${loc.host}/ws`;
|
||||
})();
|
||||
|
||||
let ws = null;
|
||||
let currentState = {};
|
||||
let subscribers = [];
|
||||
let reconnectTimer = null;
|
||||
let connectAttempts = 0;
|
||||
|
||||
// ---- WebSocket connection ----
|
||||
function connect() {
|
||||
if (ws && (ws.readyState === 0 || ws.readyState === 1)) return;
|
||||
|
||||
ws = new WebSocket(WS_URL);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('🟢 WS connected');
|
||||
connectAttempts = 0;
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === 'STATE_UPDATE' && msg.state) {
|
||||
currentState = msg.state;
|
||||
notifySubscribers();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ WS message error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('🔴 WS disconnected, reconnecting...');
|
||||
scheduleReconnect();
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.error('❌ WS error:', err);
|
||||
ws.close();
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
const delay = Math.min(1000 * 2 ** connectAttempts, 10000);
|
||||
connectAttempts++;
|
||||
reconnectTimer = setTimeout(connect, delay);
|
||||
}
|
||||
|
||||
// ---- Subscribe to state updates ----
|
||||
/**
|
||||
* Si iscrive agli aggiornamenti di stato.
|
||||
* @param {(state: object) => void} callback
|
||||
* @returns {() => void} unsubscribe function
|
||||
*/
|
||||
function onStateUpdate(callback) {
|
||||
subscribers.push(callback);
|
||||
// Chiamata immediata con stato corrente (se già presente)
|
||||
if (Object.keys(currentState).length > 0) {
|
||||
callback({ ...currentState });
|
||||
}
|
||||
return () => {
|
||||
subscribers = subscribers.filter((cb) => cb !== callback);
|
||||
};
|
||||
}
|
||||
|
||||
function notifySubscribers() {
|
||||
const state = { ...currentState };
|
||||
for (const cb of subscribers) {
|
||||
try { cb(state); } catch (e) { console.error(e); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invia un comando al server (ADD_POINT, RESET_GAME, RESET_MATCH, SET_DEUCE, SET_SERVER)
|
||||
*/
|
||||
function sendCommand(command, player = null) {
|
||||
const msg = { type: 'COMMAND', command };
|
||||
if (player !== null) msg.player = player;
|
||||
sendMessage(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invia un aggiornamento parziale di stato al server
|
||||
*/
|
||||
function sendStateUpdate(partial) {
|
||||
sendMessage({ type: 'STATE_UPDATE', state: partial });
|
||||
}
|
||||
|
||||
function sendMessage(msg) {
|
||||
if (ws && ws.readyState === 1) {
|
||||
ws.send(JSON.stringify(msg));
|
||||
} else {
|
||||
console.warn('⚠️ WS not connected, cannot send');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility: mappa punti → label tennis
|
||||
*/
|
||||
function pointLabel(state) {
|
||||
// Tiebreak: mostra punti numerici
|
||||
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'];
|
||||
}
|
||||
|
||||
// ---- Auto-connect ----
|
||||
connect();
|
||||
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Match Info</title>
|
||||
<style>
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.match-info {
|
||||
background: linear-gradient(135deg, rgba(0,0,0,0.85), rgba(20,20,30,0.85));
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
border-radius: calc(min(12px, 0.8vw));
|
||||
padding: 0.6em 1.6em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
font-size: clamp(12px, 1.4vw, 20px);
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
.match-info .tournament {
|
||||
font-weight: 700;
|
||||
color: #facc15;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.match-info .separator {
|
||||
width: 1px;
|
||||
height: 1.2em;
|
||||
background: rgba(255,255,255,0.2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.match-info .round {
|
||||
font-weight: 400;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
.match-info .status {
|
||||
font-weight: 600;
|
||||
color: rgba(255,255,255,0.9);
|
||||
}
|
||||
.match-info .status.winner {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.match-info { animation: fadeInUp 0.5s ease-out; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="widget">
|
||||
<div class="match-info">
|
||||
<span class="tournament" id="tournament">🎾 Wimbledon</span>
|
||||
<span class="separator"></span>
|
||||
<span class="round" id="round">Finale · Uomini · Singolare</span>
|
||||
<span class="separator" id="statusSep"></span>
|
||||
<span class="status" id="status">🎾 In corso · Set 1</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
function updateMatchInfo(state) {
|
||||
document.getElementById('tournament').textContent = state.tournament || '🎾 Torneo';
|
||||
document.getElementById('round').textContent = state.round || '';
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
if (state.matchOver) {
|
||||
const winner = state.sets.reduce((a, s) => a + (s[0] > s[1] ? 1 : 0), 0) >= 2
|
||||
? state.player1 : state.player2;
|
||||
statusEl.textContent = '🏆 ' + winner + ' vince!';
|
||||
statusEl.classList.add('winner');
|
||||
document.getElementById('statusSep').style.display = '';
|
||||
} else {
|
||||
statusEl.textContent = state.matchStatus || '🎾 In corso · Set ' + (state.currentSet + 1);
|
||||
statusEl.classList.remove('winner');
|
||||
document.getElementById('statusSep').style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
const unsub = onStateUpdate(updateMatchInfo);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,254 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Scoreboard</title>
|
||||
<style>
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Contenitore responsive – centra il widget */
|
||||
.widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Scoreboard – si adatta al contenuto */
|
||||
.scoreboard {
|
||||
display: inline-block;
|
||||
min-width: 380px;
|
||||
max-width: 100%;
|
||||
background: linear-gradient(160deg, rgba(0,0,0,0.88), rgba(10,10,20,0.88));
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: calc(min(16px, 1.2vw));
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
/* Header (Sets) */
|
||||
.sb-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 48px 48px 48px 60px;
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
padding: 0.5em 1.2em;
|
||||
font-size: clamp(10px, 1.2vw, 14px);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
.sb-header .col-player { text-align: left; }
|
||||
.sb-header .col-set,
|
||||
.sb-header .col-points { text-align: center; }
|
||||
|
||||
/* Player Row */
|
||||
.player-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 48px 48px 48px 60px;
|
||||
align-items: center;
|
||||
padding: 0.6em 1.2em;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
transition: background 0.3s;
|
||||
position: relative;
|
||||
font-size: clamp(14px, 1.8vw, 22px);
|
||||
}
|
||||
.player-row:last-child { border-bottom: none; }
|
||||
.player-row.serving {
|
||||
background: rgba(250, 204, 21, 0.06);
|
||||
}
|
||||
.player-row.serving::before {
|
||||
content: '●';
|
||||
position: absolute;
|
||||
left: 0.3em;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.5em;
|
||||
color: #facc15;
|
||||
text-shadow: 0 0 8px rgba(250,204,21,0.6);
|
||||
}
|
||||
|
||||
.player-row .player-name {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.player-row .player-name .flag {
|
||||
display: inline-block;
|
||||
width: 1.6em;
|
||||
height: 1.1em;
|
||||
border-radius: 2px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* Bandiere inline SVG base64 */
|
||||
.flag-it { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="1" height="2" fill="%23009246"/><rect x="1" width="1" height="2" fill="%23fff"/><rect x="2" width="1" height="2" fill="%23ce2b37"/></svg>'); }
|
||||
.flag-es { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="3" height="2" fill="%23c60b1e"/><rect y="0.5" width="3" height="1" fill="%23ffc400"/></svg>'); }
|
||||
.flag-us { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="3" height="2" fill="%23fff"/><rect width="3" height="1" fill="%23b22234"/><rect y="1" width="3" height="1" fill="%233b3b6d"/></svg>'); }
|
||||
.flag-fr { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="1" height="2" fill="%23005240"/><rect x="1" width="1" height="2" fill="%23fff"/><rect x="2" width="1" height="2" fill="%23ce2b37"/></svg>'); }
|
||||
.flag-gb { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="3" height="2" fill="%23012369"/><rect width="3" height="1" fill="%23c8102e"/><rect y="0.33" width="3" height="0.33" fill="%23fff"/><rect y="1.33" width="3" height="0.33" fill="%23fff"/></svg>'); }
|
||||
.flag-au { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="3" height="2" fill="%2300008b"/><rect width="1.5" height="1" fill="%23fff"/><path d="M0 0L1.5 1M0 1L1.5 0" stroke="%23c8102e" stroke-width="0.15"/></svg>'); }
|
||||
.flag-de { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="3" height="0.67" fill="%23000"/><rect y="0.67" width="3" height="0.67" fill="%23dd0000"/><rect y="1.33" width="3" height="0.67" fill="%23ffce00"/></svg>'); }
|
||||
.flag-ch { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="3" height="2" fill="%23d52b1e"/><rect x="1.2" y="0.4" width="0.6" height="1.2" fill="%23fff"/><rect x="0.8" y="0.8" width="1.4" height="0.4" fill="%23fff"/></svg>'); }
|
||||
.flag-rs { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="3" height="2" fill="%23fff"/><rect y="0.67" width="3" height="0.67" fill="%23005240"/><rect y="1.33" width="3" height="0.67" fill="%23c8102e"/></svg>'); }
|
||||
.flag-gr { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="3" height="2" fill="%23005b94"/><rect x="0.5" y="0.5" width="1" height="1" fill="%23fff"/><rect y="0.5" width="3" height="0.2" fill="%23fff"/></svg>'); }
|
||||
.flag-br { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="3" height="2" fill="%23009246"/><circle cx="1.5" cy="1" r="0.6" fill="%23ffcc00"/><path d="M1.5 0.6l0.3 0.5h-0.6z" fill="%23005240"/></svg>'); }
|
||||
.flag-ar { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="3" height="2" fill="%23fff"/><rect y="0.33" width="3" height="0.67" fill="%2375aadb"/><rect y="1" width="3" height="0.67" fill="%2375aadb"/></svg>'); }
|
||||
|
||||
.player-row .set-score {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
.player-row .set-score.current-set {
|
||||
color: #facc15;
|
||||
}
|
||||
.player-row .points {
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.player-row .points.deuce {
|
||||
font-size: 0.7em;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: #facc15;
|
||||
}
|
||||
.player-row .points.advantage {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
/* Animazioni */
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.scoreboard { animation: fadeInUp 0.5s ease-out; }
|
||||
|
||||
@keyframes pointFlash {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.25); }
|
||||
}
|
||||
.points.flash {
|
||||
animation: pointFlash 0.25s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="widget">
|
||||
<div class="scoreboard" id="scoreboard">
|
||||
<div class="sb-header">
|
||||
<span class="col-player">Giocatore</span>
|
||||
<span class="col-set">S1</span>
|
||||
<span class="col-set">S2</span>
|
||||
<span class="col-set">S3</span>
|
||||
<span class="col-points">Punti</span>
|
||||
</div>
|
||||
<div class="player-row" id="player1Row">
|
||||
<div class="player-name">
|
||||
<span class="flag" id="flag1"></span>
|
||||
<span id="player1Name">J. Sinner</span>
|
||||
</div>
|
||||
<span class="set-score" id="p1s1">0</span>
|
||||
<span class="set-score" id="p1s2">0</span>
|
||||
<span class="set-score" id="p1s3">0</span>
|
||||
<span class="points" id="p1points">0</span>
|
||||
</div>
|
||||
<div class="player-row" id="player2Row">
|
||||
<div class="player-name">
|
||||
<span class="flag" id="flag2"></span>
|
||||
<span id="player2Name">C. Alcaraz</span>
|
||||
</div>
|
||||
<span class="set-score" id="p2s1">0</span>
|
||||
<span class="set-score" id="p2s2">0</span>
|
||||
<span class="set-score" id="p2s3">0</span>
|
||||
<span class="points" id="p2points">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
let prevPoints = [0, 0];
|
||||
let prevDeuce = false;
|
||||
let prevAdv = 0;
|
||||
|
||||
function updateScoreboard(state) {
|
||||
// Nomi e bandiere
|
||||
document.getElementById('player1Name').textContent = state.player1;
|
||||
document.getElementById('player2Name').textContent = state.player2;
|
||||
document.getElementById('flag1').className = 'flag flag-' + (state.flag1 || 'it');
|
||||
document.getElementById('flag2').className = 'flag flag-' + (state.flag2 || 'es');
|
||||
|
||||
// Set
|
||||
for (let s = 0; s < 3; s++) {
|
||||
const e1 = document.getElementById('p1s' + (s + 1));
|
||||
const e2 = document.getElementById('p2s' + (s + 1));
|
||||
if (state.sets[s]) {
|
||||
e1.textContent = state.sets[s][0] ?? '0';
|
||||
e2.textContent = state.sets[s][1] ?? '0';
|
||||
} else {
|
||||
e1.textContent = '0';
|
||||
e2.textContent = '0';
|
||||
}
|
||||
e1.classList.toggle('current-set', s === state.currentSet);
|
||||
e2.classList.toggle('current-set', s === state.currentSet);
|
||||
}
|
||||
|
||||
// Punti
|
||||
const labels = pointLabel(state);
|
||||
const p1el = document.getElementById('p1points');
|
||||
const p2el = document.getElementById('p2points');
|
||||
p1el.textContent = labels[0];
|
||||
p2el.textContent = labels[1];
|
||||
|
||||
// Classi punti
|
||||
p1el.className = 'points';
|
||||
p2el.className = 'points';
|
||||
if (state.deuce) {
|
||||
p1el.classList.add('deuce');
|
||||
p2el.classList.add('deuce');
|
||||
if (state.advantage === 1) p1el.classList.add('advantage');
|
||||
if (state.advantage === 2) p2el.classList.add('advantage');
|
||||
}
|
||||
|
||||
// Flash su cambio punti
|
||||
const pNow = state.points;
|
||||
if (pNow[0] !== prevPoints[0] || pNow[1] !== prevPoints[1] ||
|
||||
state.deuce !== prevDeuce || state.advantage !== prevAdv) {
|
||||
const changed = pNow[0] !== prevPoints[0] ? 1 : 2;
|
||||
const el = document.getElementById('p' + changed + 'points');
|
||||
el.classList.remove('flash');
|
||||
void el.offsetWidth;
|
||||
el.classList.add('flash');
|
||||
}
|
||||
prevPoints = [...pNow];
|
||||
prevDeuce = state.deuce;
|
||||
prevAdv = state.advantage;
|
||||
|
||||
// Servizio
|
||||
document.getElementById('player1Row').classList.toggle('serving', state.server === 1);
|
||||
document.getElementById('player2Row').classList.toggle('serving', state.server === 2);
|
||||
}
|
||||
|
||||
// Ascolta aggiornamenti
|
||||
const unsub = onStateUpdate(updateScoreboard);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Server Indicator</title>
|
||||
<style>
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.server-indicator {
|
||||
background: rgba(0,0,0,0.6);
|
||||
backdrop-filter: blur(6px);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: calc(min(10px, 0.7vw));
|
||||
padding: 0.5em 1.2em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.6em;
|
||||
font-size: clamp(12px, 1.4vw, 20px);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
||||
}
|
||||
.server-indicator .dot {
|
||||
display: inline-block;
|
||||
width: 0.6em;
|
||||
height: 0.6em;
|
||||
border-radius: 50%;
|
||||
background: #facc15;
|
||||
box-shadow: 0 0 10px rgba(250,204,21,0.6);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
.server-indicator .player-label {
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
.server-indicator .player-name {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(0.9); }
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
.server-indicator { animation: fadeIn 0.5s ease-out; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="widget">
|
||||
<div class="server-indicator" id="indicator">
|
||||
<span class="dot"></span>
|
||||
<span class="player-label">Serve:</span>
|
||||
<span class="player-name" id="serverName">J. Sinner</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
function updateServer(state) {
|
||||
const name = state.server === 1 ? state.player1 : state.player2;
|
||||
document.getElementById('serverName').textContent = name;
|
||||
}
|
||||
|
||||
const unsub = onStateUpdate(updateServer);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user