feat: update quiz templates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+32
-11
@@ -10,14 +10,14 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=DM+Sans:wght@400;500&display=swap');
|
||||
*{margin:0;padding:0;box-sizing:border-box;}
|
||||
:root{
|
||||
--bg:#000510;--surface:#0d1117;--surface2:#161b22;
|
||||
--accent:#00c8ff;--green:#00ff88;--red:#ff3355;--amber:#ffcc00;--purple:#cc44ff;
|
||||
--text:#e8edf5;--muted:#6b7a99;--border:rgba(255,255,255,0.08);
|
||||
--bg:#ffffff;--surface:#f0f4f8;--surface2:#e2e8f0;
|
||||
--accent:#0077cc;--green:#00ff88;--red:#ff3355;--amber:#ffcc00;--purple:#cc44ff;
|
||||
--text:#0a0f1e;--muted:#4a5568;--border:rgba(0,0,0,0.12);
|
||||
--pixel:'Press Start 2P',monospace;
|
||||
}
|
||||
html{font-size:17px;}
|
||||
html,body{width:100%;height:100vh;background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;overflow:hidden;}
|
||||
body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.1) 2px,rgba(0,0,0,0.1) 4px);pointer-events:none;z-index:9999;}
|
||||
.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;}
|
||||
.grid-bg{position:fixed;inset:0;background-image:linear-gradient(rgba(0,119,204,0.06) 1px,transparent 1px),linear-gradient(90deg,rgba(0,119,204,0.06) 1px,transparent 1px);background-size:40px 40px;pointer-events:none;}
|
||||
|
||||
.screen{display:none;height:100vh;padding:1.5rem 2rem;flex-direction:column;}
|
||||
.screen.active{display:flex;}
|
||||
@@ -53,6 +53,8 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie
|
||||
.hbtn-secondary:hover{border-color:var(--muted);color:var(--text);}
|
||||
.hbtn-danger{border-color:var(--red);color:var(--red);background:transparent;}
|
||||
.hbtn-danger:hover{background:rgba(255,51,85,.1);}
|
||||
.hbtn-warning{border-color:var(--amber);color:var(--amber);background:transparent;}
|
||||
.hbtn-warning:hover{background:rgba(255,204,0,.1);}
|
||||
|
||||
/* ── QUESTION ── */
|
||||
#question-screen{justify-content:space-between;}
|
||||
@@ -62,8 +64,10 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie
|
||||
.timer-big.urgent{color:var(--red);animation:blink .5s step-end infinite;}
|
||||
.q-body{display:flex;gap:1.5rem;flex:1;}
|
||||
.q-left{flex:1;}
|
||||
.q-text{font-family:var(--pixel);font-size:clamp(.55rem,1.2vw,.75rem);line-height:2;margin-bottom:1.2rem;color:var(--text);}
|
||||
.opts-grid{display:grid;grid-template-columns:1fr 1fr;gap:.6rem;}
|
||||
.q-text{margin-bottom:1.2rem;}
|
||||
.q-es{font-family:var(--pixel);font-size:clamp(.55rem,1.1vw,.72rem);line-height:2;color:var(--text);display:block;}
|
||||
.q-en{font-family:var(--pixel);font-size:clamp(.45rem,.9vw,.6rem);line-height:1.8;color:var(--muted);display:block;margin-top:.25rem;}
|
||||
.opts-grid{display:flex;flex-direction:column;gap:.6rem;}
|
||||
.opt-box{background:var(--surface);border:1.5px solid var(--border);border-radius:8px;padding:.8rem 1rem;font-size:.82rem;display:flex;align-items:center;gap:.6rem;}
|
||||
.opt-box.correct{border-color:var(--green);background:rgba(0,255,136,.07);}
|
||||
.opt-letter{font-family:var(--pixel);font-size:.55rem;min-width:24px;color:var(--muted);}
|
||||
@@ -175,6 +179,7 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie
|
||||
</div>
|
||||
<div class="start-row">
|
||||
<button class="hbtn hbtn-primary" id="start-btn" disabled onclick="startGame()">▶ START GAME</button>
|
||||
<button class="hbtn hbtn-warning" onclick="clearLobby()">🧹 CLEAR LOBBY</button>
|
||||
<button class="hbtn hbtn-danger" onclick="clearScores()">🗑 CLEAR SCORES</button>
|
||||
<button class="hbtn hbtn-secondary" onclick="location.href='/scores'">📊 HISTORY</button>
|
||||
</div>
|
||||
@@ -280,11 +285,22 @@ function handleMessage(msg) {
|
||||
else if (msg.type === 'question') {
|
||||
currentQ = msg.number;
|
||||
document.getElementById('q-meta').textContent = `Q ${msg.number} / ${msg.total}`;
|
||||
document.getElementById('host-q-text').textContent = msg.q;
|
||||
const qParts = msg.q.split('\n\n[EN]');
|
||||
const qEs = qParts[0].trim();
|
||||
const qEn = qParts[1] ? qParts[1].trim() : '';
|
||||
document.getElementById('host-q-text').innerHTML =
|
||||
`<span class="q-es">${qEs}</span>${qEn ? `<span class="q-en">[EN] ${qEn}</span>` : ''}`;
|
||||
const letters = ['A','B','C','D'];
|
||||
document.getElementById('host-opts').innerHTML = msg.options.map((o,i) =>
|
||||
`<div class="opt-box" id="hopt-${i}"><span class="opt-letter">${letters[i]}</span>${o}</div>`
|
||||
).join('');
|
||||
const optColors = ['#5b9cf6','#cc44ff','#00ff88','#ffcc00'];
|
||||
document.getElementById('host-opts').innerHTML = msg.options.map((o,i) => {
|
||||
const parts = o.split(' / ');
|
||||
const oEs = parts[0] || o;
|
||||
const oEn = parts.slice(1).join(' / ') || '';
|
||||
return `<div class="opt-box" id="hopt-${i}" style="border-color:${optColors[i]};background:${optColors[i]}18">
|
||||
<span class="opt-letter" style="color:${optColors[i]}">${letters[i]}</span>
|
||||
<span><span style="display:block;font-size:.83rem">${oEs}</span>${oEn ? `<span style="display:block;font-size:.7rem;color:var(--muted)">${oEn}</span>` : ''}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
document.getElementById('ans-count').textContent = '0';
|
||||
document.getElementById('ans-sub').textContent = `of ${currentTotal} players`;
|
||||
document.getElementById('ans-bar').style.width = '0%';
|
||||
@@ -357,6 +373,11 @@ async function clearScores() {
|
||||
else alert('Error al borrar scores.');
|
||||
}
|
||||
|
||||
function clearLobby() {
|
||||
if (!confirm('¿Remover todos los jugadores del lobby? Los jugadores serán enviados de regreso al join screen.')) return;
|
||||
ws.send(JSON.stringify({type: 'clear_lobby'}));
|
||||
}
|
||||
|
||||
function buildPodium(lb) {
|
||||
const medals = ['🥇','🥈','🥉'];
|
||||
const pbClass = ['pb1','pb2','pb3'];
|
||||
|
||||
+98
-99
@@ -10,7 +10,7 @@
|
||||
:root{
|
||||
--bg:#000510;--surface:#0a0e1a;--surface2:#111827;
|
||||
--accent:#00c8ff;--green:#00ff88;--red:#ff3355;--amber:#ffcc00;--purple:#cc44ff;
|
||||
--text:#e8edf5;--muted:#4a5568;--border:rgba(255,255,255,0.08);
|
||||
--text:#e8edf5;--muted:#8a9bb5;--border:rgba(255,255,255,0.08);
|
||||
--pixel:'Press Start 2P',monospace;
|
||||
}
|
||||
html,body{width:100%;height:100%;background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;overflow:hidden;touch-action:none;}
|
||||
@@ -39,21 +39,21 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie
|
||||
|
||||
/* ── JOIN ── */
|
||||
#join-screen .logo{font-family:var(--pixel);font-size:1rem;color:var(--accent);margin-bottom:.3rem;text-shadow:0 0 20px var(--accent);line-height:1.8;}
|
||||
#join-screen .sub{font-size:.75rem;color:var(--muted);margin-bottom:2rem;}
|
||||
.field-wrap{width:100%;max-width:320px;margin-bottom:.8rem;text-align:left;}
|
||||
.field-wrap label{font-family:var(--pixel);font-size:.45rem;color:var(--muted);letter-spacing:.1em;display:block;margin-bottom:.4rem;}
|
||||
#join-screen .sub{font-size:.85rem;color:var(--text);margin-bottom:2rem;opacity:.75;}
|
||||
.field-wrap{width:100%;max-width:380px;margin-bottom:1rem;text-align:left;}
|
||||
.field-wrap label{font-family:var(--pixel);font-size:.55rem;color:var(--accent);letter-spacing:.12em;display:block;margin-bottom:.5rem;}
|
||||
.field-wrap input, .field-wrap select{
|
||||
width:100%;padding:.85rem 1rem;
|
||||
background:var(--surface2);border:1px solid var(--border);border-radius:4px;
|
||||
color:var(--text);font-size:.9rem;font-family:'DM Sans',sans-serif;
|
||||
width:100%;padding:1rem 1.1rem;
|
||||
background:var(--surface2);border:1.5px solid var(--border);border-radius:6px;
|
||||
color:var(--text);font-size:1.1rem;font-family:'DM Sans',sans-serif;
|
||||
outline:none;
|
||||
}
|
||||
.field-wrap input:focus,.field-wrap select:focus{border-color:var(--accent);}
|
||||
.field-wrap input::placeholder{color:var(--muted);}
|
||||
.field-wrap input::placeholder{color:#5a6a80;}
|
||||
.field-wrap select option{background:var(--surface2);}
|
||||
#join-btn{
|
||||
width:100%;max-width:320px;padding:1rem;margin-top:.5rem;
|
||||
background:var(--accent);border:none;border-radius:4px;
|
||||
width:100%;max-width:380px;padding:1.1rem;margin-top:.6rem;
|
||||
background:var(--accent);border:none;border-radius:6px;
|
||||
color:#000;font-family:var(--pixel);font-size:.6rem;letter-spacing:.1em;
|
||||
cursor:pointer;
|
||||
}
|
||||
@@ -96,13 +96,11 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie
|
||||
#score-hud .hud-score{font-family:var(--pixel);font-size:.5rem;color:var(--accent);}
|
||||
#score-hud .hud-timer{font-family:var(--pixel);font-size:.75rem;color:var(--amber);}
|
||||
#score-hud .hud-q{font-family:var(--pixel);font-size:.4rem;color:var(--muted);}
|
||||
#question-banner{
|
||||
font-family:'DM Sans',sans-serif;font-size:.88rem;font-weight:500;
|
||||
color:var(--text);line-height:1.5;text-align:center;
|
||||
background:rgba(0,5,16,.9);border:1px solid var(--border);
|
||||
border-radius:6px;padding:.55rem .9rem;
|
||||
/* clamp so it never grows past ~25% of viewport */
|
||||
max-height:28vh;overflow:hidden;
|
||||
#projector-hint{
|
||||
font-family:var(--pixel);font-size:.42rem;
|
||||
color:var(--muted);line-height:1.5;text-align:center;
|
||||
letter-spacing:.08em;padding:.3rem 0;
|
||||
animation:blink 2s step-end infinite;
|
||||
}
|
||||
|
||||
/* ── RESULT FLASH ── */
|
||||
@@ -156,12 +154,12 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie
|
||||
<!-- STARS BG -->
|
||||
<canvas class="pixel-stars" id="stars-canvas"></canvas>
|
||||
|
||||
<!-- LANGUAGE SELECT -->
|
||||
<!-- SPLASH — goes straight to join -->
|
||||
<div class="screen active" id="lang-screen">
|
||||
<div class="title">🌐 NI QUIZ</div>
|
||||
<div class="subtitle">NETWORK INTELLIGENCE · LCPR</div>
|
||||
<button class="lang-btn en" onclick="setLang('en')">🇺🇸 ENGLISH</button>
|
||||
<button class="lang-btn es" onclick="setLang('es')">🇵🇷 ESPAÑOL</button>
|
||||
|
||||
<button class="lang-btn es" onclick="goJoin()">▶ JUGAR / PLAY</button>
|
||||
</div>
|
||||
|
||||
<!-- JOIN -->
|
||||
@@ -180,13 +178,11 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie
|
||||
<label id="lbl-dept">DEPARTMENT</label>
|
||||
<select id="inp-dept">
|
||||
<option value="">-- Select --</option>
|
||||
<option>Network Intelligence</option>
|
||||
<option>NOC</option>
|
||||
<option>Network Engineering</option>
|
||||
<option>Field Operations</option>
|
||||
<option>IT</option>
|
||||
<option>Planning</option>
|
||||
<option>Other / Otro</option>
|
||||
<option>Finanzas</option>
|
||||
<option>People</option>
|
||||
<option>Legal</option>
|
||||
<option>B2B</option>
|
||||
<option>B2C</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="join-btn">▶ ENTER GAME</button>
|
||||
@@ -210,7 +206,7 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie
|
||||
<span class="hud-timer" id="hud-timer">15</span>
|
||||
<span class="hud-q" id="hud-q">Q1/5</span>
|
||||
</div>
|
||||
<div id="question-banner"></div>
|
||||
<div id="projector-hint"></div>
|
||||
</div>
|
||||
<div id="result-flash">
|
||||
<div id="flash-icon"></div>
|
||||
@@ -290,16 +286,16 @@ function t(key, vars={}) {
|
||||
return s;
|
||||
}
|
||||
|
||||
function setLang(l) {
|
||||
lang = l;
|
||||
// Update join labels
|
||||
document.getElementById('lbl-fname').textContent = t('fname');
|
||||
document.getElementById('lbl-lname').textContent = t('lname');
|
||||
document.getElementById('lbl-dept').textContent = t('dept');
|
||||
document.getElementById('join-btn').textContent = t('enter');
|
||||
document.getElementById('inp-dept').options[0].text = t('dept_select');
|
||||
function goJoin() {
|
||||
lang = 'es';
|
||||
document.getElementById('lbl-fname').textContent = 'NOMBRE / NAME';
|
||||
document.getElementById('lbl-lname').textContent = 'APELLIDO / LAST NAME';
|
||||
document.getElementById('lbl-dept').textContent = 'DEPARTAMENTO / DEPARTMENT';
|
||||
document.getElementById('join-btn').textContent = '▶ ENTRAR / JOIN';
|
||||
document.getElementById('inp-dept').options[0].text = '-- Selecciona / Select --';
|
||||
show('join-screen');
|
||||
}
|
||||
function setLang(l) { goJoin(); }
|
||||
|
||||
// ── SCREENS ──────────────────────────────────────────────────────────────────
|
||||
function show(id) {
|
||||
@@ -328,6 +324,10 @@ function handleMessage(msg) {
|
||||
}
|
||||
else if (msg.type === 'phase') {
|
||||
if (msg.phase === 'lobby' && myName) show('lobby-screen');
|
||||
else if (msg.phase === 'join') {
|
||||
myName = ''; myScore = 0; myDept = '';
|
||||
show('join-screen');
|
||||
}
|
||||
}
|
||||
else if (msg.type === 'question') {
|
||||
startGame(msg);
|
||||
@@ -392,62 +392,35 @@ function resizeCanvas() {
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
resizeCanvas();
|
||||
|
||||
const COL_COLORS = ['#00c8ff','#cc44ff','#00ff88','#ffcc00'];
|
||||
const COL_COLORS = ['#5b9cf6','#cc44ff','#00ff88','#ffcc00']; // A=blue B=purple C=green D=yellow
|
||||
|
||||
function startGame(msg) {
|
||||
resizeCanvas();
|
||||
const W = canvas.width, H = canvas.height;
|
||||
const colW = W / 4;
|
||||
|
||||
// Invader block sizing — responsive to screen width
|
||||
// On 375px wide: colW=93, invW≈77, invH≈90 (enough for 4-5 lines at 13px)
|
||||
const invW = colW - 12;
|
||||
const invPadX = 20; // space for letter badge on left
|
||||
const textAreaW = invW - invPadX - 10;
|
||||
const fontSize = Math.max(12, Math.min(14, W * 0.034)); // 12–14px
|
||||
const lineH = fontSize * 1.4;
|
||||
|
||||
// Pre-measure how many lines each option needs, then set uniform height
|
||||
const measCtx = canvas.getContext('2d');
|
||||
measCtx.font = `${fontSize}px 'DM Sans', sans-serif`;
|
||||
function countLines(text, maxW) {
|
||||
const words = text.split(' ');
|
||||
let line = '', count = 1;
|
||||
words.forEach(w => {
|
||||
const test = line ? line + ' ' + w : w;
|
||||
if (measCtx.measureText(test).width > maxW && line) { count++; line = w; }
|
||||
else line = test;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
const maxLines = Math.max(...msg.options.map(o => countLines(o, textAreaW)));
|
||||
const badgeH = 20; // letter badge row
|
||||
const invH = badgeH + maxLines * lineH + 20; // 20px vertical padding
|
||||
// Fixed invader size — no text inside, just letter
|
||||
const invW = Math.min(colW - 16, 90);
|
||||
const invH = invW; // square-ish UFO shape
|
||||
|
||||
gameState = {
|
||||
ship: {x: W/2, targetX: W/2, y: H - 120, w: 36, h: 28},
|
||||
bullets: [],
|
||||
invaders: msg.options.map((text, idx) => ({
|
||||
x: colW * idx + colW/2,
|
||||
y: 120,
|
||||
targetY: 120,
|
||||
y: 0,
|
||||
targetY: 0,
|
||||
w: invW,
|
||||
h: invH,
|
||||
text,
|
||||
idx,
|
||||
hit: false,
|
||||
hitTimer: 0,
|
||||
correct: idx === msg.correct_idx_hint,
|
||||
fontSize,
|
||||
lineH,
|
||||
textAreaW,
|
||||
})),
|
||||
particles: [],
|
||||
answered: false,
|
||||
correctIdx: msg.correct_idx_hint ?? -1,
|
||||
timeLeft: msg.time_limit,
|
||||
timerStart: performance.now(),
|
||||
options: msg.options,
|
||||
correctAnswer: msg.correct_idx_hint,
|
||||
W, H, colW,
|
||||
totalTime: msg.time_limit,
|
||||
@@ -455,18 +428,18 @@ function startGame(msg) {
|
||||
qTotal: msg.total,
|
||||
};
|
||||
|
||||
document.getElementById('question-banner').textContent = msg.q;
|
||||
document.getElementById('projector-hint').textContent = lang === 'es' ? '👆 MIRA EL PROYECTOR' : '👆 LOOK AT THE SCREEN';
|
||||
document.getElementById('hud-q').textContent = `${t('q_prefix')}${msg.number}${t('of')}${msg.total}`;
|
||||
document.getElementById('result-flash').classList.remove('active');
|
||||
|
||||
// Measure overlay height so invaders don't spawn behind it
|
||||
const overlay = document.getElementById('game-overlay');
|
||||
const overlayH = overlay ? overlay.getBoundingClientRect().height : 100;
|
||||
const spawnY = overlayH + invH / 2 + 20; // first visible position just below overlay
|
||||
const spawnY = overlayH + invH / 2 + 20;
|
||||
|
||||
// Invaders start high and slowly descend
|
||||
// Invaders start above screen and descend into position
|
||||
gameState.invaders.forEach((inv, i) => {
|
||||
inv.y = -invH - i * 25;
|
||||
inv.y = -invH - i * 30;
|
||||
inv.targetY = spawnY;
|
||||
});
|
||||
|
||||
@@ -545,38 +518,62 @@ function gameFrame(ts) {
|
||||
// Slowly descend after appearing
|
||||
if (!gs.answered) {
|
||||
inv.y += (inv.targetY - inv.y) * 0.08;
|
||||
inv.targetY += 0.25; // drift down slowly
|
||||
inv.targetY += 0.25;
|
||||
}
|
||||
|
||||
const color = COL_COLORS[i];
|
||||
// Box
|
||||
ctx.fillStyle = `rgba(${hexToRgb(color)},0.10)`;
|
||||
const letters = ['A','B','C','D'];
|
||||
const urgent = timeLeft <= 3 && !gs.answered && i === gs.correctIdx;
|
||||
|
||||
// Shake offset when urgent
|
||||
const shakeX = urgent ? Math.sin(ts * 0.045 + i * 1.3) * 4 : 0;
|
||||
const shakeY = urgent ? Math.cos(ts * 0.05 + i * 0.9) * 3 : 0;
|
||||
const cx = inv.x + shakeX, cy = inv.y + shakeY;
|
||||
const r = inv.w / 2;
|
||||
|
||||
// Urgent pulse: glow intensity oscillates fast
|
||||
const pulse = urgent ? 0.5 + 0.5 * Math.sin(ts * 0.025) : 0;
|
||||
|
||||
// UFO dome (top half ellipse)
|
||||
ctx.fillStyle = `rgba(${hexToRgb(color)},${0.18 + pulse * 0.22})`;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 1.5;
|
||||
roundRect(ctx, inv.x - inv.w/2, inv.y - inv.h/2, inv.w, inv.h, 8);
|
||||
ctx.lineWidth = urgent ? 2 + pulse * 2 : 2;
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = urgent ? 10 + pulse * 24 : 0;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, r, r * 0.55, 0, Math.PI, 0);
|
||||
ctx.fill(); ctx.stroke();
|
||||
|
||||
// Letter badge — top-left, full row
|
||||
const letters = ['A','B','C','D'];
|
||||
const badgeSize = 13;
|
||||
ctx.fillStyle = color;
|
||||
ctx.font = `bold ${badgeSize}px 'Press Start 2P', monospace`;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(letters[i], inv.x - inv.w/2 + 8, inv.y - inv.h/2 + 7);
|
||||
// UFO saucer body (full ellipse)
|
||||
ctx.fillStyle = `rgba(${hexToRgb(color)},${0.25 + pulse * 0.2})`;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = urgent ? 2 + pulse * 2 : 2;
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = urgent ? 14 + pulse * 20 : 0;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, r, r * 0.32, 0, 0, Math.PI * 2);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Answer text — wrap inside block, below badge row
|
||||
const fz = inv.fontSize || 12;
|
||||
const lh = inv.lineH || 17;
|
||||
const maxW = inv.textAreaW || (inv.w - 28);
|
||||
ctx.fillStyle = '#e8edf5';
|
||||
ctx.font = `${fz}px 'DM Sans', sans-serif`;
|
||||
// Glow under saucer
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = urgent ? 18 + pulse * 28 : 14;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy + r * 0.1, r * 0.6, r * 0.12, 0, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(${hexToRgb(color)},${0.35 + pulse * 0.35})`;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Big centered letter
|
||||
const letterSize = Math.max(18, r * 0.85);
|
||||
ctx.fillStyle = color;
|
||||
ctx.font = `bold ${letterSize}px 'Press Start 2P', monospace`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
// Text block starts after badge (20px) + padding (4px)
|
||||
const textBlockTop = inv.y - inv.h/2 + 24;
|
||||
const textBlockH = inv.h - 28;
|
||||
wrapText(ctx, inv.text, inv.x, textBlockTop + textBlockH/2, maxW, lh);
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = urgent ? 16 + pulse * 20 : 10;
|
||||
ctx.fillText(letters[i], cx, cy - r * 0.28);
|
||||
ctx.shadowBlur = 0;
|
||||
});
|
||||
|
||||
// ── SHIP ──
|
||||
@@ -612,12 +609,14 @@ function gameFrame(ts) {
|
||||
ctx.fillRect(b.x - 2, b.y, 4, 16);
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Collision check
|
||||
// Collision check — circular hitbox matching UFO saucer radius
|
||||
if (!gs.answered) {
|
||||
gs.invaders.forEach((inv, i) => {
|
||||
if (inv.hit) return;
|
||||
if (b.x > inv.x - inv.w/2 && b.x < inv.x + inv.w/2 &&
|
||||
b.y > inv.y - inv.h/2 && b.y < inv.y + inv.h/2) {
|
||||
const r = inv.w / 2;
|
||||
const dx = b.x - inv.x;
|
||||
const dy = b.y - inv.y;
|
||||
if (Math.sqrt(dx*dx + dy*dy) < r) {
|
||||
b.y = -999; // destroy bullet
|
||||
if (!gs.answered) {
|
||||
gs.answered = true;
|
||||
|
||||
+255
-168
@@ -3,169 +3,251 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NI Quiz · Historial de Scores</title>
|
||||
<title>NI Quiz · Historial</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=DM+Sans:wght@400;500&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Exo+2:wght@400;600;800;900&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);
|
||||
--bg:#ffffff;
|
||||
--surface:#f0f4f8;
|
||||
--surface2:#e2e8f0;
|
||||
--accent:#0077cc;
|
||||
--accent2:#cc0033;
|
||||
--warn:#ffb800;
|
||||
--text:#0a0f1e;
|
||||
--muted:#4a5568;
|
||||
--mono:'Share Tech Mono',monospace;
|
||||
--sans:'Exo 2',sans-serif;
|
||||
}
|
||||
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);}
|
||||
html,body{min-height:100vh;background:var(--bg);color:var(--text);font-family:var(--sans);}
|
||||
|
||||
main{padding:2rem;max-width:900px;margin:0 auto;position:relative;z-index:1;}
|
||||
body::before{
|
||||
content:'';position:fixed;inset:0;
|
||||
background-image:
|
||||
linear-gradient(rgba(0,119,204,0.06) 1px,transparent 1px),
|
||||
linear-gradient(90deg,rgba(0,119,204,0.06) 1px,transparent 1px);
|
||||
background-size:40px 40px;pointer-events:none;z-index:0;
|
||||
}
|
||||
|
||||
#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;}
|
||||
header{
|
||||
position:relative;z-index:1;
|
||||
padding:1.75rem 2rem 1.25rem;
|
||||
border-bottom:1px solid rgba(0,119,204,0.12);
|
||||
display:flex;align-items:flex-end;justify-content:space-between;flex-wrap:wrap;gap:1rem;
|
||||
}
|
||||
.header-left h1{
|
||||
font-family:var(--sans);font-weight:900;font-size:2rem;letter-spacing:-0.03em;
|
||||
color:var(--accent);text-shadow:0 0 20px rgba(0,119,204,0.4);line-height:1;
|
||||
}
|
||||
.header-left h1 span{color:var(--accent2);}
|
||||
.header-left p{font-family:var(--mono);font-size:.55rem;color:var(--muted);letter-spacing:.12em;margin-top:.3rem;}
|
||||
.nav-links{display:flex;gap:.5rem;}
|
||||
.nav-links a{
|
||||
font-family:var(--mono);font-size:.6rem;color:var(--muted);
|
||||
text-decoration:none;padding:.35rem .8rem;
|
||||
border:1px solid rgba(0,119,204,0.15);border-radius:2px;
|
||||
transition:all .15s;letter-spacing:.08em;
|
||||
}
|
||||
.nav-links a:hover{color:var(--accent);border-color:rgba(0,119,204,0.4);}
|
||||
|
||||
/* 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);}
|
||||
main{position:relative;z-index:1;max-width:960px;margin:0 auto;padding:2rem;}
|
||||
|
||||
/* loading / empty */
|
||||
#loading{text-align:center;color:var(--muted);padding:4rem;font-family:var(--mono);font-size:.75rem;letter-spacing:.1em;}
|
||||
#loading::after{content:'...';animation:dots 1.2s step-end infinite;}
|
||||
@keyframes dots{0%{content:''}33%{content:'.'}66%{content:'..'}100%{content:'...'}}
|
||||
#empty{display:none;text-align:center;padding:4rem;font-family:var(--mono);font-size:.7rem;color:var(--muted);}
|
||||
#empty .icon{font-size:3rem;margin-bottom:1rem;display:block;}
|
||||
#content{display:none;}
|
||||
|
||||
/* section */
|
||||
.section{margin-bottom:2.5rem;}
|
||||
.section-title{display:flex;align-items:center;gap:.75rem;margin-bottom:1.25rem;}
|
||||
.section-title h2{font-family:var(--sans);font-weight:900;font-size:1.1rem;letter-spacing:-0.01em;}
|
||||
.section-title .pill{
|
||||
font-family:var(--mono);font-size:.45rem;letter-spacing:.15em;
|
||||
padding:.2rem .5rem;border-radius:2px;
|
||||
background:rgba(0,119,204,0.1);border:1px solid rgba(0,119,204,0.3);color:var(--accent);
|
||||
}
|
||||
|
||||
/* top cards */
|
||||
.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;
|
||||
flex:1;min-width:140px;
|
||||
background:var(--surface);border:1px solid rgba(0,119,204,0.15);border-radius:4px;
|
||||
padding:.9rem 1.1rem;display:flex;align-items:center;gap:.75rem;
|
||||
transition:border-color .2s;
|
||||
}
|
||||
.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);}
|
||||
.top-card:hover{border-color:rgba(0,119,204,0.35);}
|
||||
.top-card.gold{border-color:rgba(255,184,0,.35);background:rgba(255,184,0,.04);}
|
||||
.top-card.silver{border-color:rgba(180,180,180,.25);}
|
||||
.top-card.bronze{border-color:rgba(160,90,30,.25);}
|
||||
.top-medal{font-size:1.6rem;}
|
||||
.top-info .top-name{font-weight:700;font-size:.9rem;margin-bottom:.15rem;}
|
||||
.top-info .top-score{font-family:var(--sans);font-weight:900;font-size:1.2rem;letter-spacing:-0.02em;color:var(--accent);line-height:1;}
|
||||
.top-info .top-meta{font-family:var(--mono);font-size:.45rem;color:var(--muted);margin-top:.15rem;}
|
||||
|
||||
/* 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;}
|
||||
/* ── TABS ── */
|
||||
.tabs{display:flex;gap:.5rem;margin-bottom:1.75rem;flex-wrap:wrap;border-bottom:1px solid rgba(0,119,204,0.12);padding-bottom:1rem;}
|
||||
.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;
|
||||
padding:.45rem 1.1rem;border-radius:2px;font-family:var(--mono);font-size:.62rem;
|
||||
letter-spacing:.1em;border:1px solid rgba(0,119,204,0.2);
|
||||
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.active{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:700;}
|
||||
.tab-btn:hover:not(.active){border-color:var(--accent);color:var(--accent);}
|
||||
.tab-panel{display:none;}.tab-panel.active{display:block;}
|
||||
.tab-panel{display:none;}
|
||||
.tab-panel.active{display:block;}
|
||||
|
||||
/* DEPT SECTION */
|
||||
.dept-block{margin-bottom:1.5rem;}
|
||||
/* ── TABLES ── */
|
||||
.table-wrap{background:var(--surface);border:1px solid rgba(0,119,204,0.15);border-radius:4px;overflow:hidden;}
|
||||
table{width:100%;border-collapse:collapse;}
|
||||
thead th{
|
||||
text-align:left;font-family:var(--mono);font-size:.5rem;letter-spacing:.12em;
|
||||
color:var(--muted);padding:.65rem 1.1rem;
|
||||
border-bottom:1px solid rgba(0,119,204,0.1);
|
||||
}
|
||||
tbody td{padding:.6rem 1.1rem;font-size:.85rem;border-bottom:1px solid rgba(0,0,0,0.04);}
|
||||
tbody tr:last-child td{border:none;}
|
||||
tbody tr:hover{background:var(--surface2);}
|
||||
.td-rank{font-family:var(--mono);font-weight:700;color:var(--muted);font-size:.8rem;}
|
||||
.td-score{font-family:var(--sans);font-weight:800;color:var(--accent);font-size:.95rem;}
|
||||
.td-medal{font-size:1rem;min-width:28px;}
|
||||
.td-dept{font-family:var(--mono);font-size:.7rem;color:var(--muted);}
|
||||
|
||||
/* ── DEPT SECTION ── */
|
||||
.dept-block{margin-bottom:1.25rem;border-radius:4px;overflow:hidden;border:1px solid rgba(0,119,204,0.15);}
|
||||
.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;
|
||||
padding:.7rem 1.1rem;background:var(--surface);
|
||||
border-bottom:1px solid rgba(0,119,204,0.1);
|
||||
}
|
||||
.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;}
|
||||
.dept-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;}
|
||||
.dept-name{font-family:var(--sans);font-weight:800;font-size:.95rem;}
|
||||
.dept-count{font-family:var(--mono);font-size:.5rem;color:var(--muted);margin-left:auto;}
|
||||
|
||||
/* ── SESSIONS ── */
|
||||
.session-card{
|
||||
background:var(--surface);border:1px solid rgba(0,119,204,0.15);border-radius:4px;
|
||||
margin-bottom:.75rem;overflow:hidden;
|
||||
}
|
||||
.session-header{
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
padding:.9rem 1.1rem;cursor:pointer;transition:background .15s;
|
||||
}
|
||||
.session-header:hover{background:var(--surface2);}
|
||||
.session-meta{display:flex;align-items:center;gap:.9rem;flex-wrap:wrap;}
|
||||
.session-id{font-family:var(--mono);font-size:.5rem;color:var(--muted);letter-spacing:.1em;}
|
||||
.session-date{font-size:.85rem;font-weight:600;}
|
||||
.session-badge{
|
||||
font-family:var(--mono);font-size:.5rem;
|
||||
padding:.2rem .55rem;border-radius:2px;
|
||||
background:rgba(0,119,204,0.1);border:1px solid rgba(0,119,204,0.3);color:var(--accent);
|
||||
}
|
||||
.session-right{display:flex;align-items:center;gap:.75rem;}
|
||||
.session-players{font-family:var(--mono);font-size:.5rem;color:var(--muted);}
|
||||
.chevron{color:var(--muted);transition:transform .2s;font-size:.9rem;}
|
||||
|
||||
.session-body{display:none;border-top:1px solid rgba(0,119,204,0.1);}
|
||||
.session-body.open{display:block;}
|
||||
|
||||
/* animations */
|
||||
@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
|
||||
.fade-up{animation:fadeUp .4s ease both;}
|
||||
</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 class="header-left">
|
||||
<h1>NI<span>QUIZ</span></h1>
|
||||
<p>// HISTORIAL DE SCORES · NETWORK INTELLIGENCE · LCPR</p>
|
||||
</div>
|
||||
<nav class="nav-links">
|
||||
<a href="/host">← HOST PANEL</a>
|
||||
<a href="/">JUGAR</a>
|
||||
</nav>
|
||||
</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="loading">CARGANDO HISTORIAL</div>
|
||||
<div id="empty"><span class="icon">📡</span>Aún no hay sesiones guardadas.<br>Los scores se guardan al terminar el juego.</div>
|
||||
|
||||
<div id="content" style="display:none;">
|
||||
<div class="all-time">
|
||||
<div class="section-title">🏆 <span>Top jugadores — todas las sesiones</span></div>
|
||||
<div id="content">
|
||||
|
||||
<!-- TOP PLAYERS -->
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<h2>🏆 Top Jugadores</h2>
|
||||
<span class="pill">MEJOR SCORE</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>
|
||||
<button class="tab-btn active" onclick="switchTab('global')">📊 RANKING 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 class="section-title">
|
||||
<h2>📊 Ranking Global</h2>
|
||||
<span class="pill">TODAS LAS SESIONES · MEJOR SCORE</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>JUGADOR</th>
|
||||
<th>DEPARTAMENTO</th>
|
||||
<th>MEJOR SCORE</th>
|
||||
<th>SESIONES</th>
|
||||
<th>PROMEDIO</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="global-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DEPT TAB -->
|
||||
<div class="tab-panel" id="tab-dept">
|
||||
<div class="section-title">🏢 <span>Ranking por departamento</span></div>
|
||||
<div class="section-title">
|
||||
<h2>🏢 Rankings por Departamento</h2>
|
||||
<span class="pill">MEJOR SCORE POR JUGADOR</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 class="section-title">
|
||||
<h2>📋 Historial de Sesiones</h2>
|
||||
<span class="pill" id="session-count-pill">0 JUEGOS</span>
|
||||
</div>
|
||||
<div id="sessions-list"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const medals = ['🥇','🥈','🥉'];
|
||||
const rankClasses = ['gold','silver','bronze'];
|
||||
const rankClass = ['gold','silver','bronze'];
|
||||
const DEPT_COLORS = [
|
||||
'#00c8ff','#cc44ff','#00ff88','#ffcc00','#ff6b35','#5b9cf6','#f472b6','#34d399'
|
||||
'#0077cc','#cc0033','#ffb800','#cc44ff','#00994d','#ff6b00','#5b9cf6','#f472b6'
|
||||
];
|
||||
|
||||
function switchTab(name) {
|
||||
const names = ['global','dept','sessions'];
|
||||
document.querySelectorAll('.tab-btn').forEach((b,i) => {
|
||||
b.classList.toggle('active', ['global','dept','sessions'][i] === name);
|
||||
b.classList.toggle('active', names[i] === name);
|
||||
});
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
@@ -176,134 +258,139 @@ async function load() {
|
||||
const data = await res.json();
|
||||
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('content').style.display = 'block';
|
||||
|
||||
document.getElementById('session-count-pill').textContent =
|
||||
data.length ? `${data.length} JUEGO${data.length>1?'S':''}` : '0 JUEGOS';
|
||||
|
||||
if (!data.length) {
|
||||
document.getElementById('empty').style.display = 'block';
|
||||
document.getElementById('top-row').innerHTML =
|
||||
`<p style="font-family:var(--mono);font-size:.65rem;color:var(--muted);padding:.5rem 0">Sin sesiones todavía — los scores se guardan al terminar el juego.</p>`;
|
||||
document.getElementById('global-tbody').innerHTML =
|
||||
`<tr><td colspan="6" style="font-family:var(--mono);font-size:.65rem;color:var(--muted);text-align:center;padding:2rem">Sin datos</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('content').style.display = 'block';
|
||||
|
||||
// ── BUILD PLAYER MAP ──
|
||||
// allScores[name] = { best, sessions, dept }
|
||||
const allScores = {};
|
||||
// ── BUILD PLAYER MAP ──────────────────────────────────────────────────────
|
||||
const playerMap = {};
|
||||
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' };
|
||||
if (!playerMap[key]) {
|
||||
playerMap[key] = { best: p.score, total: p.score, sessions: 1, dept: p.dept || '—' };
|
||||
} 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
|
||||
if (p.score > playerMap[key].best) playerMap[key].best = p.score;
|
||||
playerMap[key].total += p.score;
|
||||
playerMap[key].sessions++;
|
||||
if (p.dept) playerMap[key].dept = p.dept;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const sorted = Object.entries(allScores).sort((a,b) => b[1].best - a[1].best);
|
||||
const sorted = Object.entries(playerMap).sort((a,b) => b[1].best - a[1].best);
|
||||
|
||||
// ── TOP 5 CARDS ──
|
||||
// ── 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.className = `top-card ${rankClass[i]||''} fade-up`;
|
||||
card.style.animationDelay = `${i*.06}s`;
|
||||
card.innerHTML = `
|
||||
<div class="top-medal">${i < 3 ? medals[i] : `#${i+1}`}</div>
|
||||
<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 class="top-score">${info.best.toLocaleString()}</div>
|
||||
<div class="top-meta">${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);
|
||||
// ── GLOBAL TABLE ──────────────────────────────────────────────────────────
|
||||
const tbody = document.getElementById('global-tbody');
|
||||
sorted.forEach(([name, info], i) => {
|
||||
const avg = Math.round(info.total / info.sessions);
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td class="td-medal">${i<3?medals[i]:`<span class="td-rank">#${i+1}</span>`}</td>
|
||||
<td style="font-weight:600">${name}</td>
|
||||
<td class="td-dept">${info.dept}</td>
|
||||
<td class="td-score">${info.best.toLocaleString()}</td>
|
||||
<td style="font-family:var(--mono);font-size:.75rem;color:var(--muted)">${info.sessions}</td>
|
||||
<td style="font-family:var(--mono);font-size:.75rem;color:var(--muted)">${avg.toLocaleString()}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// ── DEPT RANKING ──
|
||||
// Group players by dept, pick best score per player, sum for dept total
|
||||
// ── DEPT RANKINGS ─────────────────────────────────────────────────────────
|
||||
const depts = {};
|
||||
sorted.forEach(([name, info]) => {
|
||||
const d = info.dept || 'Sin departamento';
|
||||
const d = info.dept || '—';
|
||||
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 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.className = 'dept-block fade-up';
|
||||
block.style.animationDelay = `${di*.05}s`;
|
||||
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 class="dept-count">${players.length} JUGADOR${players.length>1?'ES':''}</div>
|
||||
</div>
|
||||
<table class="dept-table">
|
||||
<tbody>${players.map((p, i) => `
|
||||
<div class="table-wrap" style="border:none;border-radius:0;">
|
||||
<table>
|
||||
<thead><tr><th></th><th>JUGADOR</th><th>MEJOR SCORE</th><th>SESIONES</th></tr></thead>
|
||||
<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>
|
||||
<td class="td-medal">${i<3?medals[i]:`<span class="td-rank">#${i+1}</span>`}</td>
|
||||
<td style="font-weight:600">${p.name}</td>
|
||||
<td class="td-score">${p.best.toLocaleString()}</td>
|
||||
<td style="font-family:var(--mono);font-size:.75rem;color:var(--muted)">${p.sessions}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>`;
|
||||
</table>
|
||||
</div>`;
|
||||
deptList.appendChild(block);
|
||||
});
|
||||
|
||||
// ── SESSIONS ──
|
||||
// ── 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'
|
||||
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.className = 'session-card fade-up';
|
||||
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>` : ''}
|
||||
${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>
|
||||
<div class="session-right">
|
||||
<span class="session-players">${session.total_players} JUGADOR${session.total_players!==1?'ES':''}</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) => `
|
||||
<table>
|
||||
<thead><tr><th></th><th>JUGADOR</th><th>DEPARTAMENTO</th><th>SCORE</th><th>RANK</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>
|
||||
<td class="td-medal">${i<3?medals[i]:''}</td>
|
||||
<td style="font-weight:600">${p.player}</td>
|
||||
<td class="td-dept">${p.dept || '—'}</td>
|
||||
<td class="td-score">${p.score.toLocaleString()}</td>
|
||||
<td style="font-family:var(--mono);font-size:.7rem;color:var(--muted)">#${p.rank}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user