265 lines
12 KiB
React
265 lines
12 KiB
React
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>
|
||
</>
|
||
);
|
||
}
|