- Move word data from hardcoded seed.py to data/words.json - Expand from 210 words across 7 categories to 1014 words across 14 categories (animals, food, colors, sports, space, nature, music, science, countries, occupations, ocean, weather, mythology, technology) - Word list sidebar now shows title case (Tiger) while grid stays uppercase; text input remains case-insensitive - Add Ocean theme (light, blue accents) and Forest theme (dark, amber accents on deep green) - Replace hardcoded rgba values with --coral-bg CSS variable so selection highlighting adapts per theme Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
159 lines
5.8 KiB
JavaScript
159 lines
5.8 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
var loadingDiv = document.getElementById("scoreboard-loading");
|
|
var recentSection = document.getElementById("recent-games-section");
|
|
var recentTbody = document.querySelector("#recent-games-table tbody");
|
|
var popularSection = document.getElementById("popular-section");
|
|
var popularRow = document.getElementById("popular-row");
|
|
var leaderboardsSection = document.getElementById("leaderboards-section");
|
|
var leaderboardsContainer = document.getElementById("leaderboards-container");
|
|
var emptyDiv = document.getElementById("scoreboard-empty");
|
|
|
|
function formatTime(seconds) {
|
|
var mins = Math.floor(seconds / 60);
|
|
var secs = Math.floor(seconds % 60);
|
|
return mins + ":" + (secs < 10 ? "0" : "") + secs;
|
|
}
|
|
|
|
function formatDate(iso) {
|
|
return new Date(iso).toLocaleDateString();
|
|
}
|
|
|
|
function titleCase(str) {
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
var div = document.createElement("div");
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Recent games from localStorage
|
|
function getRecentGames() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem("ws-recent-games") || "[]");
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function renderRecentGames(games) {
|
|
recentTbody.innerHTML = "";
|
|
games.forEach(function (g) {
|
|
var tr = document.createElement("tr");
|
|
tr.innerHTML =
|
|
"<td>" + escapeHtml(titleCase(g.category)) + "</td>" +
|
|
"<td>" + escapeHtml(g.player_name) + "</td>" +
|
|
"<td>" + escapeHtml(g.time_display || formatTime(g.time_seconds)) + "</td>" +
|
|
"<td>" + g.board_size + "x" + g.board_size + "</td>" +
|
|
"<td>" + g.word_count + "</td>" +
|
|
"<td>" + formatDate(g.completed_at) + "</td>";
|
|
recentTbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function renderPopularCategories(categories) {
|
|
popularRow.innerHTML = "";
|
|
var active = categories.filter(function (c) { return c.completed_games > 0; });
|
|
active.forEach(function (c) {
|
|
var col = document.createElement("div");
|
|
col.className = "col-md-4 col-lg-3";
|
|
col.innerHTML =
|
|
'<div class="card text-center">' +
|
|
' <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>' +
|
|
' </div>' +
|
|
'</div>';
|
|
popularRow.appendChild(col);
|
|
});
|
|
}
|
|
|
|
function renderLeaderboards(leaderboards) {
|
|
leaderboardsContainer.innerHTML = "";
|
|
var names = Object.keys(leaderboards);
|
|
names.sort();
|
|
names.forEach(function (catName) {
|
|
var entries = leaderboards[catName];
|
|
var card = document.createElement("div");
|
|
card.className = "card mb-3";
|
|
|
|
var header = '<div class="card-header">' +
|
|
'<i class="bi bi-trophy"></i> ' + escapeHtml(titleCase(catName)) +
|
|
'</div>';
|
|
|
|
var rows = "";
|
|
entries.forEach(function (e, i) {
|
|
rows +=
|
|
"<tr>" +
|
|
"<td>" + (i + 1) + "</td>" +
|
|
"<td>" + escapeHtml(e.player_name) + "</td>" +
|
|
"<td>" + formatTime(e.time_seconds) + "</td>" +
|
|
"<td>" + e.board_size + "x" + e.board_size + "</td>" +
|
|
"<td>" + e.word_count + "</td>" +
|
|
"<td>" + formatDate(e.completed_at) + "</td>" +
|
|
"</tr>";
|
|
});
|
|
|
|
card.innerHTML = header +
|
|
'<div class="card-body p-0">' +
|
|
'<div class="table-responsive">' +
|
|
'<table class="table table-hover mb-0">' +
|
|
'<thead><tr>' +
|
|
'<th>#</th><th>Player</th><th>Time</th>' +
|
|
'<th>Board</th><th>Words</th><th>Date</th>' +
|
|
'</tr></thead>' +
|
|
'<tbody>' + rows + '</tbody>' +
|
|
'</table></div></div>';
|
|
|
|
leaderboardsContainer.appendChild(card);
|
|
});
|
|
}
|
|
|
|
async function loadScoreboard() {
|
|
try {
|
|
var resp = await fetch("/api/scoreboard");
|
|
if (!resp.ok) throw new Error("Failed to load");
|
|
var data = await resp.json();
|
|
|
|
loadingDiv.classList.add("d-none");
|
|
|
|
// Recent games from localStorage
|
|
var recent = getRecentGames();
|
|
if (recent.length > 0) {
|
|
renderRecentGames(recent);
|
|
recentSection.classList.remove("d-none");
|
|
}
|
|
|
|
// Popular categories
|
|
var hasCompleted = data.popular_categories.some(function (c) {
|
|
return c.completed_games > 0;
|
|
});
|
|
if (hasCompleted) {
|
|
renderPopularCategories(data.popular_categories);
|
|
popularSection.classList.remove("d-none");
|
|
}
|
|
|
|
// Leaderboards
|
|
var hasLeaderboards = Object.keys(data.leaderboards).length > 0;
|
|
if (hasLeaderboards) {
|
|
renderLeaderboards(data.leaderboards);
|
|
leaderboardsSection.classList.remove("d-none");
|
|
}
|
|
|
|
// Empty state
|
|
if (!hasCompleted && !hasLeaderboards && recent.length === 0) {
|
|
emptyDiv.classList.remove("d-none");
|
|
}
|
|
} catch (err) {
|
|
loadingDiv.innerHTML =
|
|
'<p class="text-danger">Failed to load scoreboard data.</p>';
|
|
}
|
|
}
|
|
|
|
loadScoreboard();
|
|
})();
|