Files
VibeFinance/frontend/src/components/ProfielPopup.jsx
T

267 lines
12 KiB
React
Raw Normal View History

import { useState } from "react";
import { useApp } from "../context/AppContext.jsx";
import { RED } from "../constants/index.js";
const TEAL = "linear-gradient(135deg, #7c3aed, #a855f7)";
const TEAL_SHADOW = "0 4px 14px rgba(124,58,237,0.4)";
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" : TEAL,
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: TEAL,
border: "none", borderRadius: 10, color: "#fff",
fontWeight: 700, fontSize: 13, cursor: "pointer",
boxShadow: TEAL_SHADOW,
}}>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" : TEAL,
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: TEAL,
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: TEAL,
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>
</>
);
}