lxc-keyreg/public/login.html
2026-08-24 15:21:24 +01:00

134 lines
4.8 KiB
HTML

<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#131E1B">
<meta name="robots" content="noindex, nofollow">
<title>Registo de Chaves</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans+Condensed:wght@700&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div class="login">
<!-- the key board, as wallpaper -->
<svg id="pegfield" aria-hidden="true"></svg>
<div class="login-card">
<div class="login-mark" aria-hidden="true">
<svg viewBox="0 0 24 24">
<circle cx="8" cy="8" r="4.2"/>
<path d="M11 11 L19 19"/>
<path d="M16.4 16.4 L18.6 14.2"/>
<path d="M18.6 18.6 L20.6 16.6"/>
</svg>
</div>
<p class="eyebrow" id="brand"></p>
<h1 id="title"></h1>
<p class="login-sub" id="sub"></p>
<div class="pw-wrap">
<input class="pw" id="code" type="password" inputmode="text"
autocomplete="current-password" autocapitalize="off"
autocorrect="off" spellcheck="false" autofocus>
<button class="pw-toggle" id="toggle" type="button" aria-pressed="false">👁</button>
</div>
<button class="btn btn-p" id="go"></button>
<p class="login-err" id="err" role="alert" aria-live="polite"></p>
</div>
<p class="login-foot" id="foot"></p>
</div>
<script src="/i18n.js"></script>
<script>
/* The server substitutes the configured language before sending this page. */
setLang("__LANG__".replace(/^__.*__$/, "pt"));
document.getElementById("brand").textContent = T.brand;
document.getElementById("title").textContent = T.brand;
document.getElementById("sub").textContent = T.loginSub;
document.getElementById("go").textContent = T.loginBtn;
document.getElementById("foot").textContent = T.loginFoot;
document.title = T.brand;
const $code = document.getElementById("code");
const $err = document.getElementById("err");
const $card = document.querySelector(".login-card");
const $toggle = document.getElementById("toggle");
$code.placeholder = T.loginPlaceholder;
$toggle.title = T.loginShow;
$toggle.setAttribute("aria-label", T.loginShow);
/* Decorative peg board. Drawn rather than an image so it scales to any
screen and stays a few hundred bytes. */
(function pegfield() {
const svg = document.getElementById("pegfield");
const W = 1200, H = 900, step = 66, r = 15;
svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
svg.setAttribute("preserveAspectRatio", "xMidYMid slice");
let out = "";
for (let y = step / 2; y < H; y += step) {
for (let x = step / 2; x < W; x += step) {
/* deterministic pseudo-random, so the pattern is stable across reloads */
const n = Math.abs(Math.sin(x * 12.9898 + y * 78.233) * 43758.5453) % 1;
if (n < 0.10) continue; // empty hook
if (n < 0.26) { // issued
out += `<circle cx="${x}" cy="${y}" r="${r}" fill="none" stroke="#A87F3C" stroke-width="2"/>`;
} else if (n < 0.34) { // alarm tag
out += `<rect x="${x - r}" y="${y - r}" width="${r * 2}" height="${r * 2}" rx="4" fill="#5F55A0"/>`;
} else { // on its hook
out += `<circle cx="${x}" cy="${y}" r="${r}" fill="#2E6E60"/>`;
}
}
}
svg.innerHTML = out;
})();
let busy = false;
function fail(msg) {
$err.textContent = msg;
$err.classList.add("on");
$card.classList.remove("shake");
void $card.offsetWidth; // restart the animation
$card.classList.add("shake");
$code.value = "";
$code.focus();
}
async function submit() {
if (busy || !$code.value) return;
busy = true;
$err.classList.remove("on");
try {
const r = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: $code.value }),
});
if (r.ok) { location.href = "/"; return; }
fail(r.status === 429 ? T.loginMany : T.loginWrong);
} catch (e) {
fail(T.noServer);
}
busy = false;
}
document.getElementById("go").addEventListener("click", submit);
$code.addEventListener("keydown", (e) => { if (e.key === "Enter") submit(); });
$toggle.addEventListener("click", () => {
const shown = $code.type === "text";
$code.type = shown ? "password" : "text";
$toggle.setAttribute("aria-pressed", String(!shown));
$toggle.title = shown ? T.loginShow : T.loginHide;
$toggle.setAttribute("aria-label", $toggle.title);
$code.focus();
});
</script>
</body>
</html>