Files
VibeFinance/frontend/src/components/ProfielPopup.jsx
T
2026-09-15 14:02:17 +02:00

265 lines
12 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { useApp } from "../context/AppContext.jsx";
import { RED } from "../constants/index.js";
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 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 [view, setView] = useState("main");
const [naam, setNaam] = useState(currentUser?.naam || "");
const [email, setEmail] = useState(currentUser?.email || "");
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 [pwErr, setPwErr] = useState("");
const [pwOk, setPwOk] = useState(false);
const [pwBusy, setPwBusy] = useState(false);
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 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 popupStyle = {
position: "fixed", top: 64, right: 16, zIndex: 199,
background: T.popupBg,
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 (
<>
<div onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 198 }} />
<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={{
width: 72, height: 72, borderRadius: "50%", overflow: "hidden",
background: avatar ? "transparent" : T.accentGradient,
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: 24, fontWeight: 800, color: "#fff",
boxShadow: "0 0 20px rgba(124,58,237,0.4)", marginBottom: 4,
}}>
{avatar
? <img src={avatar} alt="avatar" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
: initials(currentUser?.naam)
}
</div>
<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: T.accentGradient,
border: "none", borderRadius: 10, color: "#fff",
fontWeight: 700, fontSize: 13, cursor: "pointer",
boxShadow: T.cardShadow,
}}>Profiel bewerken</button>
</div>
<div style={{ height: 1, background: T.border }} />
{actionRow(LOCK_ICON, "Wachtwoord wijzigen", () => setView("wachtwoord"))}
</>
)}
{/* ── 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" : T.accentGradient,
display: "flex", alignItems: "center", justifyContent: "center",
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 reader = new FileReader();
reader.onload = (ev) => setAvatar(ev.target.result);
reader.readAsDataURL(file);
}} />
</label>
</div>
<style>{`.avatar-overlay { opacity: 0 !important; } label:hover .avatar-overlay { opacity: 1 !important; }`}</style>
{avatar && (
<button onClick={() => setAvatar(null)} style={{
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>
)}
<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: T.accentGradient,
border: "none", borderRadius: 10, color: "#fff", fontWeight: 700,
fontSize: 13, cursor: profielBusy ? "default" : "pointer", opacity: profielBusy ? 0.7 : 1,
}}>{profielBusy ? "Opslaan…" : "Opslaan"}</button>
</div>
</>
)}
{/* ── Wachtwoord wijzigen ── */}
{view === "wachtwoord" && (
<>
<div style={headerStyle}>
{backBtn()}
<span style={{ fontWeight: 700, fontSize: 15, color: T.text }}>Wachtwoord wijzigen</span>
{closeBtn}
</div>
<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: T.accentGradient,
border: "none", borderRadius: 10, color: "#fff", fontWeight: 700,
fontSize: 13, cursor: pwBusy ? "default" : "pointer", opacity: pwBusy ? 0.7 : 1,
}}>{pwBusy ? "Opslaan…" : "Opslaan"}</button>
</div>
</>
)}
</div>
</>
);
}