Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
503 lines
20 KiB
HTML
503 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Route Rush · History</title>
|
|
<style>
|
|
@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:#ffffff;
|
|
--surface:#f0f4f8;
|
|
--surface2:#e2e8f0;
|
|
--accent:#0077cc;
|
|
--accent2:#cc0033;
|
|
--warn:#ffb800;
|
|
--boost:#00ff88;
|
|
--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:var(--sans);}
|
|
|
|
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;
|
|
}
|
|
|
|
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);}
|
|
|
|
main{position:relative;z-index:1;max-width:960px;margin:0 auto;padding:2rem;}
|
|
|
|
#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{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-row{display:flex;gap:.75rem;flex-wrap:wrap;}
|
|
.top-card{
|
|
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: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;}
|
|
|
|
.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:.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:#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;}
|
|
|
|
.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);}
|
|
|
|
/* ── GROUP SCORE TABLE ── */
|
|
.group-score-intro{
|
|
font-family:var(--mono);font-size:.6rem;color:var(--muted);
|
|
margin-bottom:1.25rem;line-height:1.6;letter-spacing:.03em;
|
|
padding:.75rem 1rem;
|
|
background:rgba(0,119,204,0.04);border:1px solid rgba(0,119,204,0.12);border-radius:4px;
|
|
}
|
|
.group-score-intro strong{color:var(--accent);}
|
|
|
|
.prize-bar-wrap{display:flex;align-items:center;gap:.6rem;min-width:120px;}
|
|
.prize-bar{height:6px;border-radius:3px;background:rgba(0,119,204,0.12);flex:1;overflow:hidden;}
|
|
.prize-bar-fill{height:100%;border-radius:3px;transition:width .4s ease;}
|
|
.prize-pts{
|
|
font-family:var(--mono);font-weight:700;font-size:.8rem;
|
|
min-width:36px;text-align:right;
|
|
}
|
|
|
|
.td-pct{font-family:var(--mono);font-size:.72rem;color:var(--muted);}
|
|
.td-prize{font-family:var(--sans);font-weight:800;font-size:1rem;}
|
|
|
|
/* row highlight for top group */
|
|
tbody tr.group-winner td{background:rgba(255,184,0,.06);}
|
|
tbody tr.group-winner .td-prize{color:var(--warn);}
|
|
|
|
/* ── DEPT BLOCKS (individual breakdown) ── */
|
|
.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:.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%;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;}
|
|
|
|
.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-rounds{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;}
|
|
|
|
@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
|
|
.fade-up{animation:fadeUp .4s ease both;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<div class="header-left">
|
|
<h1>ROUTE<span>RUSH</span></h1>
|
|
<p>// SCORE HISTORY</p>
|
|
</div>
|
|
<nav class="nav-links">
|
|
<a href="host">← HOST PANEL</a>
|
|
<a href=".">PLAY</a>
|
|
</nav>
|
|
</header>
|
|
|
|
<main>
|
|
<div id="loading">LOADING HISTORY</div>
|
|
<div id="empty"><span class="icon">📡</span>No sessions yet.<br>Scores are saved when a game ends.</div>
|
|
|
|
<div id="content">
|
|
|
|
<!-- TOP PLAYERS -->
|
|
<div class="section">
|
|
<div class="section-title">
|
|
<h2>🏆 All-Time Top Players</h2>
|
|
<span class="pill">BEST 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 RANKING</button>
|
|
<button class="tab-btn" onclick="switchTab('dept')">🏢 BY DEPARTMENT</button>
|
|
<button class="tab-btn" onclick="switchTab('sessions')">📋 SESSIONS</button>
|
|
</div>
|
|
|
|
<!-- GLOBAL TAB -->
|
|
<div class="tab-panel active" id="tab-global">
|
|
<div class="section-title">
|
|
<h2>📊 Global Ranking</h2>
|
|
<span class="pill">ALL SESSIONS · BEST SCORE</span>
|
|
</div>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<th>PLAYER</th>
|
|
<th>DEPARTMENT</th>
|
|
<th>BEST SCORE</th>
|
|
<th>SESSIONS</th>
|
|
<th>AVG SCORE</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="global-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DEPT TAB -->
|
|
<div class="tab-panel" id="tab-dept">
|
|
|
|
<!-- GROUP PRIZE SCORE -->
|
|
<div class="section">
|
|
<div class="section-title">
|
|
<h2>🎯 Group Prize Score</h2>
|
|
<span class="pill">AVG · MAX 25 PTS</span>
|
|
</div>
|
|
<div class="group-score-intro">
|
|
Prize points are based on each group's <strong>average individual score</strong> across all rounds.<br>
|
|
<strong>3,000 avg = 25 pts</strong> · Score scales linearly downward · Max score per player: <strong>4,500 pts</strong>
|
|
</div>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<th>DEPARTMENT</th>
|
|
<th>PLAYERS</th>
|
|
<th>AVG SCORE</th>
|
|
<th>% OF MAX</th>
|
|
<th>PRIZE PTS</th>
|
|
<th style="min-width:140px"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="group-score-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- INDIVIDUAL BREAKDOWN BY DEPT -->
|
|
<div class="section">
|
|
<div class="section-title">
|
|
<h2>🏢 Player Breakdown by Department</h2>
|
|
<span class="pill">BEST SCORE PER PLAYER</span>
|
|
</div>
|
|
<div id="dept-list"></div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- SESSIONS TAB -->
|
|
<div class="tab-panel" id="tab-sessions">
|
|
<div class="section-title">
|
|
<h2>📋 Session History</h2>
|
|
<span class="pill" id="session-count-pill">0 GAMES</span>
|
|
</div>
|
|
<div id="sessions-list"></div>
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
const medals = ['🥇','🥈','🥉'];
|
|
const rankClass = ['gold','silver','bronze'];
|
|
const DEPT_COLORS = [
|
|
'#0077cc','#cc0033','#ffb800','#cc44ff','#00994d','#ff6b00','#5b9cf6','#f472b6'
|
|
];
|
|
|
|
// Scoring constants (must match main.py)
|
|
const MAX_SCORE = 4500;
|
|
const PRIZE_THRESHOLD = 3000;
|
|
const MAX_PRIZE = 25;
|
|
|
|
function switchTab(name) {
|
|
const names = ['global','dept','sessions'];
|
|
document.querySelectorAll('.tab-btn').forEach((b,i) => {
|
|
b.classList.toggle('active', names[i] === name);
|
|
});
|
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
|
document.getElementById('tab-' + name).classList.add('active');
|
|
}
|
|
|
|
function prizeColor(pts) {
|
|
if (pts >= 23) return '#ffb800'; // gold zone
|
|
if (pts >= 18) return '#0077cc'; // blue
|
|
if (pts >= 12) return '#00994d'; // green
|
|
return '#cc0033'; // red / low
|
|
}
|
|
|
|
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';
|
|
document.getElementById('session-count-pill').textContent = `${data.length} GAME${data.length>1?'S':''}`;
|
|
|
|
// ── BUILD PLAYER MAP ──────────────────────────────────────────────────────
|
|
const playerMap = {};
|
|
data.forEach(s => {
|
|
s.players.forEach(p => {
|
|
const key = p.player;
|
|
if (!playerMap[key]) {
|
|
playerMap[key] = { best: p.score, total: p.score, sessions: 1, dept: p.dept || '—' };
|
|
} else {
|
|
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(playerMap).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 ${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-info">
|
|
<div class="top-name">${name}</div>
|
|
<div class="top-score">${info.best.toLocaleString()}</div>
|
|
<div class="top-meta">${info.dept} · ${info.sessions} SESSION${info.sessions>1?'S':''}</div>
|
|
</div>`;
|
|
topRow.appendChild(card);
|
|
});
|
|
|
|
// ── 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" style="${i>=3?'':'display:none'}">#${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);
|
|
});
|
|
|
|
// ── BUILD DEPT MAP ────────────────────────────────────────────────────────
|
|
const depts = {};
|
|
sorted.forEach(([name, info]) => {
|
|
const d = info.dept || '—';
|
|
if (!depts[d]) depts[d] = [];
|
|
depts[d].push({ name, best: info.best, total: info.total, sessions: info.sessions });
|
|
});
|
|
|
|
// ── GROUP PRIZE SCORE TABLE ───────────────────────────────────────────────
|
|
// Compute avg score per dept using best scores (fairest cross-session metric)
|
|
const groupRows = Object.entries(depts).map(([dept, players]) => {
|
|
const avgScore = Math.round(players.reduce((s, p) => s + p.best, 0) / players.length);
|
|
const pct = ((avgScore / MAX_SCORE) * 100).toFixed(1);
|
|
const prize = Math.min(MAX_PRIZE, Math.round((avgScore / PRIZE_THRESHOLD) * MAX_PRIZE));
|
|
return { dept, players: players.length, avgScore, pct, prize };
|
|
}).sort((a, b) => b.avgScore - a.avgScore);
|
|
|
|
const groupTbody = document.getElementById('group-score-tbody');
|
|
groupRows.forEach((row, i) => {
|
|
const color = prizeColor(row.prize);
|
|
const barPct = Math.min(100, (row.prize / MAX_PRIZE) * 100);
|
|
const tr = document.createElement('tr');
|
|
if (i === 0) tr.className = 'group-winner';
|
|
tr.innerHTML = `
|
|
<td class="td-medal">${i<3 ? medals[i] : ''}<span class="td-rank" style="${i>=3?'':'display:none'}">#${i+1}</span></td>
|
|
<td style="font-weight:700">${row.dept}</td>
|
|
<td style="font-family:var(--mono);font-size:.75rem;color:var(--muted)">${row.players}</td>
|
|
<td class="td-score">${row.avgScore.toLocaleString()}</td>
|
|
<td class="td-pct">${row.pct}%</td>
|
|
<td class="td-prize" style="color:${color}">${row.prize}</td>
|
|
<td>
|
|
<div class="prize-bar-wrap">
|
|
<div class="prize-bar">
|
|
<div class="prize-bar-fill" style="width:${barPct}%;background:${color}"></div>
|
|
</div>
|
|
<span class="prize-pts" style="color:${color}">${row.prize}/${MAX_PRIZE}</span>
|
|
</div>
|
|
</td>`;
|
|
groupTbody.appendChild(tr);
|
|
});
|
|
|
|
// ── DEPT INDIVIDUAL BREAKDOWN ─────────────────────────────────────────────
|
|
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 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} PLAYER${players.length>1?'S':''}</div>
|
|
</div>
|
|
<div class="table-wrap" style="border:none;border-radius:0;">
|
|
<table>
|
|
<thead><tr><th></th><th>PLAYER</th><th>BEST SCORE</th><th>SESSIONS</th></tr></thead>
|
|
<tbody>
|
|
${players.map((p, i) => `
|
|
<tr>
|
|
<td class="td-medal">${i<3?medals[i]:''}<span class="td-rank" style="${i>=3?'':'display:none'}">#${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>
|
|
</div>`;
|
|
deptList.appendChild(block);
|
|
});
|
|
|
|
// ── SESSIONS ──────────────────────────────────────────────────────────────
|
|
const list = document.getElementById('sessions-list');
|
|
data.forEach(session => {
|
|
const date = new Date(session.started_at + 'Z').toLocaleString('en-US', {
|
|
month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'
|
|
});
|
|
const winner = session.players[0];
|
|
const card = document.createElement('div');
|
|
card.className = 'session-card fade-up';
|
|
card.innerHTML = `
|
|
<div class="session-header" onclick="toggle(this)">
|
|
<div class="session-meta">
|
|
<span class="session-id">SESSION #${session.id}</span>
|
|
<span class="session-date">${date}</span>
|
|
${winner ? `<span class="session-badge">🥇 ${winner.player} · ${winner.score.toLocaleString()} PTS</span>` : ''}
|
|
</div>
|
|
<div class="session-right">
|
|
<span class="session-rounds">${session.rounds} RND${session.rounds!==1?'S':''}</span>
|
|
<span class="chevron">▼</span>
|
|
</div>
|
|
</div>
|
|
<div class="session-body">
|
|
<table>
|
|
<thead><tr><th></th><th>PLAYER</th><th>DEPARTMENT</th><th>SCORE</th><th>RANK</th></tr></thead>
|
|
<tbody>
|
|
${session.players.map((p,i) => `
|
|
<tr>
|
|
<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>
|
|
</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> |