165 lines
5.6 KiB
HTML
165 lines
5.6 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>Pedido de acesso</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="req-page">
|
|
<div class="req-top">
|
|
<div class="inner">
|
|
<p class="eyebrow" id="brand"></p>
|
|
<h1 id="title"></h1>
|
|
<p id="intro"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="req-wrap" id="wrap">
|
|
<div class="req-msg bad" id="msg" role="alert" aria-live="polite"></div>
|
|
|
|
<div class="req-field">
|
|
<span class="lab" id="l_name"></span>
|
|
<input type="text" id="f_name" autocomplete="name" maxlength="80">
|
|
</div>
|
|
|
|
<div class="req-field">
|
|
<span class="lab" id="l_dept"></span>
|
|
<input type="text" id="f_dept" autocomplete="organization" maxlength="80">
|
|
</div>
|
|
|
|
<div class="req-field">
|
|
<span class="lab" id="l_contact"></span>
|
|
<input type="text" id="f_contact" autocomplete="tel" maxlength="80">
|
|
</div>
|
|
|
|
<div class="req-field">
|
|
<span class="lab" id="l_areas"></span>
|
|
<div class="areas" id="areas"></div>
|
|
</div>
|
|
|
|
<div class="req-field">
|
|
<span class="lab" id="l_detail"></span>
|
|
<textarea id="f_detail" maxlength="600"></textarea>
|
|
</div>
|
|
|
|
<div class="req-field" id="pinbox" style="display:none">
|
|
<span class="lab" id="l_pin"></span>
|
|
<input type="text" id="f_pin" inputmode="numeric" autocomplete="off" maxlength="24">
|
|
<p class="req-hint" id="h_pin"></p>
|
|
</div>
|
|
|
|
<!-- Bots fill every field they can find; a person never sees this one. -->
|
|
<div class="hp" aria-hidden="true">
|
|
<label>Website<input type="text" id="f_website" tabindex="-1" autocomplete="off"></label>
|
|
</div>
|
|
|
|
<button class="btn btn-p" id="send" style="width:100%"></button>
|
|
<p class="req-foot" id="foot"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/i18n.js"></script>
|
|
<script>
|
|
setLang("__LANG__".replace(/^__.*__$/, "pt"));
|
|
|
|
const $ = (id) => document.getElementById(id);
|
|
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
|
|
$("brand").textContent = T.brand;
|
|
$("title").textContent = T.reqTitle;
|
|
$("intro").textContent = T.reqIntro;
|
|
$("l_name").textContent = T.reqName;
|
|
$("l_dept").textContent = T.reqDept;
|
|
$("l_contact").textContent = T.reqContact;
|
|
$("l_areas").textContent = T.reqAreas;
|
|
$("l_detail").textContent = T.reqDetail;
|
|
$("f_detail").placeholder = T.reqDetailPh;
|
|
$("l_pin").textContent = T.reqPin;
|
|
$("h_pin").textContent = T.reqPinHelp;
|
|
$("send").textContent = T.reqSend;
|
|
$("foot").textContent = T.reqFoot;
|
|
document.title = T.reqTitle;
|
|
|
|
/* Areas, not doors. The endpoint returns building names only — this page is
|
|
the one an outsider might reach, and a door list is a map of the building. */
|
|
/* kiosk=1 means the keyholder handed their own phone over: no PIN prompt,
|
|
and the confirmation offers a fresh blank form rather than a dead end. */
|
|
const KIOSK = new URLSearchParams(location.search).get("kiosk") === "1";
|
|
|
|
(async () => {
|
|
try {
|
|
const r = await fetch("/api/request/areas").then((x) => x.json());
|
|
$("areas").innerHTML = (r.areas || []).map((a, n) =>
|
|
`<label class="area"><input type="checkbox" value="${esc(a)}" id="a${n}"><span>${esc(a)}</span></label>`
|
|
).join("");
|
|
if (r.pin) $("pinbox").style.display = "";
|
|
else if (KIOSK) $("foot").textContent = T.reqNoPinStaff;
|
|
} catch (e) { /* leave the section empty; the free-text field still works */ }
|
|
})();
|
|
|
|
function fail(m) {
|
|
const el = $("msg");
|
|
el.textContent = m;
|
|
el.classList.add("on");
|
|
if (el.scrollIntoView) el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
}
|
|
|
|
let busy = false;
|
|
$("send").addEventListener("click", async () => {
|
|
if (busy) return;
|
|
$("msg").classList.remove("on");
|
|
|
|
const name = $("f_name").value.trim();
|
|
if (!name) return fail(T.reqNeedName);
|
|
const areas = [...document.querySelectorAll("#areas input:checked")].map((i) => i.value);
|
|
if (!areas.length && !$("f_detail").value.trim()) return fail(T.reqNeedArea);
|
|
|
|
busy = true;
|
|
$("send").textContent = T.reqSending;
|
|
$("send").disabled = true;
|
|
try {
|
|
const res = await fetch("/api/request", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
name,
|
|
dept: $("f_dept").value.trim(),
|
|
contact: $("f_contact").value.trim(),
|
|
areas,
|
|
detail: $("f_detail").value.trim(),
|
|
pin: $("f_pin").value.trim(),
|
|
website: $("f_website").value,
|
|
}),
|
|
});
|
|
if (res.ok) {
|
|
$("wrap").innerHTML = `<div class="req-done">
|
|
<div class="tickmark">✓</div>
|
|
<h2>${esc(T.reqOk)}</h2>
|
|
<p>${esc(KIOSK ? T.reqKioskDone : T.reqOkMore)}</p>
|
|
<button class="btn btn-g" onclick="location.reload()">${
|
|
esc(KIOSK ? T.reqKioskNext : T.reqAnother)}</button>
|
|
</div>`;
|
|
return;
|
|
}
|
|
const b = await res.json().catch(() => ({}));
|
|
fail(res.status === 429 ? T.reqTooMany
|
|
: res.status === 401 ? T.reqWrongPin
|
|
: (T.errors && T.errors[b.error]) || b.error || T.noServer);
|
|
} catch (e) {
|
|
fail(T.noServer);
|
|
}
|
|
busy = false;
|
|
$("send").textContent = T.reqSend;
|
|
$("send").disabled = false;
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|