First commit
This commit is contained in:
168
src/static/app.js
Normal file
168
src/static/app.js
Normal file
@@ -0,0 +1,168 @@
|
||||
/* app.js – Lagomare Gates frontend */
|
||||
|
||||
// ── Token helpers ─────────────────────────────────────────────────────────────
|
||||
const TOKEN_KEY = "lg_keypass_token";
|
||||
|
||||
function saveToken(t) { localStorage.setItem(TOKEN_KEY, t); }
|
||||
function clearToken() { localStorage.removeItem(TOKEN_KEY); }
|
||||
function getToken() { return localStorage.getItem(TOKEN_KEY); }
|
||||
|
||||
function tokenValid(t) {
|
||||
if (!t) return false;
|
||||
try {
|
||||
const payload = JSON.parse(atob(t.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
|
||||
return payload.exp * 1000 > Date.now();
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
// ── API helper ────────────────────────────────────────────────────────────────
|
||||
async function apiFetch(method, path, body) {
|
||||
const token = getToken();
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
const res = await fetch(path, {
|
||||
method,
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
if (res.status === 401) {
|
||||
clearToken();
|
||||
showLogin();
|
||||
throw new Error("Session expired – please log in again.");
|
||||
}
|
||||
if (!res.ok) {
|
||||
const json = await res.json().catch(() => null);
|
||||
throw new Error((json && json.detail) || `Error ${res.status}`);
|
||||
}
|
||||
if (res.status === 204) return null;
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ── Views ─────────────────────────────────────────────────────────────────────
|
||||
function showLogin() {
|
||||
document.getElementById("login-view").classList.remove("hidden");
|
||||
document.getElementById("gates-view").classList.add("hidden");
|
||||
}
|
||||
|
||||
function showGatesView() {
|
||||
document.getElementById("login-view").classList.add("hidden");
|
||||
document.getElementById("gates-view").classList.remove("hidden");
|
||||
}
|
||||
|
||||
// ── Gate rendering ────────────────────────────────────────────────────────────
|
||||
function renderGates(gates) {
|
||||
const grid = document.getElementById("gates-grid");
|
||||
const loading = document.getElementById("loading-gates");
|
||||
grid.innerHTML = "";
|
||||
|
||||
if (!gates.length) {
|
||||
loading.textContent = "No gates configured.";
|
||||
loading.classList.remove("hidden");
|
||||
grid.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
loading.classList.add("hidden");
|
||||
grid.classList.remove("hidden");
|
||||
|
||||
for (const gate of gates) {
|
||||
const icon = gate.gate_type === "car" ? "🚘" : "🚶";
|
||||
const label = gate.gate_type === "car" ? "Car" : "Pedestrian";
|
||||
const btn = document.createElement("button");
|
||||
btn.className = `gate-btn ${gate.gate_type}`;
|
||||
btn.dataset.gateId = gate.id;
|
||||
btn.innerHTML = `<span class="icon">${icon}</span><span>${gate.name}</span>`;
|
||||
btn.addEventListener("click", () => handleOpenGate(btn, gate.id));
|
||||
grid.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGates() {
|
||||
try {
|
||||
const gates = await apiFetch("GET", "/api/gates");
|
||||
renderGates(gates);
|
||||
} catch (e) {
|
||||
document.getElementById("loading-gates").textContent = e.message;
|
||||
document.getElementById("loading-gates").classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Open gate action ──────────────────────────────────────────────────────────
|
||||
async function handleOpenGate(btn, gateId) {
|
||||
btn.disabled = true;
|
||||
btn.classList.add("loading");
|
||||
btn.classList.remove("ok", "fail");
|
||||
|
||||
try {
|
||||
await apiFetch("POST", `/api/gates/${encodeURIComponent(gateId)}/open`);
|
||||
btn.classList.remove("loading");
|
||||
btn.classList.add("ok");
|
||||
showToast("Gate opened ✓", false);
|
||||
setTimeout(() => btn.classList.remove("ok"), 2000);
|
||||
} catch (e) {
|
||||
btn.classList.remove("loading");
|
||||
btn.classList.add("fail");
|
||||
showToast(e.message, true);
|
||||
setTimeout(() => btn.classList.remove("fail"), 2000);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Toast ─────────────────────────────────────────────────────────────────────
|
||||
let _toastTimer;
|
||||
function showToast(msg, isError = false) {
|
||||
const el = document.getElementById("toast");
|
||||
clearTimeout(_toastTimer);
|
||||
el.textContent = msg;
|
||||
el.className = `toast ${isError ? "error" : "success"}`;
|
||||
_toastTimer = setTimeout(() => el.classList.add("fade"), 2600);
|
||||
setTimeout(() => { el.className = "toast hidden"; }, 3000);
|
||||
}
|
||||
|
||||
// ── Login form ────────────────────────────────────────────────────────────────
|
||||
document.getElementById("login-form").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const code = document.getElementById("keypass-input").value.trim();
|
||||
if (!code) return;
|
||||
|
||||
const btn = document.getElementById("login-btn");
|
||||
const errEl = document.getElementById("login-error");
|
||||
btn.disabled = true;
|
||||
errEl.classList.add("hidden");
|
||||
|
||||
try {
|
||||
const data = await apiFetch("POST", "/api/auth/keypass", { code });
|
||||
saveToken(data.token);
|
||||
document.getElementById("keypass-input").value = "";
|
||||
showGatesView();
|
||||
loadGates();
|
||||
} catch (e) {
|
||||
errEl.textContent = e.message;
|
||||
errEl.classList.remove("hidden");
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Logout ────────────────────────────────────────────────────────────────────
|
||||
document.getElementById("logout-btn").addEventListener("click", () => {
|
||||
clearToken();
|
||||
showLogin();
|
||||
});
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
(function init() {
|
||||
const t = getToken();
|
||||
if (tokenValid(t)) {
|
||||
showGatesView();
|
||||
loadGates();
|
||||
} else {
|
||||
clearToken();
|
||||
showLogin();
|
||||
}
|
||||
})();
|
||||
|
||||
// ── Service worker registration ───────────────────────────────────────────────
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
||||
}
|
||||
Reference in New Issue
Block a user