0.1.5-dev
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#7c3aed"/>
|
||||
<stop offset="100%" stop-color="#a855f7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="32" height="32" rx="8" fill="url(#bg)"/>
|
||||
<polyline
|
||||
points="4,24 10,15 16,19 22,9 28,13"
|
||||
fill="none" stroke="#ffffff" stroke-width="3"
|
||||
stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<line x1="4" y1="27" x2="28" y2="27"
|
||||
stroke="rgba(255,255,255,0.35)" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 578 B |
+5
-15
@@ -10,6 +10,7 @@ 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";
|
||||
import InstellingenModal from "./components/InstellingenModal.jsx";
|
||||
|
||||
function initials(naam = "") {
|
||||
const parts = naam.trim().split(/\s+/);
|
||||
@@ -20,11 +21,11 @@ function initials(naam = "") {
|
||||
const PAGES = [DashboardTab, EigenVermogenTab, SchuldTab, VoortgangTab];
|
||||
|
||||
function AppInner() {
|
||||
const { loggedIn, login, logout, locked, tab, showUsers, setShowUsers, loading, T, darkMode, setDarkMode, currentUser, avatar } = useApp();
|
||||
const { loggedIn, login, logout, locked, tab, showUsers, setShowUsers, showInstellingen, setShowInstellingen, loading, T, currentUser, avatar, idleMinutes } = useApp();
|
||||
const [profielOpen, setProfielOpen] = useState(false);
|
||||
|
||||
const idleTimer = useRef(null);
|
||||
const IDLE_MS = 30 * 60 * 1000;
|
||||
const IDLE_MS = (idleMinutes ?? 30) * 60 * 1000;
|
||||
const [sessionVerlopen, setSessionVerlopen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -131,21 +132,9 @@ function AppInner() {
|
||||
: 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 */}
|
||||
@@ -166,8 +155,9 @@ function AppInner() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gebruikersbeheer modal */}
|
||||
{/* Modals */}
|
||||
{showUsers && <GebruikersBeheer onClose={() => setShowUsers(false)} />}
|
||||
{showInstellingen && <InstellingenModal onClose={() => setShowInstellingen(false)} />}
|
||||
|
||||
{/* Actieve pagina */}
|
||||
<div style={{ maxWidth: 1200, margin: "0 auto", padding: "24px 16px" }}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { PURPLE, PURPLE_LIGHT, RED, GREEN } from "../constants/index.js";
|
||||
import { Overlay, ModalBox, ModalHeader } from "./ui/index.jsx";
|
||||
import { useApp } from "../context/AppContext.jsx";
|
||||
|
||||
const getToken = () => localStorage.getItem("vf_token");
|
||||
@@ -168,19 +167,44 @@ export default function GebruikersBeheer({ onClose }) {
|
||||
const td = { padding: "10px 12px", fontSize: 13, color: T.text, borderBottom: `1px solid ${T.border}` };
|
||||
|
||||
return (
|
||||
<Overlay onClose={onClose}>
|
||||
<ModalBox maxWidth={700}>
|
||||
<ModalHeader
|
||||
icon="👥" iconBg={PURPLE}
|
||||
title="Gebruikersbeheer"
|
||||
subtitle={`${users.length} gebruiker${users.length !== 1 ? "s" : ""}`}
|
||||
subtitleColor={PURPLE_LIGHT}
|
||||
onClose={onClose}
|
||||
headerBg={darkMode ? `${PURPLE}08` : "#f8f5ff"}
|
||||
T={T}
|
||||
/>
|
||||
<div
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: "fixed", inset: 0, zIndex: 400,
|
||||
background: "rgba(0,0,0,0.55)", backdropFilter: "blur(4px)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
background: T.card, border: `1px solid ${T.border}`,
|
||||
borderRadius: 16, width: "100%", maxWidth: 700,
|
||||
maxHeight: "90vh", display: "flex", flexDirection: "column",
|
||||
boxShadow: "0 16px 48px rgba(0,0,0,0.4)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||
padding: "18px 22px", borderBottom: `1px solid ${T.border}`, flexShrink: 0,
|
||||
}}>
|
||||
<div>
|
||||
<span style={{ fontWeight: 700, fontSize: 15, color: T.text }}>Gebruikersbeheer</span>
|
||||
<span style={{ marginLeft: 10, fontSize: 11, fontWeight: 600, color: PURPLE_LIGHT,
|
||||
background: `${PURPLE}22`, borderRadius: 99, padding: "2px 8px", border: `1px solid ${PURPLE}44`,
|
||||
}}>
|
||||
{users.length} gebruiker{users.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
<button onClick={onClose} style={{
|
||||
background: "transparent", border: "none", cursor: "pointer",
|
||||
color: T.muted, fontSize: 20, lineHeight: 1, padding: 4,
|
||||
}}>×</button>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: "16px 22px" }}>
|
||||
<div style={{ padding: "16px 22px", overflowY: "auto", flex: 1 }}>
|
||||
{loadErr && (
|
||||
<div style={{ color: RED, fontSize: 13, marginBottom: 12 }}>{loadErr}</div>
|
||||
)}
|
||||
@@ -342,14 +366,7 @@ export default function GebruikersBeheer({ onClose }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: "12px 22px", borderTop: `1px solid ${T.border}` }}>
|
||||
<button onClick={onClose} style={{
|
||||
width: "100%", padding: "10px", background: "transparent",
|
||||
border: `1px solid ${T.border}`, borderRadius: 10,
|
||||
color: T.muted, cursor: "pointer", fontWeight: 600, fontSize: 13,
|
||||
}}>Sluiten</button>
|
||||
</div>
|
||||
</ModalBox>
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useApp } from "../context/AppContext.jsx";
|
||||
import { PURPLE } from "../constants/index.js";
|
||||
|
||||
const BACKUP_ICON = (
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 15V5M8 11l4-4 4 4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M4 17v2a1 1 0 001 1h14a1 1 0 001-1v-2" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const RESTORE_ICON = (
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M4 8a8 8 0 1 1 0 8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/>
|
||||
<polyline points="4,4 4,8 8,8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
function ActionRow({ icon, title, description, onClick }) {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
style={{
|
||||
display: "flex", alignItems: "center", gap: 14,
|
||||
padding: "12px 0", cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div style={{ color: "#94a3b8", flexShrink: 0 }}>{icon}</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: 14, color: "inherit", marginBottom: 2 }}>{title}</div>
|
||||
<div style={{ fontSize: 12, color: "#64748b" }}>{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InstellingenModal({ onClose }) {
|
||||
const { T, darkMode, setDarkMode, doBackup, doRestore } = useApp();
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: "fixed", inset: 0, zIndex: 400,
|
||||
background: "rgba(0,0,0,0.55)", backdropFilter: "blur(4px)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
background: T.card, border: `1px solid ${T.border}`,
|
||||
borderRadius: 16, padding: "22px 22px 8px",
|
||||
width: 360, boxShadow: "0 16px 48px rgba(0,0,0,0.4)",
|
||||
color: T.text,
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 20 }}>
|
||||
<span style={{ fontWeight: 700, fontSize: 16 }}>Instellingen</span>
|
||||
<button onClick={onClose} style={{
|
||||
background: "transparent", border: "none", cursor: "pointer",
|
||||
color: T.muted, fontSize: 20, lineHeight: 1, padding: "0 4px",
|
||||
}}>×</button>
|
||||
</div>
|
||||
|
||||
{/* Dark mode rij */}
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||
padding: "4px 0 16px",
|
||||
}}>
|
||||
<span style={{ fontWeight: 600, fontSize: 14 }}>Dark mode</span>
|
||||
<div
|
||||
onClick={() => setDarkMode((d) => !d)}
|
||||
style={{
|
||||
width: 44, height: 24, borderRadius: 12, cursor: "pointer",
|
||||
background: darkMode ? `linear-gradient(135deg, ${PURPLE}, #a855f7)` : "#334155",
|
||||
position: "relative", transition: "background 0.2s", flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
position: "absolute", top: 3, borderRadius: "50%",
|
||||
width: 18, height: 18, background: "#fff",
|
||||
left: darkMode ? 23 : 3,
|
||||
transition: "left 0.2s",
|
||||
boxShadow: "0 1px 4px rgba(0,0,0,0.3)",
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 1, background: T.border, margin: "0 -22px" }} />
|
||||
|
||||
{/* Acties */}
|
||||
<div style={{ padding: "4px 0 10px" }}>
|
||||
<ActionRow
|
||||
icon={BACKUP_ICON}
|
||||
title="Backup maken"
|
||||
description="Download een volledige backup als ZIP"
|
||||
onClick={() => { doBackup(); onClose(); }}
|
||||
/>
|
||||
<div style={{ height: 1, background: T.border }} />
|
||||
<ActionRow
|
||||
icon={RESTORE_ICON}
|
||||
title="Backup herstellen"
|
||||
description="Herstel data vanuit een backup-bestand"
|
||||
onClick={() => { doRestore(); onClose(); }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,82 @@
|
||||
import { useState } from "react";
|
||||
import { PURPLE, PURPLE_LIGHT, RED } from "../constants/index.js";
|
||||
|
||||
const PersonIcon = () => (
|
||||
<svg width="15" height="15" viewBox="0 0 16 16" fill="none">
|
||||
<circle cx="8" cy="5" r="3" stroke="currentColor" strokeWidth="1.5"/>
|
||||
<path d="M2 14c0-3 2.5-5 6-5s6 2 6 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||||
</svg>
|
||||
);
|
||||
const MailIcon = () => (
|
||||
<svg width="15" height="15" viewBox="0 0 16 16" fill="none">
|
||||
<rect x="1" y="3" width="14" height="10" rx="2" stroke="currentColor" strokeWidth="1.5"/>
|
||||
<polyline points="1,5 8,9.5 15,5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
const LockIcon = () => (
|
||||
<svg width="15" height="15" viewBox="0 0 16 16" fill="none">
|
||||
<rect x="3" y="7" width="10" height="8" rx="2" stroke="currentColor" strokeWidth="1.5"/>
|
||||
<path d="M5 7V5a3 3 0 016 0v2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
function EyeBtn({ show, onToggle, absolute = false }) {
|
||||
const style = absolute
|
||||
? { position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)" }
|
||||
: { position: "absolute", right: 10, top: "50%", transform: "translateY(-50%)" };
|
||||
return (
|
||||
<button onClick={onToggle} type="button" style={{
|
||||
...style, background: "none", border: "none", cursor: "pointer",
|
||||
color: "rgba(255,255,255,0.35)", padding: 0, lineHeight: 1, display: "flex",
|
||||
}}>
|
||||
{show
|
||||
? <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M1 8s2.5-5 7-5 7 5 7 5-2.5 5-7 5-7-5-7-5z" stroke="currentColor" strokeWidth="1.4"/><circle cx="8" cy="8" r="2" stroke="currentColor" strokeWidth="1.4"/><line x1="2" y1="2" x2="14" y2="14" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/></svg>
|
||||
: <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M1 8s2.5-5 7-5 7 5 7 5-2.5 5-7 5-7-5-7-5z" stroke="currentColor" strokeWidth="1.4"/><circle cx="8" cy="8" r="2" stroke="currentColor" strokeWidth="1.4"/></svg>
|
||||
}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldCard({ icon, label, children, extra }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.09)",
|
||||
borderRadius: 12, padding: "10px 14px", display: "flex", alignItems: "center", gap: 12,
|
||||
}}>
|
||||
<span style={{ color: "rgba(255,255,255,0.35)", flexShrink: 0, display: "flex", paddingTop: 2 }}>{icon}</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, color: PURPLE_LIGHT, textTransform: "uppercase", letterSpacing: "0.07em", marginBottom: 2 }}>{label}</div>
|
||||
<div style={{ position: "relative" }}>
|
||||
{children}
|
||||
{extra}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LoginPage({ onLogin }) {
|
||||
const [mode, setMode] = useState("login");
|
||||
const [naam, setNaam] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [wachtwoord, setWachtwoord] = useState("");
|
||||
const [herhaalWw, setHerhaalWw] = useState("");
|
||||
const [showPass, setShowPass] = useState(false);
|
||||
const [showPass2, setShowPass2] = useState(false);
|
||||
const [err, setErr] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const switchMode = (m) => { setMode(m); setNaam(""); setEmail(""); setWachtwoord(""); setErr(""); };
|
||||
const switchMode = (m) => { setMode(m); setNaam(""); setEmail(""); setWachtwoord(""); setHerhaalWw(""); setErr(""); };
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setErr("");
|
||||
if (mode === "register" && !naam.trim()) { setErr("Vul je naam in."); return; }
|
||||
if (mode === "register") {
|
||||
if (!naam.trim()) { setErr("Vul je naam in."); return; }
|
||||
if (wachtwoord.length < 8) { setErr("Wachtwoord moet minimaal 8 tekens zijn."); return; }
|
||||
if (!/[A-Z]/.test(wachtwoord)) { setErr("Wachtwoord moet minimaal één hoofdletter bevatten."); return; }
|
||||
if (!/[0-9]/.test(wachtwoord)) { setErr("Wachtwoord moet minimaal één cijfer bevatten."); return; }
|
||||
if (wachtwoord !== herhaalWw) { setErr("Wachtwoorden komen niet overeen."); return; }
|
||||
}
|
||||
if (!email || !wachtwoord) { setErr("Vul e-mail en wachtwoord in."); return; }
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -33,11 +95,8 @@ export default function LoginPage({ onLogin }) {
|
||||
};
|
||||
|
||||
const iStyle = {
|
||||
width: "100%",
|
||||
background: "rgba(255,255,255,0.06)",
|
||||
border: "1px solid rgba(255,255,255,0.1)",
|
||||
borderRadius: 8, color: "#f1f5f9",
|
||||
padding: "11px 14px", fontSize: 13,
|
||||
width: "100%", background: "transparent", border: "none",
|
||||
color: "#f1f5f9", padding: "0", fontSize: 13,
|
||||
outline: "none", boxSizing: "border-box",
|
||||
};
|
||||
|
||||
@@ -80,7 +139,7 @@ export default function LoginPage({ onLogin }) {
|
||||
</div>
|
||||
|
||||
{/* Heading */}
|
||||
<div style={{ marginBottom: 28 }}>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h1 style={{ fontSize: 26, fontWeight: 800, margin: "0 0 8px", color: "#fff" }}>
|
||||
{mode === "login" ? "Welkom terug!" : "Account aanmaken"}
|
||||
</h1>
|
||||
@@ -91,36 +150,86 @@ export default function LoginPage({ onLogin }) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tab switcher */}
|
||||
<div style={{
|
||||
display: "flex", background: "rgba(255,255,255,0.06)",
|
||||
border: "1px solid rgba(255,255,255,0.09)",
|
||||
borderRadius: 12, padding: 4, marginBottom: 20, gap: 4,
|
||||
}}>
|
||||
{[["login", "Inloggen"], ["register", "Registreren"]].map(([m, label]) => (
|
||||
<button key={m} onClick={() => switchMode(m)} style={{
|
||||
flex: 1, padding: "9px 0", borderRadius: 9, border: "none",
|
||||
cursor: "pointer", fontSize: 13, fontWeight: 600,
|
||||
background: mode === m ? "rgba(255,255,255,0.12)" : "transparent",
|
||||
color: mode === m ? "#fff" : "rgba(255,255,255,0.4)",
|
||||
transition: "all 0.15s",
|
||||
}}>{label}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Formulier */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10, marginBottom: 20 }}>
|
||||
{mode === "register" && (
|
||||
<div>
|
||||
<input type="text" placeholder="Naam" value={naam}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 20 }}>
|
||||
{mode === "register" ? (
|
||||
<>
|
||||
{/* Volledige naam */}
|
||||
<FieldCard icon={<PersonIcon />} label="Volledige naam">
|
||||
<input type="text" value={naam} placeholder="Jan de Vries"
|
||||
onChange={(e) => { setNaam(e.target.value); setErr(""); }}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
||||
style={iStyle} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<input type="email" placeholder="E-mailadres" value={email}
|
||||
</FieldCard>
|
||||
|
||||
{/* E-mail */}
|
||||
<FieldCard icon={<MailIcon />} label="E-mailadres">
|
||||
<input type="email" value={email} placeholder="jij@voorbeeld.nl"
|
||||
onChange={(e) => { setEmail(e.target.value); setErr(""); }}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
||||
style={iStyle} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ position: "relative" }}>
|
||||
<input type={showPass ? "text" : "password"} placeholder="Wachtwoord"
|
||||
value={wachtwoord}
|
||||
</FieldCard>
|
||||
|
||||
{/* Wachtwoord */}
|
||||
<FieldCard icon={<LockIcon />} label="Wachtwoord" extra={
|
||||
<EyeBtn show={showPass} onToggle={() => setShowPass((s) => !s)} />
|
||||
}>
|
||||
<input type={showPass ? "text" : "password"} value={wachtwoord} placeholder="••••••••"
|
||||
onChange={(e) => { setWachtwoord(e.target.value); setErr(""); }}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
||||
style={{ ...iStyle, paddingRight: 42 }} />
|
||||
<button onClick={() => setShowPass((s) => !s)} style={{
|
||||
position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)",
|
||||
background: "none", border: "none", cursor: "pointer",
|
||||
color: "rgba(255,255,255,0.4)", fontSize: 14, padding: 0, lineHeight: 1,
|
||||
}}>{showPass ? "🙈" : "👁️"}</button>
|
||||
</div>
|
||||
style={{ ...iStyle, paddingRight: 36 }} />
|
||||
</FieldCard>
|
||||
|
||||
{/* Herhaal wachtwoord */}
|
||||
<FieldCard icon={<LockIcon />} label="Herhaal wachtwoord" extra={
|
||||
<EyeBtn show={showPass2} onToggle={() => setShowPass2((s) => !s)} />
|
||||
}>
|
||||
<input type={showPass2 ? "text" : "password"} value={herhaalWw} placeholder="••••••••"
|
||||
onChange={(e) => { setHerhaalWw(e.target.value); setErr(""); }}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
||||
style={{ ...iStyle, paddingRight: 36 }} />
|
||||
</FieldCard>
|
||||
|
||||
<div style={{ fontSize: 11, color: "rgba(255,255,255,0.3)", marginTop: 2 }}>
|
||||
Min. 8 tekens · één hoofdletter · één cijfer
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FieldCard icon={<MailIcon />} label="E-mailadres">
|
||||
<input type="email" value={email} placeholder="jij@voorbeeld.nl"
|
||||
onChange={(e) => { setEmail(e.target.value); setErr(""); }}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
||||
style={iStyle} />
|
||||
</FieldCard>
|
||||
|
||||
<FieldCard icon={<LockIcon />} label="Wachtwoord" extra={
|
||||
<EyeBtn show={showPass} onToggle={() => setShowPass((s) => !s)} />
|
||||
}>
|
||||
<input type={showPass ? "text" : "password"} value={wachtwoord} placeholder="••••••••"
|
||||
onChange={(e) => { setWachtwoord(e.target.value); setErr(""); }}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
||||
style={{ ...iStyle, paddingRight: 36 }} />
|
||||
</FieldCard>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{err && (
|
||||
@@ -145,20 +254,6 @@ export default function LoginPage({ onLogin }) {
|
||||
</button>
|
||||
|
||||
|
||||
{/* Switch mode */}
|
||||
<div style={{ marginTop: 20, textAlign: "center" }}>
|
||||
{mode === "login" ? (
|
||||
<span style={{ fontSize: 12, color: "rgba(255,255,255,0.4)" }}>
|
||||
Nog geen account?{" "}
|
||||
<button onClick={() => switchMode("register")} style={{ background: "none", border: "none", color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, fontWeight: 600, padding: 0 }}>Registreer</button>
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ fontSize: 12, color: "rgba(255,255,255,0.4)" }}>
|
||||
Al een account?{" "}
|
||||
<button onClick={() => switchMode("login")} style={{ background: "none", border: "none", color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, fontWeight: 600, padding: 0 }}>Inloggen</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rechts: app mockup — flat, groot, licht zwevend */}
|
||||
|
||||
@@ -35,10 +35,9 @@ const GEBRUIKERS_ICON = (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const DATA_ICON = (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M8 1v8M5 6l3 3 3-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M2 11v2a1 1 0 001 1h10a1 1 0 001-1v-2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||||
const INSTELLINGEN_ICON = (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M6.5 1a.75.75 0 0 0-.743.648l-.16 1.112a5.07 5.07 0 0 0-.88.51l-1.054-.424a.75.75 0 0 0-.9.31L1.263 4.95a.75.75 0 0 0 .165.96l.888.717a5.14 5.14 0 0 0 0 1.018l-.888.717a.75.75 0 0 0-.165.96l1.5 2.598a.75.75 0 0 0 .9.31l1.053-.424c.276.19.572.36.881.51l.16 1.112A.75.75 0 0 0 6.5 14h3a.75.75 0 0 0 .743-.648l.16-1.112c.309-.15.605-.32.88-.51l1.054.424a.75.75 0 0 0 .9-.31l1.5-2.598a.75.75 0 0 0-.165-.96l-.888-.717a5.14 5.14 0 0 0 0-1.018l.888-.717a.75.75 0 0 0 .165-.96l-1.5-2.598a.75.75 0 0 0-.9-.31l-1.053.424a5.07 5.07 0 0 0-.881-.51L9.743 1.648A.75.75 0 0 0 9.5 1h-3Zm1.5 4.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -95,13 +94,12 @@ export default function NavBar() {
|
||||
const {
|
||||
tab, setTab,
|
||||
setShowUsers,
|
||||
setShowInstellingen,
|
||||
T,
|
||||
logout,
|
||||
doBackup, doRestore,
|
||||
} = useApp();
|
||||
|
||||
const [nieuweVersie, setNieuweVersie] = useState(null);
|
||||
const [dataOpen, setDataOpen] = useState(false);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const W = collapsed ? 56 : 220;
|
||||
@@ -197,62 +195,13 @@ export default function NavBar() {
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
|
||||
{/* Data */}
|
||||
{collapsed ? (
|
||||
<div style={{ position: "relative" }}>
|
||||
<SidebarItem
|
||||
onClick={() => setDataOpen((o) => !o)}
|
||||
icon={DATA_ICON}
|
||||
label="Data"
|
||||
onClick={() => setShowInstellingen(true)}
|
||||
icon={INSTELLINGEN_ICON}
|
||||
label="Instellingen"
|
||||
T={T}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
{dataOpen && (
|
||||
<div style={{
|
||||
position: "absolute", left: "calc(100% + 10px)", top: 0,
|
||||
background: T.card, border: `1px solid ${T.border}`,
|
||||
borderRadius: 8, padding: "4px 0", minWidth: 160, zIndex: 200,
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.3)",
|
||||
}}>
|
||||
<div onClick={() => { doBackup(); setDataOpen(false); }} style={{ display: "flex", alignItems: "center", gap: 8, padding: "8px 14px", cursor: "pointer", color: T.muted, fontSize: 12 }}>
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 1v8M5 6l3 3 3-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/><path d="M2 11v2a1 1 0 001 1h10a1 1 0 001-1v-2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
|
||||
Backup maken
|
||||
</div>
|
||||
<div onClick={() => { doRestore(); setDataOpen(false); }} style={{ display: "flex", alignItems: "center", gap: 8, padding: "8px 14px", cursor: "pointer", color: T.muted, fontSize: 12 }}>
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M2 8a6 6 0 106 -6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/><polyline points="2,4 2,8 6,8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||
Backup herstellen
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div onClick={() => setDataOpen((o) => !o)} style={{
|
||||
display: "flex", alignItems: "center", gap: 10,
|
||||
padding: "9px 12px", borderRadius: 8, cursor: "pointer", marginBottom: 6,
|
||||
color: T.muted, fontSize: 13, fontWeight: 500,
|
||||
}}>
|
||||
{DATA_ICON}
|
||||
<span style={{ flex: 1 }}>Data</span>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"
|
||||
style={{ transform: dataOpen ? "rotate(180deg)" : "none", transition: "transform 0.2s", flexShrink: 0 }}>
|
||||
<polyline points="2,4 6,8 10,4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
{dataOpen && (
|
||||
<div style={{ paddingLeft: 14, marginBottom: 4 }}>
|
||||
<div onClick={() => { doBackup(); setDataOpen(false); }} style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 12px", borderRadius: 8, cursor: "pointer", color: T.muted, fontSize: 12 }}>
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 1v8M5 6l3 3 3-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/><path d="M2 11v2a1 1 0 001 1h10a1 1 0 001-1v-2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
|
||||
Backup maken
|
||||
</div>
|
||||
<div onClick={() => { doRestore(); setDataOpen(false); }} style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 12px", borderRadius: 8, cursor: "pointer", color: T.muted, fontSize: 12 }}>
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M2 8a6 6 0 106 -6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/><polyline points="2,4 2,8 6,8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||
Backup herstellen
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Onderste sectie */}
|
||||
|
||||
@@ -8,225 +8,256 @@ function initials(naam = "") {
|
||||
return naam.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
const LOCK_ICON = (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<rect x="3" y="7" width="10" height="8" rx="2" stroke="currentColor" strokeWidth="1.5"/>
|
||||
<path d="M5 7V5a3 3 0 016 0v2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const LOGOUT_ICON = (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M6 2H3a1 1 0 00-1 1v10a1 1 0 001 1h3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||||
<polyline points="10,5 13,8 10,11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<line x1="13" y1="8" x2="6" y2="8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CHEVRON = (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<polyline points="5,3 9,7 5,11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const BACK_ICON = (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<polyline points="10,3 6,8 10,13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
function InputField({ label, type = "text", value, onChange, T }) {
|
||||
return (
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: T.muted, fontWeight: 600, display: "block", marginBottom: 5, textTransform: "uppercase", letterSpacing: "0.06em" }}>{label}</label>
|
||||
<input type={type} value={value} onChange={onChange} style={{
|
||||
width: "100%", padding: "10px 12px", borderRadius: 10, fontSize: 13,
|
||||
background: T.bg, border: `1px solid ${T.border}`, color: T.text, outline: "none",
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProfielPopup({ onClose }) {
|
||||
const { currentUser, updateProfile, changePassword, logout, T, avatar, setAvatar } = useApp();
|
||||
|
||||
const [activeTab, setActiveTab] = useState("profiel");
|
||||
const [view, setView] = useState("main");
|
||||
|
||||
const [naam, setNaam] = useState(currentUser?.naam || "");
|
||||
const [email, setEmail] = useState(currentUser?.email || "");
|
||||
const [profielError, setProfielError] = useState("");
|
||||
const [profielSuccess, setProfielSuccess] = useState(false);
|
||||
const [profielLoading, setProfielLoading] = useState(false);
|
||||
const [profielErr, setProfielErr] = useState("");
|
||||
const [profielOk, setProfielOk] = useState(false);
|
||||
const [profielBusy, setProfielBusy] = useState(false);
|
||||
|
||||
const [huidig, setHuidig] = useState("");
|
||||
const [nieuw, setNieuw] = useState("");
|
||||
const [bevestig, setBevestig] = useState("");
|
||||
const [pwError, setPwError] = useState("");
|
||||
const [pwSuccess, setPwSuccess] = useState(false);
|
||||
const [pwLoading, setPwLoading] = useState(false);
|
||||
const [pwErr, setPwErr] = useState("");
|
||||
const [pwOk, setPwOk] = useState(false);
|
||||
const [pwBusy, setPwBusy] = useState(false);
|
||||
|
||||
const pIs = {
|
||||
background: T.inputBg, border: `1px solid ${T.inputBorder}`,
|
||||
borderRadius: 10, color: T.text, padding: "10px 12px",
|
||||
fontSize: 13, outline: "none", width: "100%", boxSizing: "border-box",
|
||||
const saveProfiel = async () => {
|
||||
setProfielErr(""); setProfielOk(false);
|
||||
if (!naam.trim() || !email.trim()) { setProfielErr("Naam en e-mail zijn verplicht."); return; }
|
||||
setProfielBusy(true);
|
||||
try { await updateProfile(naam.trim(), email.trim()); setProfielOk(true); }
|
||||
catch (err) { setProfielErr(err.message || "Opslaan mislukt."); }
|
||||
finally { setProfielBusy(false); }
|
||||
};
|
||||
|
||||
const handleSaveProfiel = async () => {
|
||||
setProfielError(""); setProfielSuccess(false);
|
||||
if (!naam.trim() || !email.trim()) { setProfielError("Naam en e-mail zijn verplicht."); return; }
|
||||
setProfielLoading(true);
|
||||
try {
|
||||
await updateProfile(naam.trim(), email.trim());
|
||||
setProfielSuccess(true);
|
||||
} catch (err) {
|
||||
setProfielError(err.message || "Opslaan mislukt.");
|
||||
} finally {
|
||||
setProfielLoading(false);
|
||||
}
|
||||
const saveWachtwoord = async () => {
|
||||
setPwErr(""); setPwOk(false);
|
||||
if (!huidig || !nieuw || !bevestig) { setPwErr("Vul alle velden in."); return; }
|
||||
if (nieuw !== bevestig) { setPwErr("Nieuwe wachtwoorden komen niet overeen."); return; }
|
||||
if (nieuw.length < 8) { setPwErr("Minimaal 8 tekens."); return; }
|
||||
setPwBusy(true);
|
||||
try { await changePassword(huidig, nieuw); setPwOk(true); setHuidig(""); setNieuw(""); setBevestig(""); }
|
||||
catch (err) { setPwErr(err.message || "Wijzigen mislukt."); }
|
||||
finally { setPwBusy(false); }
|
||||
};
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
setPwError(""); setPwSuccess(false);
|
||||
if (!huidig || !nieuw || !bevestig) { setPwError("Vul alle velden in."); return; }
|
||||
if (nieuw !== bevestig) { setPwError("Nieuwe wachtwoorden komen niet overeen."); return; }
|
||||
if (nieuw.length < 8) { setPwError("Nieuw wachtwoord moet minimaal 8 tekens zijn."); return; }
|
||||
setPwLoading(true);
|
||||
try {
|
||||
await changePassword(huidig, nieuw);
|
||||
setPwSuccess(true);
|
||||
setHuidig(""); setNieuw(""); setBevestig("");
|
||||
} catch (err) {
|
||||
setPwError(err.message || "Wachtwoord wijzigen mislukt.");
|
||||
} finally {
|
||||
setPwLoading(false);
|
||||
}
|
||||
const popupStyle = {
|
||||
position: "fixed", top: 64, right: 16, zIndex: 199,
|
||||
background: T.card, border: `1px solid ${T.border}`,
|
||||
borderRadius: 16, width: 340,
|
||||
boxShadow: "0 16px 48px rgba(0,0,0,0.45)",
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
const headerStyle = {
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
padding: "16px 16px 14px", position: "relative",
|
||||
borderBottom: `1px solid ${T.border}`,
|
||||
};
|
||||
|
||||
const closeBtn = (
|
||||
<button onClick={onClose} style={{
|
||||
position: "absolute", right: 14, top: "50%", transform: "translateY(-50%)",
|
||||
background: "transparent", border: "none", cursor: "pointer",
|
||||
color: T.muted, fontSize: 20, lineHeight: 1, padding: 4,
|
||||
}}>×</button>
|
||||
);
|
||||
|
||||
const backBtn = (label) => (
|
||||
<button onClick={() => setView("main")} style={{
|
||||
position: "absolute", left: 12, top: "50%", transform: "translateY(-50%)",
|
||||
background: "transparent", border: "none", cursor: "pointer",
|
||||
color: T.muted, display: "flex", alignItems: "center", padding: 4,
|
||||
}}>{BACK_ICON}</button>
|
||||
);
|
||||
|
||||
const actionRow = (icon, label, onClick, color) => (
|
||||
<div onClick={onClick} style={{
|
||||
display: "flex", alignItems: "center", gap: 12,
|
||||
padding: "14px 18px", cursor: "pointer",
|
||||
color: color || T.text,
|
||||
}}>
|
||||
<span style={{ color: color || T.muted, display: "flex" }}>{icon}</span>
|
||||
<span style={{ flex: 1, fontWeight: 600, fontSize: 14 }}>{label}</span>
|
||||
{!color && <span style={{ color: T.muted, display: "flex" }}>{CHEVRON}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 198 }} />
|
||||
|
||||
{/* Popup */}
|
||||
<div style={popupStyle}>
|
||||
|
||||
{/* ── Hoofdscherm ── */}
|
||||
{view === "main" && (
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
<span style={{ fontWeight: 700, fontSize: 15, color: T.text }}>Mijn account</span>
|
||||
{closeBtn}
|
||||
</div>
|
||||
|
||||
{/* Avatar + naam + email */}
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: "24px 18px 20px", gap: 6 }}>
|
||||
<div style={{
|
||||
position: "fixed", top: 60, right: 16, zIndex: 199,
|
||||
background: T.card, border: `1px solid ${T.border}`,
|
||||
borderRadius: 14, width: 360,
|
||||
boxShadow: "0 12px 40px rgba(0,0,0,0.4)",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", gap: 12,
|
||||
padding: "16px 18px", borderBottom: `1px solid ${T.border}`,
|
||||
}}>
|
||||
<div style={{ position: "relative", flexShrink: 0 }}>
|
||||
<div style={{
|
||||
width: 40, height: 40, borderRadius: "50%", overflow: "hidden",
|
||||
width: 72, height: 72, borderRadius: "50%", overflow: "hidden",
|
||||
background: avatar ? "transparent" : `linear-gradient(135deg, ${PURPLE}, #a855f7)`,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: 14, fontWeight: 800, color: "#fff",
|
||||
boxShadow: `0 0 10px ${PURPLE}66`,
|
||||
fontSize: 24, fontWeight: 800, color: "#fff",
|
||||
boxShadow: `0 0 20px ${PURPLE}55`, marginBottom: 4,
|
||||
}}>
|
||||
{avatar
|
||||
? <img src={avatar} alt="avatar" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
||||
: initials(currentUser?.naam)
|
||||
}
|
||||
</div>
|
||||
<label style={{
|
||||
position: "absolute", bottom: -2, right: -2,
|
||||
width: 16, height: 16, borderRadius: "50%",
|
||||
background: PURPLE, cursor: "pointer",
|
||||
<div style={{ fontWeight: 700, fontSize: 16, color: T.text }}>{currentUser?.naam}</div>
|
||||
<div style={{ fontSize: 13, color: T.muted }}>{currentUser?.email}</div>
|
||||
<button onClick={() => setView("profiel")} style={{
|
||||
marginTop: 8, padding: "9px 24px",
|
||||
background: `linear-gradient(135deg, ${PURPLE}, #a855f7)`,
|
||||
border: "none", borderRadius: 10, color: "#fff",
|
||||
fontWeight: 700, fontSize: 13, cursor: "pointer",
|
||||
boxShadow: `0 4px 14px ${PURPLE}44`,
|
||||
}}>Profiel bewerken</button>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 1, background: T.border }} />
|
||||
{actionRow(LOCK_ICON, "Wachtwoord wijzigen", () => setView("wachtwoord"))}
|
||||
<div style={{ height: 1, background: T.border }} />
|
||||
{actionRow(LOGOUT_ICON, "Uitloggen", () => { logout(); onClose(); }, RED)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Profiel bewerken ── */}
|
||||
{view === "profiel" && (
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
{backBtn()}
|
||||
<span style={{ fontWeight: 700, fontSize: 15, color: T.text }}>Profiel bewerken</span>
|
||||
{closeBtn}
|
||||
</div>
|
||||
<div style={{ padding: "20px 18px", display: "flex", flexDirection: "column", gap: 14 }}>
|
||||
{/* Avatar — klikbaar om foto te uploaden */}
|
||||
<div style={{ display: "flex", justifyContent: "center", marginBottom: 4 }}>
|
||||
<label style={{ cursor: "pointer", position: "relative", display: "block" }} title="Klik om foto te uploaden">
|
||||
<div style={{
|
||||
width: 64, height: 64, borderRadius: "50%", overflow: "hidden",
|
||||
background: avatar ? "transparent" : `linear-gradient(135deg, ${PURPLE}, #a855f7)`,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
border: `2px solid ${T.card}`,
|
||||
}} title="Foto wijzigen">
|
||||
<svg width="8" height="8" viewBox="0 0 10 10" fill="none">
|
||||
<path d="M1 9l2.5-.5L9 3 7 1 1.5 6.5 1 9z" stroke="#fff" strokeWidth="1.2" strokeLinejoin="round"/>
|
||||
fontSize: 20, fontWeight: 800, color: "#fff",
|
||||
}}>
|
||||
{avatar
|
||||
? <img src={avatar} alt="avatar" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
||||
: initials(currentUser?.naam)
|
||||
}
|
||||
</div>
|
||||
<div style={{
|
||||
position: "absolute", inset: 0, borderRadius: "50%",
|
||||
background: "rgba(0,0,0,0.35)", display: "flex", alignItems: "center", justifyContent: "center",
|
||||
opacity: 0, transition: "opacity 0.15s",
|
||||
}} className="avatar-overlay">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M13 3H7L5 6H2a1 1 0 00-1 1v9a1 1 0 001 1h16a1 1 0 001-1V7a1 1 0 00-1-1h-3L13 3z" stroke="#fff" strokeWidth="1.5" strokeLinejoin="round"/>
|
||||
<circle cx="10" cy="11" r="2.5" stroke="#fff" strokeWidth="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<input type="file" accept="image/*" style={{ display: "none" }} onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const file = e.target.files?.[0]; if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => setAvatar(ev.target.result);
|
||||
reader.readAsDataURL(file);
|
||||
}} />
|
||||
</label>
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 14, color: T.text, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
{currentUser?.naam}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: T.muted, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
{currentUser?.email}
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} style={{
|
||||
background: "transparent", border: "none", color: T.muted,
|
||||
fontSize: 18, cursor: "pointer", padding: 4, lineHeight: 1, flexShrink: 0,
|
||||
}}>×</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div style={{ display: "flex", borderBottom: `1px solid ${T.border}` }}>
|
||||
{[
|
||||
{ id: "profiel", label: "👤 Profiel" },
|
||||
{ id: "wachtwoord", label: "🔑 Wachtwoord" },
|
||||
].map(({ id, label }) => (
|
||||
<button key={id} onClick={() => setActiveTab(id)} style={{
|
||||
flex: 1, padding: "10px 0", background: "transparent", border: "none",
|
||||
cursor: "pointer", fontSize: 13, fontWeight: 600,
|
||||
color: activeTab === id ? PURPLE_LIGHT : T.muted,
|
||||
borderBottom: activeTab === id ? `2px solid ${PURPLE}` : "2px solid transparent",
|
||||
marginBottom: -1,
|
||||
}}>{label}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Profiel tab */}
|
||||
{activeTab === "profiel" && (
|
||||
<div style={{ padding: "18px 18px 20px", display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{/* Avatar upload */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<div style={{
|
||||
width: 56, height: 56, borderRadius: "50%", overflow: "hidden", flexShrink: 0,
|
||||
background: avatar ? "transparent" : `linear-gradient(135deg, ${PURPLE}, #a855f7)`,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: 18, fontWeight: 800, color: "#fff",
|
||||
boxShadow: `0 0 10px ${PURPLE}44`,
|
||||
}}>
|
||||
{avatar
|
||||
? <img src={avatar} alt="avatar" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
||||
: initials(currentUser?.naam)
|
||||
}
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
<label style={{
|
||||
padding: "6px 12px", background: `${PURPLE}22`, border: `1px solid ${PURPLE}44`,
|
||||
borderRadius: 8, color: PURPLE_LIGHT, fontSize: 12, fontWeight: 600, cursor: "pointer",
|
||||
}}>
|
||||
Foto uploaden
|
||||
<input type="file" accept="image/*" style={{ display: "none" }} onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => setAvatar(ev.target.result);
|
||||
reader.readAsDataURL(file);
|
||||
}} />
|
||||
</label>
|
||||
<style>{`.avatar-overlay { opacity: 0 !important; } label:hover .avatar-overlay { opacity: 1 !important; }`}</style>
|
||||
{avatar && (
|
||||
<button onClick={() => setAvatar(null)} style={{
|
||||
padding: "6px 12px", background: "transparent", border: `1px solid ${T.border}`,
|
||||
borderRadius: 8, color: T.muted, fontSize: 12, fontWeight: 600, cursor: "pointer",
|
||||
}}>Verwijderen</button>
|
||||
alignSelf: "center", padding: "4px 12px", background: "transparent",
|
||||
border: `1px solid ${T.border}`, borderRadius: 8,
|
||||
color: T.muted, fontSize: 12, fontWeight: 600, cursor: "pointer",
|
||||
}}>Foto verwijderen</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: T.muted, fontWeight: 700, display: "block", marginBottom: 6, textTransform: "uppercase" }}>Naam</label>
|
||||
<input value={naam} onChange={(e) => { setNaam(e.target.value); setProfielSuccess(false); }} style={pIs} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: T.muted, fontWeight: 700, display: "block", marginBottom: 6, textTransform: "uppercase" }}>E-mail</label>
|
||||
<input type="email" value={email} onChange={(e) => { setEmail(e.target.value); setProfielSuccess(false); }} style={pIs} />
|
||||
</div>
|
||||
{profielError && <div style={{ fontSize: 12, color: RED }}>{profielError}</div>}
|
||||
{profielSuccess && <div style={{ fontSize: 12, color: "#10b981" }}>Profiel opgeslagen.</div>}
|
||||
<button onClick={handleSaveProfiel} disabled={profielLoading} style={{
|
||||
<InputField label="Naam" value={naam} onChange={(e) => { setNaam(e.target.value); setProfielOk(false); }} T={T} />
|
||||
<InputField label="E-mail" type="email" value={email} onChange={(e) => { setEmail(e.target.value); setProfielOk(false); }} T={T} />
|
||||
{profielErr && <div style={{ fontSize: 12, color: RED }}>{profielErr}</div>}
|
||||
{profielOk && <div style={{ fontSize: 12, color: "#10b981" }}>Profiel opgeslagen.</div>}
|
||||
<button onClick={saveProfiel} disabled={profielBusy} style={{
|
||||
padding: "11px", background: `linear-gradient(135deg, ${PURPLE}, #a855f7)`,
|
||||
border: "none", borderRadius: 10, color: "#fff", fontWeight: 700,
|
||||
cursor: profielLoading ? "default" : "pointer", fontSize: 13, opacity: profielLoading ? 0.7 : 1,
|
||||
}}>{profielLoading ? "Opslaan…" : "Opslaan"}</button>
|
||||
<button onClick={() => { logout(); onClose(); }} style={{
|
||||
padding: "11px", background: "rgba(180,30,30,0.85)",
|
||||
border: "1px solid rgba(220,50,50,0.4)", borderRadius: 10,
|
||||
color: "#fff", fontWeight: 700, cursor: "pointer", fontSize: 13,
|
||||
}}>Uitloggen</button>
|
||||
fontSize: 13, cursor: profielBusy ? "default" : "pointer", opacity: profielBusy ? 0.7 : 1,
|
||||
}}>{profielBusy ? "Opslaan…" : "Opslaan"}</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Wachtwoord tab */}
|
||||
{activeTab === "wachtwoord" && (
|
||||
<div style={{ padding: "18px 18px 20px", display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{[
|
||||
{ label: "Huidig wachtwoord", value: huidig, set: setHuidig },
|
||||
{ label: "Nieuw wachtwoord", value: nieuw, set: setNieuw },
|
||||
{ label: "Bevestig nieuw", value: bevestig, set: setBevestig },
|
||||
].map(({ label, value, set }) => (
|
||||
<div key={label}>
|
||||
<label style={{ fontSize: 11, color: T.muted, fontWeight: 700, display: "block", marginBottom: 6, textTransform: "uppercase" }}>{label}</label>
|
||||
<input type="password" value={value} onChange={(e) => { set(e.target.value); setPwSuccess(false); }} style={pIs} />
|
||||
{/* ── Wachtwoord wijzigen ── */}
|
||||
{view === "wachtwoord" && (
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
{backBtn()}
|
||||
<span style={{ fontWeight: 700, fontSize: 15, color: T.text }}>Wachtwoord wijzigen</span>
|
||||
{closeBtn}
|
||||
</div>
|
||||
))}
|
||||
{pwError && <div style={{ fontSize: 12, color: RED }}>{pwError}</div>}
|
||||
{pwSuccess && <div style={{ fontSize: 12, color: "#10b981" }}>Wachtwoord gewijzigd.</div>}
|
||||
<button onClick={handleChangePassword} disabled={pwLoading} style={{
|
||||
<div style={{ padding: "20px 18px", display: "flex", flexDirection: "column", gap: 14 }}>
|
||||
<InputField label="Huidig wachtwoord" type="password" value={huidig} onChange={(e) => { setHuidig(e.target.value); setPwOk(false); }} T={T} />
|
||||
<InputField label="Nieuw wachtwoord" type="password" value={nieuw} onChange={(e) => { setNieuw(e.target.value); setPwOk(false); }} T={T} />
|
||||
<InputField label="Bevestig nieuw" type="password" value={bevestig} onChange={(e) => { setBevestig(e.target.value); setPwOk(false); }} T={T} />
|
||||
{pwErr && <div style={{ fontSize: 12, color: RED }}>{pwErr}</div>}
|
||||
{pwOk && <div style={{ fontSize: 12, color: "#10b981" }}>Wachtwoord gewijzigd.</div>}
|
||||
<button onClick={saveWachtwoord} disabled={pwBusy} style={{
|
||||
padding: "11px", background: `linear-gradient(135deg, ${PURPLE}, #a855f7)`,
|
||||
border: "none", borderRadius: 10, color: "#fff", fontWeight: 700,
|
||||
cursor: pwLoading ? "default" : "pointer", fontSize: 13, opacity: pwLoading ? 0.7 : 1,
|
||||
}}>{pwLoading ? "Opslaan…" : "Opslaan"}</button>
|
||||
fontSize: 13, cursor: pwBusy ? "default" : "pointer", opacity: pwBusy ? 0.7 : 1,
|
||||
}}>{pwBusy ? "Opslaan…" : "Opslaan"}</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -39,6 +39,8 @@ export function AppProvider({ children }) {
|
||||
const [tab, setTab] = useState(0);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [showUsers, setShowUsers] = useState(false);
|
||||
const [showInstellingen, setShowInstellingen] = useState(false);
|
||||
const [idleMinutes, setIdleMinutes] = useState(30);
|
||||
|
||||
// ── Financiële data ───────────────────────────────────────────
|
||||
const [data, setDataRaw] = useState(INITIAL_DATA);
|
||||
@@ -201,6 +203,8 @@ export function AppProvider({ children }) {
|
||||
tab, setTab,
|
||||
menuOpen, setMenuOpen,
|
||||
showUsers, setShowUsers,
|
||||
showInstellingen, setShowInstellingen,
|
||||
idleMinutes, setIdleMinutes,
|
||||
T,
|
||||
data, setData,
|
||||
ev, schuld, cats, schuldBlokken, schuldHistory,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vibefinance",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5-dev",
|
||||
"description": "VibeFinance — persoonlijk vermogensbeheer",
|
||||
"private": true,
|
||||
"repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance",
|
||||
|
||||
Reference in New Issue
Block a user