feat: add scoreboard with player names, per-category leaderboards, and themed tables
Add player_name column to games (migration included), with regex-based validation and anti-abuse sanitization. New /scoreboard page shows recent games from localStorage, popular categories with play counts, and top-10 per-category leaderboards sorted by completion time. Also includes two-click reverse word matching, base URL prefix support for reverse-proxy hosting, BasePageHandler refactoring, themed table CSS for all 5 themes, and comprehensive test coverage for player names and scoreboard API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9319f08875
commit
36c3c712fe
@@ -269,6 +269,28 @@ body {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Tables (scoreboard) */
|
||||
.table {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--indigo-mid);
|
||||
}
|
||||
|
||||
.table > :not(caption) > * > * {
|
||||
background-color: var(--indigo-dark);
|
||||
border-bottom-color: var(--indigo-mid);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.table-hover > tbody > tr:hover > * {
|
||||
background-color: var(--indigo-mid);
|
||||
color: var(--heading-color);
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
color: var(--heading-color);
|
||||
border-bottom-color: var(--indigo-mid);
|
||||
}
|
||||
|
||||
/* Form controls */
|
||||
.form-select,
|
||||
.form-control {
|
||||
|
||||
+5
-3
@@ -1,6 +1,8 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var BASE = document.body.dataset.baseUrl || "/";
|
||||
|
||||
let gameState = null;
|
||||
let timerInterval = null;
|
||||
let selectedCells = [];
|
||||
@@ -35,7 +37,7 @@
|
||||
// Load categories
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const resp = await fetch("/api/categories");
|
||||
const resp = await fetch(BASE + "api/categories");
|
||||
if (!resp.ok) throw new Error("Server error");
|
||||
const categories = await resp.json();
|
||||
categorySelect.innerHTML = "";
|
||||
@@ -65,7 +67,7 @@
|
||||
const boardSize = parseInt(boardSizeInput.value);
|
||||
const wordCount = parseInt(wordCountInput.value);
|
||||
const playerName = playerNameInput.value.trim();
|
||||
const resp = await fetch("/api/game/new", {
|
||||
const resp = await fetch(BASE + "api/game/new", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
@@ -253,7 +255,7 @@
|
||||
// Send guess to server
|
||||
async function sendGuess(payload) {
|
||||
try {
|
||||
const resp = await fetch("/api/game/" + gameState.id + "/guess", {
|
||||
const resp = await fetch(BASE + "api/game/" + gameState.id + "/guess", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var BASE = document.body.dataset.baseUrl || "/";
|
||||
|
||||
var loadingDiv = document.getElementById("scoreboard-loading");
|
||||
var recentSection = document.getElementById("recent-games-section");
|
||||
var recentTbody = document.querySelector("#recent-games-table tbody");
|
||||
@@ -65,7 +67,7 @@
|
||||
' <div class="card-body">' +
|
||||
' <h5 class="card-title">' + escapeHtml(titleCase(c.name)) + '</h5>' +
|
||||
' <p class="card-text text-muted">' + c.completed_games + ' games completed</p>' +
|
||||
' <a href="/game" class="btn btn-accent btn-sm">Play</a>' +
|
||||
' <a href="' + BASE + 'game" class="btn btn-accent btn-sm">Play</a>' +
|
||||
' </div>' +
|
||||
'</div>';
|
||||
popularRow.appendChild(col);
|
||||
@@ -115,7 +117,7 @@
|
||||
|
||||
async function loadScoreboard() {
|
||||
try {
|
||||
var resp = await fetch("/api/scoreboard");
|
||||
var resp = await fetch(BASE + "api/scoreboard");
|
||||
if (!resp.ok) throw new Error("Failed to load");
|
||||
var data = await resp.json();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user