add website pages

Copies of the pages served from /var/www/html/wordle on the server:
the user search page (index.html), stats.html, games.html and
game.html. Browsers are sent here by nginx while curl gets the raw
board and stats files. These aren't deployed by the workflow yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-09-29 17:50:22 +00:00
1 parent 8e62094a6f
commit 4c6c53ff13
4 files changed
+916

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>