const { useState, useEffect } = React; const CONFIG = window.PROTO_CONFIG || {}; const BRAND = CONFIG.brand || "Konstruera"; const TAGLINE = CONFIG.tagline || "Beräkningsverktyg för konstruktörer"; const MENU_ITEMS = [ { id: "takstol", label: "Takstolsdimensionering", hint: "EN 1995 · trätakstolar", href: "/verktyg/takstol/" }, { id: "balk", label: "Balkdimensionering", hint: "Trä, stål och limträ", href: "/verktyg/balk/" }, { id: "balk-klimat", label: "Balk – klimatdata", hint: "Boverkets klimatdatabas", href: "/verktyg/balk-klimat/" }, { id: "bjalklag-klimat", label: "Bjälklag – klimatdata", hint: "Klimatpåverkan bjälklag", href: "/verktyg/bjalklag-klimat/" }, { id: "betesplanering", label: "Betesplanering", hint: "Rotation, djur och journal", href: "/verktyg/betesplanering/" }, { id: "grund", label: "Grundberäkning", hint: "Plintar, plattor, murar" }, { id: "virke", label: "Virkesåtgång", hint: "Mängdning och inköpslista" }, { id: "projekt", label: "Projekt", hint: "Mina ritningar och arkiv" }, ]; function Logo({ size = 24 }) { return ( ); } const URL_PARAMS = new URLSearchParams(window.location.search); const INITIAL_VIEW = URL_PARAMS.get("view") === "home" || URL_PARAMS.get("menu") ? "home" : "login"; const INITIAL_MENU = URL_PARAMS.get("menu") === "1"; function App() { const [view, setView] = useState(INITIAL_VIEW); const [menuOpen, setMenuOpen] = useState(INITIAL_MENU); const [active, setActive] = useState(null); const [username, setUsername] = useState(""); const [pw, setPw] = useState(""); const [remember, setRemember] = useState(true); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); // apps: null = unknown (preview/demo); ["*"] = super-user, sees all; // ["takstol", "balk-klimat"] etc. = explicit allowlist. const [apps, setApps] = useState(null); useEffect(() => { const onKey = (e) => { if (e.key === "Escape") { setMenuOpen(false); setActive(null); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); // On load, ask the server if we're already logged in. If the endpoint doesn't // exist (offline preview), silently stay in demo mode. useEffect(() => { fetch("/api/me", { credentials: "same-origin" }) .then((r) => (r.ok ? r.json() : null)) .then((data) => { if (data && data.user) { setView("home"); setApps(Array.isArray(data.apps) ? data.apps : ["*"]); } }) .catch(() => {}); }, []); const handleLogin = async () => { setError(""); setBusy(true); try { const r = await fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "same-origin", body: JSON.stringify({ username, password: pw, remember }), }); if (r.ok) { const data = await r.json().catch(() => ({})); setApps(Array.isArray(data.apps) ? data.apps : ["*"]); setView("home"); return; } if (r.status === 401) { setError("Fel användarnamn eller lösenord."); return; } // Endpoint missing (offline preview) — fall through to demo behaviour. setView("home"); } catch (e) { // Network error / no backend — demo mode setView("home"); } finally { setBusy(false); } }; const handleLogout = async () => { try { await fetch("/api/logout", { method: "POST", credentials: "same-origin" }); } catch (e) {} setView("login"); setMenuOpen(false); setActive(null); }; return (
{view === "login" ? ( ) : ( )}
); } function Login({ username, pw, remember, setUsername, setPw, setRemember, error, busy, onSubmit }) { const submit = (e) => { e.preventDefault(); onSubmit(); }; return (
{BRAND}
Inloggning

Välkommen.

{TAGLINE}

{error &&
{error}
}
© {BRAND} 2026 · v0.4 · Sekretess · Villkor
); } function Home({ menuOpen, setMenuOpen, active, setActive, apps, onLogout }) { // apps === null → preview/demo (servern svarade inte) — visa allt // apps === ["*"] → super-user — visa allt // apps === [...] → explicit lista — visa bara matchande id:n const visibleItems = (apps == null || apps.includes("*")) ? MENU_ITEMS : MENU_ITEMS.filter((it) => apps.includes(it.id)); return (
{BRAND}
Tisdag 19 maj · 14:02

Var god välj verktyg.

setMenuOpen(false)} /> {active && (
{active.label}
{active.soon ? "\u00d6ppnar inte \u00e4n \u2014 verktyget kommer snart." : active.hint}
)}
); } ReactDOM.createRoot(document.getElementById("root")).render();