- Aggiunge src/db.js con better-sqlite3: tabella partite con nomi, modalità, set, formazione di partenza per set, punteggi e vincitore - Salvataggio automatico al termine della partita (websocket-handler.js) - Aggiunge formInizioSet in gameState per tracciare la formazione iniziale di ogni set - Aggiunge storico.html: pagina vanilla dark-theme con lista partite espandibili (set, punteggio, formazioni) - Aggiunge server storico su porta 3002 in dev (vite-plugin-websocket.js) - Aggiunge endpoint /api/partite su displayApp (server.js) - Migliora banner di avvio con URL storico locale e da rete - Migliora log WebSocket: connessione aperta, ruolo unregistered, client rimanenti - Aggiorna .gitignore: ignora tutta la cartella data/
294 lines
7.1 KiB
HTML
294 lines
7.1 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="it">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Storico Partite</title>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
background: #111;
|
||
color: #e0e0e0;
|
||
font-family: 'Inter', system-ui, sans-serif;
|
||
min-height: 100vh;
|
||
padding: 24px 16px;
|
||
}
|
||
|
||
h1 {
|
||
text-align: center;
|
||
font-size: 22px;
|
||
font-weight: 800;
|
||
letter-spacing: 0.1em;
|
||
color: #fdd835;
|
||
text-transform: uppercase;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
#lista {
|
||
max-width: 700px;
|
||
margin: 0 auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.card {
|
||
background: #1e1e1e;
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px 20px;
|
||
gap: 12px;
|
||
user-select: none;
|
||
}
|
||
|
||
.card-header:hover {
|
||
background: rgba(255,255,255,0.04);
|
||
}
|
||
|
||
.card-data {
|
||
font-size: 11px;
|
||
color: #777;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.card-teams {
|
||
flex: 1;
|
||
text-align: center;
|
||
}
|
||
|
||
.card-nomi {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.card-result {
|
||
font-size: 22px;
|
||
font-weight: 900;
|
||
letter-spacing: 0.05em;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.card-result .winner { color: #fdd835; }
|
||
|
||
.card-modalita {
|
||
font-size: 11px;
|
||
color: #777;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.card-arrow {
|
||
font-size: 18px;
|
||
color: #555;
|
||
transition: transform 0.2s;
|
||
}
|
||
.card.open .card-arrow { transform: rotate(180deg); }
|
||
|
||
.card-detail {
|
||
display: none;
|
||
border-top: 1px solid rgba(255,255,255,0.08);
|
||
padding: 16px 20px;
|
||
}
|
||
.card.open .card-detail { display: block; }
|
||
|
||
.set-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.set-table th {
|
||
text-align: left;
|
||
padding: 6px 8px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: #777;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||
}
|
||
|
||
.set-table td {
|
||
padding: 8px 8px;
|
||
vertical-align: top;
|
||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||
}
|
||
|
||
.set-table tr:last-child td { border-bottom: none; }
|
||
|
||
.set-num {
|
||
font-weight: 800;
|
||
color: #aaa;
|
||
width: 32px;
|
||
}
|
||
|
||
.set-vince {
|
||
font-weight: 700;
|
||
color: #fdd835;
|
||
}
|
||
|
||
.set-punt {
|
||
font-weight: 700;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.form-grid {
|
||
display: inline-grid;
|
||
grid-template-columns: repeat(3, 28px);
|
||
grid-template-rows: repeat(2, 24px);
|
||
gap: 2px;
|
||
background: rgba(255,255,255,0.05);
|
||
border-radius: 6px;
|
||
padding: 4px;
|
||
}
|
||
|
||
.form-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
background: rgba(255,255,255,0.08);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.form-label {
|
||
font-size: 11px;
|
||
color: #666;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.form-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
#vuoto {
|
||
text-align: center;
|
||
color: #555;
|
||
padding: 48px 16px;
|
||
font-size: 15px;
|
||
}
|
||
|
||
#errore {
|
||
text-align: center;
|
||
color: #ef5350;
|
||
padding: 24px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>Storico Partite</h1>
|
||
<div id="lista"></div>
|
||
|
||
<script>
|
||
function formatData(iso) {
|
||
const d = new Date(iso)
|
||
return d.toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||
+ ' ' + d.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' })
|
||
}
|
||
|
||
// Layout campo: righe [fila attacco 3-2-1, fila difesa 4-5-0] → indici nell'array form
|
||
const LAYOUT = [3, 2, 1, 4, 5, 0]
|
||
|
||
function renderForm(form, label) {
|
||
const cells = LAYOUT.map(i => `<div class="form-cell">${form[i] ?? '?'}</div>`).join('')
|
||
return `<div class="form-wrap">
|
||
<div class="form-label">${label}</div>
|
||
<div class="form-grid">${cells}</div>
|
||
</div>`
|
||
}
|
||
|
||
function renderDettaglio(dati, nomiHome, nomiGuest) {
|
||
if (!dati.strisce || dati.strisce.length === 0) return '<p style="color:#555">Nessun set registrato.</p>'
|
||
|
||
const righe = dati.strisce.map(s => {
|
||
const vince = s.vincitore === 'home' ? nomiHome : nomiGuest
|
||
const formHome = s.formInizio?.home ?? []
|
||
const formGuest = s.formInizio?.guest ?? []
|
||
return `<tr>
|
||
<td class="set-num">${s.set}</td>
|
||
<td class="set-vince">${vince}</td>
|
||
<td class="set-punt">${s.punt.home} – ${s.punt.guest}</td>
|
||
<td>${renderForm(formHome, nomiHome)}</td>
|
||
<td>${renderForm(formGuest, nomiGuest)}</td>
|
||
</tr>`
|
||
}).join('')
|
||
|
||
return `<table class="set-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Set</th>
|
||
<th>Vincitore</th>
|
||
<th>Punteggio</th>
|
||
<th>Form Home</th>
|
||
<th>Form Guest</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${righe}</tbody>
|
||
</table>`
|
||
}
|
||
|
||
function renderCard(p) {
|
||
const dati = JSON.parse(p.json)
|
||
const nomeHome = p.nome_home
|
||
const nomeGuest = p.nome_guest
|
||
const vincitoreNome = p.vincitore === 'home' ? nomeHome : nomeGuest
|
||
|
||
const homeWin = p.vincitore === 'home'
|
||
const resultHome = homeWin ? `<span class="winner">${p.set_home}</span>` : p.set_home
|
||
const resultGuest = homeWin ? p.set_guest : `<span class="winner">${p.set_guest}</span>`
|
||
|
||
const card = document.createElement('div')
|
||
card.className = 'card'
|
||
card.innerHTML = `
|
||
<div class="card-header">
|
||
<div class="card-data">${formatData(p.data)}</div>
|
||
<div class="card-teams">
|
||
<div class="card-nomi">${nomeHome} vs ${nomeGuest}</div>
|
||
<div class="card-result">${resultHome} – ${resultGuest}</div>
|
||
</div>
|
||
<div class="card-modalita">${p.modalita}</div>
|
||
<div class="card-arrow">▾</div>
|
||
</div>
|
||
<div class="card-detail">
|
||
${renderDettaglio(dati, nomeHome, nomeGuest)}
|
||
</div>
|
||
`
|
||
card.querySelector('.card-header').addEventListener('click', () => {
|
||
card.classList.toggle('open')
|
||
})
|
||
return card
|
||
}
|
||
|
||
async function caricaPartite() {
|
||
const lista = document.getElementById('lista')
|
||
try {
|
||
const res = await fetch('/api/partite')
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||
const partite = await res.json()
|
||
|
||
if (partite.length === 0) {
|
||
lista.innerHTML = '<div id="vuoto">Nessuna partita registrata.</div>'
|
||
return
|
||
}
|
||
|
||
partite.forEach(p => lista.appendChild(renderCard(p)))
|
||
} catch (err) {
|
||
lista.innerHTML = `<div id="errore">Errore caricamento: ${err.message}</div>`
|
||
}
|
||
}
|
||
|
||
caricaPartite()
|
||
</script>
|
||
</body>
|
||
</html>
|