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 { 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>
);
}