clause fixes #16

Merged
adam merged 20 commits from dev into main 2026-09-29 22:13:57 +00:00
4 changed files with 916 additions and 0 deletions
Showing only changes of commit 4c6c53ff13 - Show all commits

No files matched your search

+404
View File
@@ -0,0 +1,404 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wordle Board Viewer</title>
<style>
body {
background: #111;
color: #eee;
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
margin: 0;
min-height: 100vh;
}
h2 {
font-size: 26px;
margin-bottom: 6px;
color: white;
text-align: center;
}
.date-label {
margin-bottom: 16px;
font-size: 15px;
color: #ccc;
}
.nav {
margin-bottom: 18px;
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
justify-content: center;
}
.nav a {
text-decoration: none;
padding: 0.35rem 0.9rem;
border-radius: 999px;
border: 1px solid #2f5740;
background: #162218;
color: #9af7b8;
font-size: 0.85rem;
transition: background 0.2s, transform 0.1s, box-shadow 0.2s;
}
.nav a:hover {
background: #1c3421;
transform: translateY(-1px);
box-shadow: 0 0 6px rgba(81,255,122,0.4);
}
/* --- grid --- */
.grid {
display: grid;
grid-template-columns: repeat(5, 50px);
grid-gap: 6px;
margin-bottom: 20px;
}
.tile {
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
font-weight: bold;
color: white;
border-radius: 6px;
box-shadow: 0 0 4px rgba(0,0,0,0.5);
transition: color 0.3s ease;
}
.empty {
background: #333 !important;
color: #666;
}
.hidden {
color: transparent !important;
}
/* Reveal button */
#reveal {
padding: 10px 18px;
font-size: 16px;
border: none;
border-radius: 6px;
background: #4CAF50;
color: white;
cursor: pointer;
margin-bottom: 25px;
transition: transform 0.1s ease, box-shadow 0.1s ease;
}
#reveal:hover {
background: #45A047;
}
#reveal.pressed {
transform: translateY(2px);
box-shadow: inset 0 0 10px rgba(0,0,0,0.4);
}
/* Raw ANSI box (hidden) */
pre {
background: #222;
color: #ccc;
padding: 10px;
border-radius: 6px;
width: 90%;
max-width: 600px;
overflow-x: auto;
display: none;
}
/* --- Error box (nice green glow) --- */
.error-box {
background: #141414;
border: 1px solid #2f5740;
padding: 1.5rem;
margin-top: 25px;
border-radius: 10px;
box-shadow: 0 0 20px #000 inset, 0 0 15px rgba(81,255,122,0.12);
max-width: 600px;
text-align: center;
font-size: 1.1rem;
color: #ccc;
}
</style>
<link rel="icon" type="image/png" href="https://www.adbraun.lol/wordle/favicon.ico"/>
</head>
<body>
<h2 id="title">Wordle Board</h2>
<div id="date" class="date-label"></div>
<div class="nav">
<a href="/wordle" id="search-link">🔍 Search users</a>
<a href="#" id="stats-link">📊 Back to stats</a>
<a href="#" id="games-link">📅 Recent games</a>
</div>
<div id="grid-container"></div>
<button id="reveal">Reveal Letters</button>
<div id="error" class="error-box" style="display:none;"></div>
<pre id="raw-output"></pre>
<script>
/* Convert xterm colors */
function xtermToRgb(n) {
// Custom overrides for Wordle colors
if (n === 10) return "rgb(44, 192, 44)"; // green
if (n === 3) return "rgb(176, 176, 33)"; // yellow
// Default behavior for all other xterm colors
if (n < 16) {
const base = [
'#000000','#800000','#008000','#808000',
'#000080','#800080','#008080','#c0c0c0',
'#808080','#ff0000','#00ff00','#ffff00',
'#0000ff','#ff00ff','#00ffff','#ffffff'
];
return base[n] || '#000';
}
if (n <= 231) {
n -= 16;
const r = Math.floor(n / 36) % 6;
const g = Math.floor(n / 6) % 6;
const b = n % 6;
const f = x => (x === 0 ? 0 : x * 40 + 55);
return `rgb(${f(r)}, ${f(g)}, ${f(b)})`;
}
const c = (n - 232) * 10 + 8;
return `rgb(${c}, ${c}, ${c})`;
}
/* Parse ANSI board text into rows of {char, bg} */
function parseAnsiBoard(text) {
const lines = text.split("\n").filter(l => l.includes("│"));
const board = [];
for (const line of lines) {
const inside = line.replace(/.*│(.*)│.*/, "$1").trim();
let row = [];
let bg = "#000";
let i = 0;
while (i < inside.length) {
if (inside[i] === "\x1b") {
const end = inside.indexOf("m", i);
const seq = inside.substring(i+2, end).split(";").map(Number);
if (seq[0] === 48 && seq[1] === 5) {
bg = xtermToRgb(seq[2]);
} else if (seq[0] === 0) {
bg = "#000";
}
i = end + 1;
continue;
}
if (/[A-Z]/.test(inside[i])) {
row.push({ char: inside[i], bg });
}
i++;
}
if (row.length === 5) board.push(row);
}
return board;
}
/* Fetch an epoch-day-based board (user-<day>.board) */
async function fetchBoard(user, epochDay) {
const boardName = `${user}-${epochDay}.board`;
const url = `/wordle/rawboard/${boardName}`;
const resp = await fetch(url);
if (!resp.ok) return null;
const text = await resp.text();
return { boardName, text };
}
async function loadBoard() {
const params = new URLSearchParams(location.search);
const boardName = params.get("board");
const errorBox = document.getElementById("error");
const dateEl = document.getElementById("date");
const titleEl = document.getElementById("title");
const gridContainer = document.getElementById("grid-container");
const rawOutput = document.getElementById("raw-output");
const revealBtn = document.getElementById("reveal");
// Hide button until we know we have a board
revealBtn.style.display = "none";
if (!boardName) {
errorBox.style.display = "block";
errorBox.textContent = "No board specified.";
return;
}
const user = boardName.replace(/-.*$/, "").replace(".board", "");
titleEl.textContent = `Wordle Board for ${user}`;
// Wire nav
document.getElementById("stats-link").href = `/stats.html?user=${encodeURIComponent(user)}`;
document.getElementById("games-link").href = `/games.html?user=${encodeURIComponent(user)}`;
let finalText = "";
let finalEpoch = null;
const idMatch = boardName.match(/-(\d+)\.board$/);
// Case 1: boardName has ID → user-<epochDay>.board
if (idMatch) {
const epochDay = Number(idMatch[1]);
const r = await fetchBoard(user, epochDay);
// If not found OR text-only (no ANSI), this day has no board
if (!r || !r.text.includes("\x1b[")) {
errorBox.style.display = "block";
errorBox.textContent = "This board does not exist for that day.";
return;
}
finalText = r.text;
finalEpoch = epochDay;
}
// Case 2: boardName has NO ID → "user.board" semantics
else {
const today = Math.floor(Date.now() / 1000 / 86400);
// First: check today's epoch board (user-<today>.board)
const rToday = await fetchBoard(user, today);
if (rToday && rToday.text.includes("\x1b[")) {
finalText = rToday.text;
finalEpoch = today;
} else {
// Fallback: last board played via /wordle/rawboard/user.board
const latestUrl = `/wordle/rawboard/${encodeURIComponent(user)}.board`;
const latestResp = await fetch(latestUrl);
if (!latestResp.ok) {
errorBox.style.display = "block";
errorBox.textContent = "This user has no recorded boards.";
return;
}
const latestText = await latestResp.text();
// If that "latest" file is only text (no ANSI), treat as no boards
if (!latestText.includes("\x1b[")) {
errorBox.style.display = "block";
errorBox.textContent = "This user has no recorded boards.";
return;
}
finalText = latestText;
finalEpoch = null;
dateEl.textContent = "Last board played";
}
}
// If we know the exact epoch day and haven't set date text yet, show it
if (finalEpoch !== null && !dateEl.textContent) {
dateEl.textContent = new Date(finalEpoch * 86400 * 1000)
.toLocaleDateString(undefined, {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric"
});
}
rawOutput.textContent = finalText;
let parsed = parseAnsiBoard(finalText);
// If somehow there is no parsed grid (safety), treat as "no boards"
if (!parsed.length) {
errorBox.style.display = "block";
errorBox.textContent = "This user has no recorded boards.";
gridContainer.innerHTML = "";
return;
}
// Always fill to 6 rows with empties for a full Wordle grid
while (parsed.length < 6) {
parsed.push([
{ char: " ", bg: "#333", empty: true },
{ char: " ", bg: "#333", empty: true },
{ char: " ", bg: "#333", empty: true },
{ char: " ", bg: "#333", empty: true },
{ char: " ", bg: "#333", empty: true }
]);
}
const grid = document.createElement("div");
grid.className = "grid";
parsed.forEach(row => {
row.forEach(cell => {
const div = document.createElement("div");
div.className = "tile hidden";
div.textContent = cell.char.trim() === "" ? "" : cell.char;
if (cell.empty) {
div.classList.add("empty");
} else {
div.style.backgroundColor = cell.bg;
}
grid.appendChild(div);
});
});
gridContainer.innerHTML = "";
gridContainer.appendChild(grid);
// Now that we have a real board, show reveal button and wire press-to-reveal behavior
revealBtn.style.display = "inline-block";
const tiles = document.querySelectorAll(".tile");
function showLetters() {
tiles.forEach(t => t.classList.remove("hidden"));
revealBtn.classList.add("pressed");
}
function hideLetters() {
tiles.forEach(t => t.classList.add("hidden"));
revealBtn.classList.remove("pressed");
}
// Mouse support
revealBtn.addEventListener("mousedown", showLetters);
document.addEventListener("mouseup", hideLetters);
// Touch support
revealBtn.addEventListener("touchstart", (e) => {
e.preventDefault();
showLetters();
}, { passive: false });
document.addEventListener("touchend", hideLetters);
}
loadBoard();
</script>
</body>
</html>
+215
View File
@@ -0,0 +1,215 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Recent Wordle Games</title>
<style>
body {
background: #0d0d0d;
color: #f2f2f2;
font-family: Arial, sans-serif;
padding: 2rem;
display: flex;
justify-content: center;
min-height: 100vh;
margin: 0;
}
.container {
max-width: 700px;
width: 100%;
text-align: center;
animation: fadein 0.7s ease-out;
}
@keyframes fadein {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #51ff7a;
text-shadow: 0 0 8px rgba(81,255,122,0.7);
}
.subtitle {
margin-bottom: 1.5rem;
opacity: 0.9;
color: #b0ffca;
}
.nav {
margin-bottom: 1.5rem;
display: flex;
justify-content: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.nav a {
text-decoration: none;
padding: 0.4rem 0.9rem;
border-radius: 999px;
border: 1px solid #2f5740;
background: #162218;
color: #9af7b8;
font-size: 0.85rem;
transition: background 0.2s, transform 0.1s, box-shadow 0.2s;
}
.nav a:hover {
background: #1c3421;
transform: translateY(-1px);
box-shadow: 0 0 6px rgba(81,255,122,0.4);
}
.games-card {
background: #141414;
border: 1px solid #2f5740;
padding: 1.5rem;
border-radius: 10px;
box-shadow: 0 0 20px #000 inset, 0 0 15px rgba(81,255,122,0.12);
text-align: left;
margin: 0 auto;
}
ul.games {
list-style: none;
padding: 0;
margin: 0;
}
ul.games li {
padding: 0.5rem 0;
border-bottom: 1px solid #252525;
display: flex;
justify-content: space-between;
align-items: center;
}
ul.games li:last-child {
border-bottom: none;
}
.game-date {
color: #e0ffe8;
font-family: monospace;
}
.game-link {
text-decoration: none;
color: #51ff7a;
font-size: 0.9rem;
}
.game-link:hover {
text-decoration: underline;
}
.empty-msg {
color: #aaa;
font-style: italic;
}
</style>
<link rel="icon" type="image/png" href="https://www.adbraun.lol/wordle/favicon.ico"/>
</head>
<body>
<div class="container">
<h1>Recent Games for <span id="username">...</span></h1>
<div class="subtitle">Last 30 days (only days actually played)</div>
<div class="nav">
<a href="/wordle" id="search-link">🔍 Search users</a>
<a href="#" id="stats-link">📊 Back to stats</a>
</div>
<div class="games-card">
<ul class="games" id="games-list"></ul>
<div id="empty" class="empty-msg" style="display:none;">No games in the last 30 days.</div>
</div>
</div>
<script>
function formatDateFromEpochDays(epochDays) {
const date = new Date(epochDays * 86400 * 1000);
return date.toLocaleDateString(undefined, {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric"
});
}
(async () => {
const params = new URLSearchParams(location.search);
const user = params.get("user");
const usernameSpan = document.getElementById("username");
const gamesList = document.getElementById("games-list");
const emptyMsg = document.getElementById("empty");
const statsLink = document.getElementById("stats-link");
if (!user) {
usernameSpan.textContent = "Unknown";
emptyMsg.style.display = "block";
emptyMsg.textContent = "Error: missing ?user=username in URL.";
statsLink.removeAttribute("href");
return;
}
usernameSpan.textContent = user;
statsLink.href = `/stats.html?user=${encodeURIComponent(user)}`;
const todayEpochDays = Math.floor(Date.now() / 1000 / 86400);
const games = [];
// Look back over last 30 days
for (let d = todayEpochDays; d > todayEpochDays - 30; d--) {
const boardName = `${user}-${d}.board`;
const url = `/wordle/rawboard/${encodeURIComponent(boardName)}`;
try {
const resp = await fetch(url);
if (!resp.ok) continue;
const text = await resp.text();
// A "real" game day has ANSI escape codes
if (text.includes("\x1b[")) {
games.push({ epochDays: d, boardName });
}
} catch (e) {
// ignore errors for individual days
}
}
if (!games.length) {
emptyMsg.style.display = "block";
return;
}
gamesList.innerHTML = "";
games.forEach(game => {
const li = document.createElement("li");
const dateSpan = document.createElement("span");
dateSpan.className = "game-date";
dateSpan.textContent = formatDateFromEpochDays(game.epochDays);
const link = document.createElement("a");
link.className = "game-link";
link.href = `/game.html?board=${encodeURIComponent(game.boardName)}`;
link.textContent = "View board";
li.appendChild(dateSpan);
li.appendChild(link);
gamesList.appendChild(li);
});
})();
</script>
</body>
</html>
+120
View File
@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wordle Viewer</title>
<style>
body {
background: #0b0b0b;
color: #f2f2f2;
font-family: Arial, sans-serif;
min-height: 100vh;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
.container {
background: #141414;
border-radius: 12px;
padding: 2rem 2.5rem;
box-shadow: 0 0 30px rgba(0,0,0,0.7);
max-width: 420px;
width: 100%;
text-align: center;
}
h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-size: 2rem;
color: #51ff7a;
text-shadow: 0 0 10px rgba(81,255,122,0.6);
}
p {
margin-top: 0;
margin-bottom: 2rem;
color: #aaa;
font-size: 0.95rem;
}
.form-row {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
input[type="text"] {
flex: 1;
padding: 0.6rem 0.7rem;
border-radius: 6px;
border: 1px solid #333;
background: #1c1c1c;
color: #f2f2f2;
font-size: 1rem;
}
input[type="text"]:focus {
outline: none;
border-color: #51ff7a;
box-shadow: 0 0 6px rgba(81,255,122,0.6);
}
button {
padding: 0.6rem 1rem;
border-radius: 6px;
border: none;
background: #51ff7a;
color: #061006;
font-weight: bold;
cursor: pointer;
font-size: 1rem;
}
button:hover {
background: #3ee067;
}
.hint {
margin-top: 1rem;
font-size: 0.85rem;
color: #777;
}
</style>
<link rel="icon" type="image/png" href="https://www.adbraun.lol/wordle/favicon.ico"/>
</head>
<body>
<div class="container">
<h1>Wordle Viewer</h1>
<p>Enter a username to view their stats and games.</p>
<form id="search-form">
<div class="form-row">
<input id="user-input" type="text" placeholder="Username (e.g. adam)" autocomplete="off" />
<button type="submit">View stats</button>
</div>
</form>
<div class="hint">
You’ll be taken to <code>stats.html?user=&lt;name&gt;</code>.
</div>
</div>
<script>
const form = document.getElementById("search-form");
const input = document.getElementById("user-input");
form.addEventListener("submit", (e) => {
e.preventDefault();
const user = input.value.trim();
if (!user) return;
window.location.href = `/stats.html?user=${encodeURIComponent(user)}`;
});
</script>
</body>
</html>
+177
View File
@@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wordle Stats</title>
<style>
body {
background: #0d0d0d;
color: #f2f2f2;
font-family: monospace;
padding: 2rem;
display: flex;
justify-content: center;
min-height: 100vh;
margin: 0;
}
.container {
max-width: 700px;
width: 100%;
text-align: center;
animation: fadein 0.7s ease-out;
}
@keyframes fadein {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #51ff7a;
text-shadow: 0 0 8px rgba(81,255,122,0.7);
}
.subtitle {
margin-bottom: 1.5rem;
opacity: 0.9;
color: #b0ffca;
}
.nav {
margin-bottom: 1.5rem;
display: flex;
justify-content: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.nav a {
text-decoration: none;
padding: 0.4rem 0.9rem;
border-radius: 999px;
border: 1px solid #2f5740;
background: #162218;
color: #9af7b8;
font-size: 0.85rem;
transition: background 0.2s, transform 0.1s, box-shadow 0.2s;
}
.nav a:hover {
background: #1c3421;
transform: translateY(-1px);
box-shadow: 0 0 6px rgba(81,255,122,0.4);
}
.stats-card {
background: #141414;
border: 1px solid #2f5740;
padding: 1.5rem;
border-radius: 10px;
box-shadow: 0 0 20px #000 inset, 0 0 15px rgba(81,255,122,0.12);
text-align: left;
margin: 0 auto;
white-space: pre-wrap;
}
pre {
font-family: inherit;
margin-top: 1rem;
white-space: pre-wrap;
}
/* ANSI color classes */
.ansi-black { color: #000; }
.ansi-red { color: #ff6b6b; }
.ansi-green { color: #51ff7a; }
.ansi-yellow { color: #ffe66d; }
.ansi-blue { color: #4dc0ff; }
.ansi-magenta { color: #ff70ff; }
.ansi-cyan { color: #63e6ff; }
.ansi-white { color: #ddd; }
</style>
<link rel="icon" type="image/png" href="https://www.adbraun.lol/wordle/favicon.ico"/>
</head>
<body>
<div class="container">
<h1>Wordle Stats for <span id="username">...</span></h1>
<div class="subtitle">Daily performance summary</div>
<div class="nav">
<a href="/wordle" id="search-link">🔍 Search users</a>
<a href="#" id="games-link">📅 Recent games</a>
<a href="#" id="latest-board-link">🧩 Latest board</a>
</div>
<div class="stats-card">
<pre id="output">Loading…</pre>
</div>
</div>
<script>
(async () => {
const params = new URLSearchParams(location.search);
const user = params.get("user");
const output = document.getElementById("output");
const usernameSpan = document.getElementById("username");
const gamesLink = document.getElementById("games-link");
const latestBoardLink = document.getElementById("latest-board-link");
if (!user) {
usernameSpan.textContent = "Unknown";
output.textContent = "Error: missing ?user=username in URL.";
gamesLink.removeAttribute("href");
latestBoardLink.removeAttribute("href");
return;
}
usernameSpan.textContent = user;
// Wire nav links for this user
gamesLink.href = `/games.html?user=${encodeURIComponent(user)}`;
latestBoardLink.href = `/game.html?board=${encodeURIComponent(user)}.board`;
try {
const resp = await fetch(`/wordle/rawstats/${encodeURIComponent(user)}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
let text = await resp.text();
const ansiMap = {
'30': 'ansi-black',
'31': 'ansi-red',
'32': 'ansi-green',
'33': 'ansi-yellow',
'34': 'ansi-blue',
'35': 'ansi-magenta',
'36': 'ansi-cyan',
'37': 'ansi-white'
};
text = text.replace(/\x1b\[(\d+)m/g, (match, code) => {
if (code === "0") return "</span>";
const cls = ansiMap[code];
return cls ? `<span class="${cls}">` : "";
});
if ((text.match(/<span/g) || []).length > (text.match(/<\/span>/g) || []).length) {
text += "</span>";
}
output.innerHTML = text;
} catch (err) {
output.textContent = `Error loading stats for ${user}: ${err.message}`;
}
})();
</script>
</body>
</html>