134 lines
4.8 KiB
HTML
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>
|