diff --git a/api/scoreboard.php b/api/scoreboard.php index 549d822..60f182b 100644 --- a/api/scoreboard.php +++ b/api/scoreboard.php @@ -11,6 +11,15 @@ run_migrations(); try { $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')); + header('Allow: GET, POST, HEAD, OPTIONS'); + + if ($method === 'OPTIONS') { + jsonResponse(['success' => true], 204); + } + + if ($method === 'HEAD') { + listScores((int) ($_GET['limit'] ?? 10), false); + } if ($method === 'GET') { listScores((int) ($_GET['limit'] ?? 10)); @@ -106,7 +115,7 @@ function saveScore(array $payload): void ]); } -function listScores(int $limit = 10): void +function listScores(int $limit = 10, bool $includeBody = true): void { $limit = max(1, min(25, $limit)); @@ -136,6 +145,11 @@ function listScores(int $limit = 10): void ]; } + if (!$includeBody) { + http_response_code(200); + exit; + } + jsonResponse([ 'success' => true, 'scores' => $scores, diff --git a/assets/js/main.js b/assets/js/main.js index 24ee788..4c4a381 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -634,6 +634,59 @@ document.addEventListener('DOMContentLoaded', () => { : 'Just now'; } + + function escapeHtml(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + function renderScoreboard() { + if (!ui.scoreboardList || !ui.scoreboardEmpty) return; + + if (!Array.isArray(scoreboardEntries) || scoreboardEntries.length === 0) { + ui.scoreboardList.innerHTML = ''; + ui.scoreboardEmpty.classList.remove('d-none'); + return; + } + + ui.scoreboardEmpty.classList.add('d-none'); + ui.scoreboardList.innerHTML = scoreboardEntries.map((entry, index) => { + const playerName = escapeHtml(entry.player_name || 'Player'); + const scoreLabel = Number(entry.score || 0).toLocaleString(); + const levelLabel = Number(entry.level || 0).toString(); + const linesLabel = Number(entry.lines || 0).toString(); + const modeLabel = entry.mode === 'multiplayer' ? 'Multiplayer' : 'Solo'; + const durationLabel = formatDuration(Number(entry.duration_seconds || 0) * 1000); + const createdAt = entry.created_at + ? new Date(String(entry.created_at).replace(' ', 'T')).toLocaleString([], { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + : 'Just now'; + const roomCode = entry.room_code ? ` · Room ${escapeHtml(entry.room_code)}` : ''; + + return ` +
+
#${index + 1}
+
+
+ ${playerName} + ${scoreLabel} +
+
Level ${levelLabel} · Lines ${linesLabel} · ${durationLabel}
+
${modeLabel}${roomCode} · ${createdAt}
+
+
+ `; + }).join(''); + } + async function loadScoreboard(options = {}) { if (!ui.scoreboardStatus) return; const preserveOnError = Boolean(options.preserveOnError);