The game saves usernames in lowercase, but the search box and the user/board URL parameters were used as typed, so searching "Adam" reported that the user had never played. Lowercase them before use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
408 lines
11 KiB
HTML
408 lines
11 KiB
HTML
<!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")?.toLowerCase(); // game saves names in lowercase
|
|
|
|
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) {
|
|
// Epoch days start at midnight UTC, so format in UTC or viewers
|
|
// west of UTC see the previous day
|
|
dateEl.textContent = new Date(finalEpoch * 86400 * 1000)
|
|
.toLocaleDateString(undefined, {
|
|
timeZone: "UTC",
|
|
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>
|
|
|