feat: expand to 1014 words, add title-case display and 2 new themes
- 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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
cd1dc9200b
commit
9319f08875
+40
-3
@@ -6,6 +6,10 @@
|
||||
let selectedCells = [];
|
||||
let feedbackTimeout = null;
|
||||
|
||||
function titleCase(word) {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
// DOM refs
|
||||
const setupDiv = document.getElementById("game-setup");
|
||||
const boardDiv = document.getElementById("game-board");
|
||||
@@ -26,6 +30,7 @@
|
||||
const finalTime = document.getElementById("final-time");
|
||||
const playAgainBtn = document.getElementById("play-again-btn");
|
||||
const inputArea = document.getElementById("input-area");
|
||||
const playerNameInput = document.getElementById("player-name");
|
||||
|
||||
// Load categories
|
||||
async function loadCategories() {
|
||||
@@ -59,10 +64,16 @@
|
||||
const categoryId = parseInt(categorySelect.value);
|
||||
const boardSize = parseInt(boardSizeInput.value);
|
||||
const wordCount = parseInt(wordCountInput.value);
|
||||
const playerName = playerNameInput.value.trim();
|
||||
const resp = await fetch("/api/game/new", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ category_id: categoryId, board_size: boardSize, word_count: wordCount }),
|
||||
body: JSON.stringify({
|
||||
category_id: categoryId,
|
||||
board_size: boardSize,
|
||||
word_count: wordCount,
|
||||
player_name: playerName,
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const errData = await resp.json().catch(function () { return {}; });
|
||||
@@ -110,7 +121,7 @@
|
||||
gameState.words.forEach(function (w) {
|
||||
const li = document.createElement("li");
|
||||
li.className = "list-group-item";
|
||||
li.textContent = w.text;
|
||||
li.textContent = titleCase(w.text);
|
||||
li.dataset.word = w.text;
|
||||
if (w.found) {
|
||||
li.classList.add("word-found");
|
||||
@@ -255,12 +266,14 @@
|
||||
if (result.correct) {
|
||||
highlightFoundCells(result.cells);
|
||||
renderWordList();
|
||||
showFeedback("Found: " + result.word + "!", "text-success");
|
||||
showFeedback("Found: " + titleCase(result.word) + "!", "text-success");
|
||||
} else {
|
||||
showFeedback("Not a match.", "text-danger");
|
||||
}
|
||||
|
||||
if (result.status === "completed") {
|
||||
gameState.completed_at = result.completed_at;
|
||||
gameState.player_name = result.player_name;
|
||||
onGameComplete(result.completed_at);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -294,6 +307,30 @@
|
||||
finalTime.textContent = timerEl.textContent;
|
||||
playAgainBtn.classList.remove("d-none");
|
||||
inputArea.classList.add("d-none");
|
||||
|
||||
var startedAt = new Date(gameState.started_at);
|
||||
var endedAt = new Date(completedAt);
|
||||
var seconds = Math.floor((endedAt.getTime() - startedAt.getTime()) / 1000);
|
||||
saveRecentGame({
|
||||
game_id: gameState.id,
|
||||
player_name: gameState.player_name || "Anonymous",
|
||||
category: gameState.category,
|
||||
time_display: timerEl.textContent,
|
||||
time_seconds: seconds,
|
||||
board_size: gameState.board_size,
|
||||
word_count: gameState.words.length,
|
||||
completed_at: completedAt,
|
||||
});
|
||||
}
|
||||
|
||||
function saveRecentGame(entry) {
|
||||
var KEY = "ws-recent-games";
|
||||
try {
|
||||
var games = JSON.parse(localStorage.getItem(KEY) || "[]");
|
||||
games.unshift(entry);
|
||||
if (games.length > 20) games = games.slice(0, 20);
|
||||
localStorage.setItem(KEY, JSON.stringify(games));
|
||||
} catch (e) { /* localStorage unavailable */ }
|
||||
}
|
||||
|
||||
playAgainBtn.addEventListener("click", function () {
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
(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();
|
||||
})();
|
||||
@@ -0,0 +1,34 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var themes = ["indigo", "dark", "light", "ocean", "forest"];
|
||||
var icons = {
|
||||
indigo: "bi-moon-stars",
|
||||
dark: "bi-circle-half",
|
||||
light: "bi-sun",
|
||||
ocean: "bi-water",
|
||||
forest: "bi-tree"
|
||||
};
|
||||
var lightThemes = { light: true, ocean: true };
|
||||
var btn = document.getElementById("theme-toggle");
|
||||
var current = localStorage.getItem("ws-theme") || "indigo";
|
||||
|
||||
function apply(theme) {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
document.documentElement.setAttribute(
|
||||
"data-bs-theme",
|
||||
lightThemes[theme] ? "light" : "dark"
|
||||
);
|
||||
btn.querySelector("i").className = "bi " + icons[theme];
|
||||
btn.title = theme.charAt(0).toUpperCase() + theme.slice(1) + " theme";
|
||||
localStorage.setItem("ws-theme", theme);
|
||||
current = theme;
|
||||
}
|
||||
|
||||
apply(current);
|
||||
|
||||
btn.addEventListener("click", function () {
|
||||
var idx = (themes.indexOf(current) + 1) % themes.length;
|
||||
apply(themes[idx]);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user