first commit
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
import { AppProvider, useApp } from "./context/AppContext.jsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import LoginPage from "./components/LoginPage.jsx";
|
||||
import NavBar from "./components/NavBar.jsx";
|
||||
import GebruikersBeheer from "./components/GebruikersBeheer.jsx";
|
||||
import DashboardTab from "./pages/DashboardTab.jsx";
|
||||
import EigenVermogenTab from "./pages/EigenVermogenTab.jsx";
|
||||
import SchuldTab from "./pages/SchuldTab.jsx";
|
||||
import VoortgangTab from "./pages/VoortgangTab.jsx";
|
||||
import { RED, PURPLE, PURPLE_LIGHT } from "./constants/index.js";
|
||||
import ProfielPopup from "./components/ProfielPopup.jsx";
|
||||
|
||||
function initials(naam = "") {
|
||||
const parts = naam.trim().split(/\s+/);
|
||||
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||
return naam.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
const PAGES = [DashboardTab, EigenVermogenTab, SchuldTab, VoortgangTab];
|
||||
|
||||
function AppInner() {
|
||||
const { loggedIn, login, logout, locked, tab, showUsers, setShowUsers, loading, T, darkMode, setDarkMode, currentUser, avatar } = useApp();
|
||||
const [profielOpen, setProfielOpen] = useState(false);
|
||||
|
||||
const idleTimer = useRef(null);
|
||||
const IDLE_MS = 30 * 60 * 1000;
|
||||
const [sessionVerlopen, setSessionVerlopen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loggedIn) return;
|
||||
const reset = () => {
|
||||
clearTimeout(idleTimer.current);
|
||||
idleTimer.current = setTimeout(() => {
|
||||
logout();
|
||||
setSessionVerlopen(true);
|
||||
}, IDLE_MS);
|
||||
};
|
||||
const events = ["mousemove", "mousedown", "keydown", "touchstart", "scroll"];
|
||||
events.forEach((e) => window.addEventListener(e, reset, { passive: true }));
|
||||
reset();
|
||||
return () => {
|
||||
events.forEach((e) => window.removeEventListener(e, reset));
|
||||
clearTimeout(idleTimer.current);
|
||||
};
|
||||
}, [loggedIn]);
|
||||
|
||||
if (!loggedIn) {
|
||||
if (sessionVerlopen) {
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: "100vh", background: T.bg, color: T.text,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontFamily: "'Inter', system-ui, sans-serif",
|
||||
}}>
|
||||
<div style={{
|
||||
background: T.card, border: `1px solid ${T.border}`,
|
||||
borderRadius: 20, padding: "40px 36px", maxWidth: 360, width: "100%",
|
||||
textAlign: "center", boxShadow: "0 16px 48px rgba(0,0,0,0.4)",
|
||||
}}>
|
||||
<div style={{ fontSize: 52, marginBottom: 16 }}>⏱</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 800, marginBottom: 10 }}>Sessie verlopen</div>
|
||||
<div style={{ fontSize: 14, color: T.muted, marginBottom: 28, lineHeight: 1.6 }}>
|
||||
Je bent automatisch uitgelogd na 30 minuten inactiviteit. Klik hieronder om opnieuw in te loggen.
|
||||
</div>
|
||||
<button onClick={() => { setSessionVerlopen(false); }} style={{
|
||||
width: "100%", padding: "14px",
|
||||
background: "linear-gradient(135deg, #8b5cf6, #a855f7)",
|
||||
border: "none", borderRadius: 12,
|
||||
color: "#fff", fontWeight: 700, fontSize: 15, cursor: "pointer",
|
||||
boxShadow: "0 4px 16px rgba(139,92,246,0.4)",
|
||||
}}>
|
||||
Opnieuw inloggen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <LoginPage onLogin={login} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: "100vh", background: T.bg, color: T.text,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
flexDirection: "column", gap: 16,
|
||||
fontFamily: "'Inter', system-ui, sans-serif",
|
||||
}}>
|
||||
<div style={{
|
||||
width: 40, height: 40, borderRadius: "50%",
|
||||
border: `3px solid rgba(139,92,246,0.2)`,
|
||||
borderTop: `3px solid ${PURPLE_LIGHT}`,
|
||||
animation: "spin 0.8s linear infinite",
|
||||
}} />
|
||||
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
||||
<div style={{ color: T.muted, fontSize: 14 }}>Data laden…</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ActivePage = PAGES[tab] ?? DashboardTab;
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", minHeight: "100vh", background: T.bg, color: T.text, fontFamily: "'Inter', system-ui, sans-serif" }}>
|
||||
<style>{`
|
||||
* { box-sizing: border-box; }
|
||||
input[type=number]::-webkit-inner-spin-button { opacity: 0.4; }
|
||||
`}</style>
|
||||
|
||||
<NavBar />
|
||||
|
||||
{/* Hoofdinhoud */}
|
||||
<div style={{ flex: 1, overflowY: "auto", maxHeight: "100vh" }}>
|
||||
{/* Topbalk rechts */}
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 8, padding: "12px 20px 0" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setProfielOpen((o) => !o)} style={{
|
||||
display: "flex", alignItems: "center", gap: 6,
|
||||
background: "transparent", border: "none", cursor: "pointer", padding: 0,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: "50%", overflow: "hidden", flexShrink: 0,
|
||||
background: avatar ? "transparent" : (profielOpen ? `linear-gradient(135deg, ${PURPLE}, #a855f7)` : `${PURPLE}33`),
|
||||
border: `1px solid ${profielOpen ? PURPLE : PURPLE + "55"}`,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: 13, fontWeight: 800, color: profielOpen ? "#fff" : PURPLE_LIGHT,
|
||||
}}>
|
||||
{avatar
|
||||
? <img src={avatar} alt="avatar" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
||||
: initials(currentUser?.naam || "")
|
||||
}
|
||||
</div>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none"
|
||||
style={{ color: T.muted, transition: "transform 0.2s", transform: profielOpen ? "rotate(180deg)" : "none", flexShrink: 0 }}>
|
||||
<polyline points="1,3 5,7 9,3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
{profielOpen && <ProfielPopup onClose={() => setProfielOpen(false)} />}
|
||||
</div>
|
||||
<button onClick={() => setDarkMode((d) => !d)} style={{
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
background: T.card, border: `1px solid ${T.border}`,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
cursor: "pointer", fontSize: 20,
|
||||
}} title="Dark modus">
|
||||
{darkMode ? "🌙" : "☀️"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Vergrendeld banner */}
|
||||
{locked && (
|
||||
<div style={{
|
||||
position: "sticky", top: 16, zIndex: 150,
|
||||
margin: "16px auto 0", maxWidth: 400,
|
||||
background: "rgba(239,68,68,0.15)",
|
||||
border: "1px solid rgba(239,68,68,0.4)",
|
||||
borderRadius: 99, padding: "8px 20px",
|
||||
display: "flex", alignItems: "center", gap: 8,
|
||||
backdropFilter: "blur(8px)",
|
||||
}}>
|
||||
<span style={{ fontSize: 14 }}>🔒</span>
|
||||
<span style={{ fontSize: 13, color: RED, fontWeight: 600 }}>
|
||||
Vergrendeld — ga naar Gebruikers om te ontgrendelen
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gebruikersbeheer modal */}
|
||||
{showUsers && <GebruikersBeheer onClose={() => setShowUsers(false)} />}
|
||||
|
||||
{/* Actieve pagina */}
|
||||
<div style={{ maxWidth: 1200, margin: "0 auto", padding: "24px 16px" }}>
|
||||
<ActivePage />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<AppProvider>
|
||||
<AppInner />
|
||||
</AppProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user