Een dating-app op basis van brieven, niet van gezichten!

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

8.5 kB Ruw bestand
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>
  );
}