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:
1 parent
8e62094a6f
commit
4c6c53ff13
4 files changed
+916
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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=<name></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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user