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.

4.2 kB Ruw bestand
import { useRef, useState } from "react";
import { INK, PAPER, RED, FONT_MONO } from "../theme";

/*
 * Datum als drie getalvelden: dag / maand / jaar. Geen <input type="date">: de native
 * kiezer is gemaakt voor datums dichtbij vandaag, niet om decennia terug te bladeren
 * (geboortedatum). Na twee cijfers springt de focus door naar het volgende veld.
 *
 *   value     'YYYY-MM-DD' of '' (alleen als startwaarde gelezen)
 *   onChange  krijgt 'YYYY-MM-DD', of null zolang de invoer onvolledig is of geen
 *             bestaande datum vormt (31-02, 29-02-2023, …)
 *   inputStyle  extra stijl voor de velden (bijv. de formulierstijl van het scherm)
 *
 * Bereik (leeftijdsgrenzen e.d.) hoort bij de aanroeper of de server; deze component
 * bewaakt alleen dat het een echte kalenderdatum is.
 */

const isoFromParts = ({ d, m, y }) => {
  if (!/^\d{1,2}$/.test(d) || !/^\d{1,2}$/.test(m) || !/^\d{4}$/.test(y)) return null;
  const D = +d, M = +m, Y = +y;
  const date = new Date(Date.UTC(Y, M - 1, D));
  if (date.getUTCFullYear() !== Y || date.getUTCMonth() !== M - 1 || date.getUTCDate() !== D) return null;
  return `${Y}-${String(M).padStart(2, "0")}-${String(D).padStart(2, "0")}`;
};

const partsFromIso = (iso) => {
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso ?? "");
  return m ? { d: m[3], m: m[2], y: m[1] } : { d: "", m: "", y: "" };
};

const digits = (s, n) => s.replace(/\D/g, "").slice(0, n);

export default function DateFields({ value, onChange, inputStyle }) {
  const [parts, setParts] = useState(() => partsFromIso(value));
  const monthRef = useRef(null);
  const yearRef = useRef(null);

  const change = (key, max, nextRef) => (e) => {
    const v = digits(e.target.value, max);
    const next = { ...parts, [key]: v };
    setParts(next);
    onChange(isoFromParts(next));
    /*
     * Vol veld → door naar het volgende, en dáár de inhoud selecteren. Anders komt de cursor
     * achter een al ingevulde maand te staan en doet de volgende toets niets (maxLength is
     * bereikt): je typt tegen een vol veld aan. Met de selectie erop overschrijft de eerste
     * toets wat er stond, wat is wat je bedoelt als je een datum verbetert.
     */
    if (v.length === max && nextRef?.current) {
      nextRef.current.focus();
      nextRef.current.select();
    }
  };

  const complete = parts.d && parts.m && parts.y.length === 4;
  const invalid = complete && !isoFromParts(parts);

  const field = (w) => ({
    padding: "10px 12px", fontSize: 15, border: `2px solid ${INK}`, background: PAPER, borderRadius: 0,
    textAlign: "center", fontVariantNumeric: "tabular-nums", ...inputStyle, width: w,
  });
  /*
   * Elk veld selecteert zijn inhoud zodra het de focus krijgt — met de muis, met tab, of
   * doordat het vorige veld vol raakte. Deze velden hebben een vaste lengte, dus een cursor
   * ergens in "07" zetten helpt niemand: je wilt de maand vervangen, niet een cijfer
   * bijplakken tegen een vol veld aan. onMouseUp moet mee, want een klik zet ná de focus de
   * cursor alsnog op de aangeklikte plek en gooit de selectie weg.
   */
  const selectAll = (e) => e.target.select();
  const common = {
    type: "text", inputMode: "numeric", pattern: "[0-9]*", autoCorrect: "off",
    onFocus: selectAll, onMouseUp: (e) => e.preventDefault(),
  };

  return (
    <div>
      <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
        <input {...common} value={parts.d} onChange={change("d", 2, monthRef)} maxLength={2}
          placeholder="dd" aria-label="Dag" autoComplete="bday-day" style={field(64)} />
        <span aria-hidden="true">–</span>
        <input {...common} ref={monthRef} value={parts.m} onChange={change("m", 2, yearRef)} maxLength={2}
          placeholder="mm" aria-label="Maand" autoComplete="bday-month" style={field(64)} />
        <span aria-hidden="true">–</span>
        <input {...common} ref={yearRef} value={parts.y} onChange={change("y", 4)} maxLength={4}
          placeholder="jjjj" aria-label="Jaar" autoComplete="bday-year" style={field(88)} />
      </div>
      {invalid && (
        <p role="alert" style={{ fontFamily: FONT_MONO, fontSize: 11, color: RED, margin: "6px 0 0" }}>
          {parts.d}-{parts.m}-{parts.y} bestaat niet
        </p>
      )}
    </div>
  );
}