<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>CyberPatriot Leaderboard</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
body { font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; background: #0f172a; color: #e6eef8; margin: 0; padding: 24px; }
.container { max-width: 960px; margin: 0 auto; }
header { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:16px; }
h1 { font-size:1.4rem; margin:0; }
.meta { color:#9fb3d8; font-size:0.9rem; }
table { width:100%; border-collapse:collapse; background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); border-radius:8px; overflow:hidden; }
th, td { padding:10px 12px; text-align:left; font-size:0.95rem; }
th { background: rgba(255,255,255,0.03); color:#cfe5ff; position:sticky; top:0; z-index:2; }
tr { border-bottom:1px solid rgba(255,255,255,0.03); }
tr:hover { background: rgba(255,255,255,0.02); }
.rank { width:64px; font-weight:700; color:#ffd166; }
.score { width:120px; text-align:right; font-weight:700; color:#9ae6b4; }
.small { font-size:0.8rem; color:#9fb3d8; }
.error { color:#ffb4b4; margin-top:12px; }
.refresh { display:inline-flex; gap:8px; align-items:center; cursor:pointer; padding:6px 10px; border-radius:6px; background: rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.02); color:#dff1ff; }
</style>
</head>
<body>
<div class="container">
<header>
<div>
<h1>CyberPatriot Live Leaderboard</h1>
<div class="meta">Auto-updates every minute • Source: scoreboard.uscyberpatriot.org</div>
</div>
<div>
<button id="manualRefresh" class="refresh">Refresh now</button>
<div id="lastUpdated" class="small" style="margin-top:6px; text-align:right;"></div>
</div>
</header>
<main>
<table id="leaderboard">
<thead>
<tr><th class="rank">#</th><th>Team</th><th>Team Number</th><th class="score">Score</th></tr>
</thead>
<tbody>
<tr><td colspan="4" class="small">Loading…</td></tr>
</tbody>
</table>
<div id="error" class="error" role="alert" style="display:none;"></div>
</main>
</div>
<script>
const API = '/api/teams'; // adjust if needed
const tbody = document.querySelector('#leaderboard tbody');
const lastUpdatedEl = document.getElementById('lastUpdated');
const errorEl = document.getElementById('error');
const manualRefresh = document.getElementById('manualRefresh');
async function load() {
errorEl.style.display = 'none';
try {
const resp = await fetch(API);
if (!resp.ok) throw new Error('Network error: ' + resp.status);
const json = await resp.json();
render(json);
} catch (e) {
errorEl.style.display = 'block';
errorEl.textContent = 'Failed to fetch leaderboard: ' + e.message;
console.error(e);
}
}
function render(data) {
tbody.innerHTML = '';
const teams = data.teams || [];
if (!teams.length) {
tbody.innerHTML = '<tr><td colspan="4" class="small">No teams found.</td></tr>';
} else {
teams.forEach((t, idx) => {
const tr = document.createElement('tr');
const rankTd = document.createElement('td'); rankTd.className='rank'; rankTd.textContent = idx + 1;
const nameTd = document.createElement('td'); nameTd.textContent = t.name || (t.raw && t.raw.join(' | ')) || '-';
const numTd = document.createElement('td'); numTd.textContent = t.teamNumber || '-';
const scoreTd = document.createElement('td'); scoreTd.className='score'; scoreTd.textContent = (t.score !== null && t.score !== undefined) ? t.score : '-';
tr.appendChild(rankTd);
tr.appendChild(nameTd);
tr.appendChild(numTd);
tr.appendChild(scoreTd);
tbody.appendChild(tr);
});
}
lastUpdatedEl.textContent = 'Updated: ' + (data.updated ? new Date(data.updated).toLocaleString() : new Date().toLocaleString()) + ' • Teams: ' + (data.count || teams.length);
}
// initial load
load();
// update every minute (60,000 ms)
const INTERVAL = 60 * 1000;
setInterval(load, INTERVAL);
manualRefresh.addEventListener('click', () => {
load();
});
</script>
</body>
</html>