Een dating-app op basis van brieven, niet van gezichten!
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import { useState } from "react";
import { INK, RED, BLUE, PAPER, SKY, MUTE, FONT_MONO } from "../../theme";
import { GENDER_LABEL, genderLabel, PASSWORD_MIN, PASSWORD_MAX, NAME_MAX } from "../../constants";
import DateFields from "../../components/DateFields";
/*
* Account (AVG art. 16, rectificatie): naam, e-mail, geboortedatum, gender/trans en wachtwoord
* wijzigen — bij Mijn profiel. Ingeklapt tot je op 'wijzigen' klikt. Alleen gewijzigde velden gaan
* mee; het huidige wachtwoord is altijd verplicht. onSave(changes) → Promise<me>.
*/
/* Date (lokaal) → 'YYYY-MM-DD' */
const toIso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
export default function AccountSection({ me, onSave, onLogoutOthers }) {
const [open, setOpen] = useState(false);
const [name, setName] = useState(me.name);
const [email, setEmail] = useState(me.email);
const [birthdate, setBirthdate] = useState(toIso(me.birth)); // null zolang de datumvelden geen echte datum vormen
const [gender, setGender] = useState(me.gender);
const [trans, setTrans] = useState(me.trans);
const [newPassword, setNewPassword] = useState("");
const [currentPassword, setCurrentPassword] = useState("");
const [state, setState] = useState({ busy: false, saved: false, error: null });
/* De knop 'overal elders uitloggen' heeft een eigen uitkomst: null = nog niet gedaan */
const [elders, setElders] = useState({ busy: false, done: null });
/* Wat is er anders dan wat de server heeft? Alleen dat sturen we */
const changes = {
...(name.trim() !== me.name && { name: name.trim() }),
...(email.trim().toLowerCase() !== me.email && { email: email.trim() }),
...(birthdate && birthdate !== toIso(me.birth) && { birthdate }),
...(gender !== me.gender && { gender }),
...(trans !== me.trans && { trans }),
...(newPassword && { newPassword }),
};
const dirty = Object.keys(changes).length > 0;
const valid =
name.trim() && name.trim().length <= NAME_MAX && email.trim() && birthdate &&
(!newPassword || newPassword.length >= PASSWORD_MIN);
const canSave = dirty && valid && currentPassword && !state.busy;
const save = async (e) => {
e.preventDefault();
if (!canSave) return;
setState({ busy: true, saved: false, error: null });
try {
await onSave({ ...changes, currentPassword });
setNewPassword("");
setCurrentPassword("");
setState({ busy: false, saved: true, error: null });
setTimeout(() => setState((s) => ({ ...s, saved: false })), 2500);
} catch (err) {
setState({ busy: false, saved: false, error: err.message });
}
};
/*
* Overal elders uitloggen. Gebruikt hetzelfde 'huidig wachtwoord' als het formulier eromheen —
* daarom staat het hier en niet als losse knop met een eigen wachtwoordveld.
*/
const logoutOthers = async () => {
if (!currentPassword || elders.busy || !onLogoutOthers) return;
setElders({ busy: true, done: null });
setState((s) => ({ ...s, error: null }));
try {
const { loggedOut } = await onLogoutOthers(currentPassword);
setElders({ busy: false, done: loggedOut });
} catch (err) {
setElders({ busy: false, done: null });
setState((s) => ({ ...s, error: err.message }));
}
};
const input = { width: "100%", padding: "10px 12px", fontSize: 15, border: `2px solid ${INK}`, background: PAPER, borderRadius: 0, marginTop: 6 };
const label = { display: "block", fontWeight: 700, fontSize: 14, marginBottom: 14 };
const chip = (active) => ({
padding: "8px 14px", border: `2px solid ${INK}`, fontWeight: 600, fontSize: 14,
background: active ? INK : "#fff", color: active ? "#fff" : INK,
});
const link = { background: "none", border: "none", color: BLUE, fontWeight: 700, fontSize: 13, padding: 0, textDecoration: "underline" };
return (
<div style={{ marginTop: 36, paddingTop: 16, borderTop: `1px solid ${SKY}` }}>
<h3 style={{ margin: "0 0 4px", fontSize: 16 }}>Account</h3>
<p style={{ fontFamily: FONT_MONO, fontSize: 11, color: MUTE, margin: "0 0 10px" }}>
{me.name} · {genderLabel(me)} · {me.age} jaar · {me.email}
</p>
{!open ? (
<button onClick={() => setOpen(true)} style={link}>gegevens of wachtwoord wijzigen</button>
) : (
<form onSubmit={save} noValidate>
<label style={label}>
Naam
<input type="text" value={name} onChange={(e) => setName(e.target.value)} maxLength={NAME_MAX} autoComplete="given-name" style={input} />
</label>
<label style={label}>
E-mailadres
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} autoComplete="email" inputMode="email" autoCapitalize="none" style={input} />
</label>
<p style={{ fontWeight: 700, fontSize: 14, margin: "0 0 6px" }}>Ik ben</p>
<div style={{ display: "flex", gap: 8, marginBottom: 8, flexWrap: "wrap", alignItems: "center" }}>
{Object.entries(GENDER_LABEL).map(([g, text]) => (
<button type="button" key={g} onClick={() => setGender(g)} aria-pressed={gender === g} style={chip(gender === g)}>
{text}
</button>
))}
<span aria-hidden="true" style={{ fontWeight: 700 }}>–</span>
<button type="button" onClick={() => setTrans((t) => !t)} aria-pressed={trans} style={chip(trans)}>
trans
</button>
</div>
<p style={{ fontFamily: FONT_MONO, fontSize: 11, color: MUTE, margin: "0 0 14px" }}>
je staat als: {genderLabel({ gender, trans })}
{(gender !== me.gender || trans !== me.trans) && <> · let op: dit verandert wie jou ziet en wie jij ziet</>}
</p>
<div style={label}>
Geboortedatum
<DateFields value={toIso(me.birth)} onChange={setBirthdate} inputStyle={{ marginTop: 6 }} />
</div>
<label style={label}>
Nieuw wachtwoord <span style={{ fontWeight: 400, color: MUTE }}>(leeg = ongewijzigd)</span>
<input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} autoComplete="new-password"
maxLength={PASSWORD_MAX} placeholder={`minimaal ${PASSWORD_MIN} tekens`} style={input} />
</label>
{newPassword && (
<p style={{ fontSize: 13, color: MUTE, margin: "-8px 0 14px" }}>Na het wijzigen zijn je andere toestellen uitgelogd; dit toestel blijft ingelogd.</p>
)}
<label style={{ ...label, paddingTop: 8, borderTop: `1px solid ${SKY}` }}>
Huidig wachtwoord <span style={{ fontWeight: 400, color: MUTE }}>(ter bevestiging)</span>
<input type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} autoComplete="current-password"
maxLength={PASSWORD_MAX} style={input} />
</label>
{onLogoutOthers && (
<p style={{ fontSize: 13, color: MUTE, margin: "-8px 0 14px" }}>
Staat er nog ergens anders een toestel ingelogd?{" "}
<button type="button" onClick={logoutOthers} disabled={!currentPassword || elders.busy}
style={{ ...link, fontSize: 13, opacity: currentPassword && !elders.busy ? 1 : 0.5 }}>
{elders.busy ? "bezig…" : "log ze uit"}
</button>
{elders.done !== null && (
<> — {elders.done === 0
? "er stond er geen een."
: `${elders.done} ${elders.done === 1 ? "toestel is" : "toestellen zijn"} uitgelogd.`}</>
)}
</p>
)}
{state.error && (
<p role="alert" style={{ fontFamily: FONT_MONO, fontSize: 12, color: RED, margin: "0 0 10px" }}>fout: {state.error}</p>
)}
<div style={{ display: "flex", gap: 12, alignItems: "center" }}>
<button
type="submit"
disabled={!canSave}
style={{
background: state.saved ? BLUE : RED, color: "#fff", border: "none", padding: "12px 22px", fontSize: 15, fontWeight: 700,
opacity: canSave || state.saved ? 1 : 0.5,
}}
>
{state.busy ? "Opslaan…" : state.saved ? "✓ Opgeslagen" : "Account opslaan"}
</button>
<button type="button" onClick={() => setOpen(false)} style={{ ...link, color: MUTE, fontWeight: 600 }}>sluiten</button>
</div>
</form>
)}
</div>
);
}