325 lines
13 KiB
HTML
325 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>NI Quiz · Historial de Scores</title>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=DM+Sans:wght@400;500&display=swap');
|
|
*{margin:0;padding:0;box-sizing:border-box;}
|
|
:root{
|
|
--bg:#0a0e1a;--surface:#111827;--surface2:#1a2235;
|
|
--accent:#00c8ff;--green:#10b981;--amber:#f59e0b;--purple:#7c3aed;
|
|
--text:#e8edf5;--muted:#6b7a99;--border:rgba(255,255,255,0.08);
|
|
}
|
|
html,body{min-height:100vh;background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;}
|
|
.grid-bg{position:fixed;inset:0;background-image:linear-gradient(rgba(0,200,255,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,200,255,0.03) 1px,transparent 1px);background-size:40px 40px;pointer-events:none;}
|
|
|
|
header{padding:2rem 2rem 1rem;border-bottom:1px solid var(--border);position:relative;z-index:1;}
|
|
header h1{font-family:'Syne',sans-serif;font-size:1.8rem;color:var(--accent);}
|
|
header p{font-size:.85rem;color:var(--muted);margin-top:.2rem;}
|
|
.nav-links{margin-top:.8rem;display:flex;gap:.8rem;}
|
|
.nav-links a{font-size:.8rem;color:var(--muted);text-decoration:none;padding:.25rem .7rem;border:1px solid var(--border);border-radius:6px;}
|
|
.nav-links a:hover{color:var(--text);}
|
|
|
|
main{padding:2rem;max-width:900px;margin:0 auto;position:relative;z-index:1;}
|
|
|
|
#loading{text-align:center;color:var(--muted);padding:3rem;font-size:.9rem;}
|
|
#empty{display:none;text-align:center;color:var(--muted);padding:3rem;}
|
|
#empty .icon{font-size:3rem;margin-bottom:1rem;}
|
|
|
|
/* ALL-TIME TOP 5 */
|
|
.all-time{margin-bottom:2.5rem;}
|
|
.section-title{font-family:'Syne',sans-serif;font-size:1rem;margin-bottom:1rem;display:flex;align-items:center;gap:.6rem;}
|
|
.section-title span{font-size:.7rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);}
|
|
.top-row{display:flex;gap:.75rem;flex-wrap:wrap;}
|
|
.top-card{
|
|
background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
|
padding:.9rem 1.1rem;display:flex;align-items:center;gap:.8rem;
|
|
min-width:160px;flex:1;
|
|
}
|
|
.top-card.gold{border-color:rgba(245,158,11,.5);background:rgba(245,158,11,.05);}
|
|
.top-card.silver{border-color:rgba(200,200,200,.3);}
|
|
.top-card.bronze{border-color:rgba(180,120,60,.3);}
|
|
.top-medal{font-size:1.4rem;}
|
|
.top-info{flex:1;}
|
|
.top-name{font-weight:500;font-size:.9rem;}
|
|
.top-score{font-family:'Syne',sans-serif;font-size:1.1rem;color:var(--accent);font-weight:700;}
|
|
.top-sessions{font-size:.7rem;color:var(--muted);}
|
|
|
|
/* SESSIONS */
|
|
.session-card{
|
|
background:var(--surface);border:1px solid var(--border);border-radius:14px;
|
|
margin-bottom:1rem;overflow:hidden;
|
|
}
|
|
.session-header{
|
|
padding:1rem 1.2rem;display:flex;align-items:center;justify-content:space-between;
|
|
cursor:pointer;
|
|
}
|
|
.session-header:hover{background:var(--surface2);}
|
|
.session-meta{display:flex;align-items:center;gap:1rem;}
|
|
.session-id{font-size:.7rem;letter-spacing:.1em;color:var(--muted);font-family:'Syne',sans-serif;}
|
|
.session-date{font-size:.85rem;font-weight:500;}
|
|
.session-badge{
|
|
padding:.2rem .6rem;border-radius:100px;font-size:.7rem;
|
|
background:rgba(0,200,255,.08);border:1px solid rgba(0,200,255,.2);color:var(--accent);
|
|
}
|
|
.chevron{color:var(--muted);transition:transform .2s;font-size:1rem;}
|
|
.session-body{display:none;border-top:1px solid var(--border);}
|
|
.session-body.open{display:block;}
|
|
|
|
.scores-table{width:100%;border-collapse:collapse;}
|
|
.scores-table th{
|
|
text-align:left;font-size:.7rem;letter-spacing:.12em;text-transform:uppercase;
|
|
color:var(--muted);padding:.7rem 1.2rem;border-bottom:1px solid var(--border);
|
|
}
|
|
.scores-table td{padding:.65rem 1.2rem;font-size:.85rem;border-bottom:1px solid rgba(255,255,255,.03);}
|
|
.scores-table tr:last-child td{border:none;}
|
|
.rank-cell{font-family:'Syne',sans-serif;font-weight:700;color:var(--muted);}
|
|
.score-cell{font-family:'Syne',sans-serif;color:var(--accent);font-weight:700;}
|
|
.medal-cell{font-size:1.1rem;}
|
|
|
|
/* TABS */
|
|
.tabs{display:flex;gap:.5rem;margin-bottom:1.5rem;flex-wrap:wrap;}
|
|
.tab-btn{
|
|
padding:.4rem 1rem;border-radius:100px;font-size:.78rem;font-weight:500;
|
|
border:1px solid var(--border);background:transparent;color:var(--muted);
|
|
cursor:pointer;transition:all .15s;
|
|
}
|
|
.tab-btn.active{background:var(--accent);border-color:var(--accent);color:#000;font-weight:700;}
|
|
.tab-btn:hover:not(.active){border-color:var(--accent);color:var(--accent);}
|
|
.tab-panel{display:none;}.tab-panel.active{display:block;}
|
|
|
|
/* DEPT SECTION */
|
|
.dept-block{margin-bottom:1.5rem;}
|
|
.dept-header{
|
|
display:flex;align-items:center;gap:.7rem;
|
|
padding:.6rem .9rem;border-radius:10px 10px 0 0;
|
|
background:var(--surface2);border:1px solid var(--border);border-bottom:none;
|
|
}
|
|
.dept-dot{width:10px;height:10px;border-radius:50%;}
|
|
.dept-name{font-family:'Syne',sans-serif;font-size:.95rem;font-weight:700;}
|
|
.dept-count{font-size:.75rem;color:var(--muted);}
|
|
.dept-table{width:100%;border-collapse:collapse;background:var(--surface);border:1px solid var(--border);border-radius:0 0 10px 10px;overflow:hidden;}
|
|
.dept-table td{padding:.6rem 1rem;font-size:.85rem;border-bottom:1px solid rgba(255,255,255,.03);}
|
|
.dept-table tr:last-child td{border:none;}
|
|
.dept-rank{font-family:'Syne',sans-serif;font-weight:700;color:var(--muted);width:36px;}
|
|
.dept-score{font-family:'Syne',sans-serif;color:var(--accent);font-weight:700;text-align:right;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="grid-bg"></div>
|
|
|
|
<header>
|
|
<h1>🌐 NI Quiz · Historial</h1>
|
|
<p>Network Intelligence · LCPR — Scores guardados por sesión</p>
|
|
<div class="nav-links">
|
|
<a href="/host">← Host Panel</a>
|
|
<a href="/">Jugar</a>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
<div id="loading">Cargando historial...</div>
|
|
<div id="empty"><div class="icon">📭</div><p>Aún no hay sesiones guardadas.<br>Los scores se guardan al terminar el juego.</p></div>
|
|
|
|
<div id="content" style="display:none;">
|
|
<div class="all-time">
|
|
<div class="section-title">🏆 <span>Top jugadores — todas las sesiones</span></div>
|
|
<div class="top-row" id="top-row"></div>
|
|
</div>
|
|
|
|
<!-- TABS -->
|
|
<div class="tabs">
|
|
<button class="tab-btn active" onclick="switchTab('global')">🌐 Global</button>
|
|
<button class="tab-btn" onclick="switchTab('dept')">🏢 Por departamento</button>
|
|
<button class="tab-btn" onclick="switchTab('sessions')">📋 Sesiones</button>
|
|
</div>
|
|
|
|
<!-- GLOBAL TAB -->
|
|
<div class="tab-panel active" id="tab-global">
|
|
<div class="section-title">🥇 <span>Ranking global — mejor score histórico</span></div>
|
|
<div id="global-list"></div>
|
|
</div>
|
|
|
|
<!-- DEPT TAB -->
|
|
<div class="tab-panel" id="tab-dept">
|
|
<div class="section-title">🏢 <span>Ranking por departamento</span></div>
|
|
<div id="dept-list"></div>
|
|
</div>
|
|
|
|
<!-- SESSIONS TAB -->
|
|
<div class="tab-panel" id="tab-sessions">
|
|
<div class="section-title">📋 <span>Sesiones recientes</span></div>
|
|
<div id="sessions-list"></div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
const medals = ['🥇','🥈','🥉'];
|
|
const rankClasses = ['gold','silver','bronze'];
|
|
const DEPT_COLORS = [
|
|
'#00c8ff','#cc44ff','#00ff88','#ffcc00','#ff6b35','#5b9cf6','#f472b6','#34d399'
|
|
];
|
|
|
|
function switchTab(name) {
|
|
document.querySelectorAll('.tab-btn').forEach((b,i) => {
|
|
b.classList.toggle('active', ['global','dept','sessions'][i] === name);
|
|
});
|
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
|
document.getElementById('tab-' + name).classList.add('active');
|
|
}
|
|
|
|
async function load() {
|
|
const res = await fetch('/api/scores');
|
|
const data = await res.json();
|
|
|
|
document.getElementById('loading').style.display = 'none';
|
|
|
|
if (!data.length) {
|
|
document.getElementById('empty').style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
document.getElementById('content').style.display = 'block';
|
|
|
|
// ── BUILD PLAYER MAP ──
|
|
// allScores[name] = { best, sessions, dept }
|
|
const allScores = {};
|
|
data.forEach(s => {
|
|
s.players.forEach(p => {
|
|
const key = p.player;
|
|
if (!allScores[key]) {
|
|
allScores[key] = { best: p.score, sessions: 1, dept: p.dept || 'Sin departamento' };
|
|
} else {
|
|
if (p.score > allScores[key].best) allScores[key].best = p.score;
|
|
allScores[key].sessions++;
|
|
if (p.dept) allScores[key].dept = p.dept; // keep most recent dept
|
|
}
|
|
});
|
|
});
|
|
|
|
const sorted = Object.entries(allScores).sort((a,b) => b[1].best - a[1].best);
|
|
|
|
// ── TOP 5 CARDS ──
|
|
const topRow = document.getElementById('top-row');
|
|
sorted.slice(0, 5).forEach(([name, info], i) => {
|
|
const card = document.createElement('div');
|
|
card.className = `top-card ${rankClasses[i] || ''}`;
|
|
card.innerHTML = `
|
|
<div class="top-medal">${i < 3 ? medals[i] : `#${i+1}`}</div>
|
|
<div class="top-info">
|
|
<div class="top-name">${name}</div>
|
|
<div class="top-score">${info.best.toLocaleString()} pts</div>
|
|
<div class="top-sessions">${info.dept || '—'} · ${info.sessions} sesión${info.sessions > 1 ? 'es' : ''}</div>
|
|
</div>`;
|
|
topRow.appendChild(card);
|
|
});
|
|
|
|
// ── GLOBAL RANKING TABLE ──
|
|
const globalList = document.getElementById('global-list');
|
|
const globalTable = document.createElement('table');
|
|
globalTable.className = 'scores-table';
|
|
globalTable.style.cssText = 'background:var(--surface);border:1px solid var(--border);border-radius:10px;overflow:hidden;';
|
|
globalTable.innerHTML = `
|
|
<thead><tr>
|
|
<th></th><th>Jugador</th><th>Departamento</th><th>Mejor score</th><th>Sesiones</th>
|
|
</tr></thead>
|
|
<tbody>${sorted.map(([ name, info], i) => `
|
|
<tr>
|
|
<td class="medal-cell">${i < 3 ? medals[i] : `<span style="font-family:'Syne',sans-serif;color:var(--muted)">#${i+1}</span>`}</td>
|
|
<td style="font-weight:500">${name}</td>
|
|
<td style="font-size:.78rem;color:var(--muted)">${info.dept || '—'}</td>
|
|
<td class="score-cell">${info.best.toLocaleString()}</td>
|
|
<td style="font-size:.78rem;color:var(--muted)">${info.sessions}</td>
|
|
</tr>`).join('')}
|
|
</tbody>`;
|
|
globalList.appendChild(globalTable);
|
|
|
|
// ── DEPT RANKING ──
|
|
// Group players by dept, pick best score per player, sum for dept total
|
|
const depts = {};
|
|
sorted.forEach(([name, info]) => {
|
|
const d = info.dept || 'Sin departamento';
|
|
if (!depts[d]) depts[d] = [];
|
|
depts[d].push({ name, best: info.best, sessions: info.sessions });
|
|
});
|
|
|
|
// Sort depts by their top player's score
|
|
const deptsSorted = Object.entries(depts).sort((a, b) => b[1][0].best - a[1][0].best);
|
|
|
|
const deptList = document.getElementById('dept-list');
|
|
deptsSorted.forEach(([dept, players], di) => {
|
|
const color = DEPT_COLORS[di % DEPT_COLORS.length];
|
|
const block = document.createElement('div');
|
|
block.className = 'dept-block';
|
|
block.innerHTML = `
|
|
<div class="dept-header">
|
|
<div class="dept-dot" style="background:${color}"></div>
|
|
<div class="dept-name">${dept}</div>
|
|
<div class="dept-count">${players.length} jugador${players.length > 1 ? 'es' : ''}</div>
|
|
</div>
|
|
<table class="dept-table">
|
|
<tbody>${players.map((p, i) => `
|
|
<tr>
|
|
<td class="dept-rank">${i < 3 ? medals[i] : '#' + (i+1)}</td>
|
|
<td style="font-weight:500">${p.name}</td>
|
|
<td class="dept-score">${p.best.toLocaleString()} pts</td>
|
|
</tr>`).join('')}
|
|
</tbody>
|
|
</table>`;
|
|
deptList.appendChild(block);
|
|
});
|
|
|
|
// ── SESSIONS ──
|
|
const list = document.getElementById('sessions-list');
|
|
data.forEach(session => {
|
|
const date = new Date(session.started_at + 'Z').toLocaleString('es-PR', {
|
|
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
|
|
});
|
|
const winner = session.players[0];
|
|
const card = document.createElement('div');
|
|
card.className = 'session-card';
|
|
card.innerHTML = `
|
|
<div class="session-header" onclick="toggle(this)">
|
|
<div class="session-meta">
|
|
<span class="session-id">SESIÓN #${session.id}</span>
|
|
<span class="session-date">${date}</span>
|
|
${winner ? `<span class="session-badge">🥇 ${winner.player} · ${winner.score.toLocaleString()} pts</span>` : ''}
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:.8rem;">
|
|
<span style="font-size:.75rem;color:var(--muted);">${session.total_players} jugadores</span>
|
|
<span class="chevron">▼</span>
|
|
</div>
|
|
</div>
|
|
<div class="session-body">
|
|
<table class="scores-table">
|
|
<thead><tr>
|
|
<th></th><th>Jugador</th><th>Departamento</th><th>Puntaje</th>
|
|
</tr></thead>
|
|
<tbody>${session.players.map((p,i) => `
|
|
<tr>
|
|
<td class="medal-cell">${i < 3 ? medals[i] : ''}</td>
|
|
<td>${p.player}</td>
|
|
<td style="font-size:.78rem;color:var(--muted)">${p.dept || '—'}</td>
|
|
<td class="score-cell">${p.score.toLocaleString()}</td>
|
|
</tr>`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>`;
|
|
list.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function toggle(header) {
|
|
const body = header.nextElementSibling;
|
|
const chevron = header.querySelector('.chevron');
|
|
body.classList.toggle('open');
|
|
chevron.style.transform = body.classList.contains('open') ? 'rotate(180deg)' : '';
|
|
}
|
|
|
|
load();
|
|
</script>
|
|
</body>
|
|
</html> |