From e74987e819a90562e0707030380cd23598fcc9e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 9 Sep 2026 23:22:14 +0200 Subject: [PATCH] Give the settings profile form the same per-field rejection slots It shares the route and had the same gap: the banner named the first refused field and nothing marked the input that held the value. Each of the seven submitted fields now carries the reason in its own slot, through the same component the onboarding step uses. --- .../settings/account-section/index.tsx | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/components/settings/account-section/index.tsx b/src/components/settings/account-section/index.tsx index 1dd03c3f4..cd3370dd2 100644 --- a/src/components/settings/account-section/index.tsx +++ b/src/components/settings/account-section/index.tsx @@ -37,8 +37,10 @@ import { detectBrowserTimezone } from "@/lib/tz/format"; import { apiFetchRaw } from "@/lib/api/api-fetch"; import { describeRejectedProfileField, + describeRejectedProfileFields, type RejectedProfileField, } from "@/lib/profile/rejected-fields"; +import { FieldError } from "@/components/forms/field-error"; import { resolveInitialTimezone, statusText, @@ -90,6 +92,11 @@ export function AccountSection() { const [saveMsgType, setSaveMsgType] = useState< "success" | "warning" | "error" | null >(null); + // One sentence per field the last save refused, keyed by the schema + // name the server sent. The banner above the button says a save was + // partial; these say which value was not stored and why, under the + // input holding it. Cleared at the start of every save. + const [fieldErrors, setFieldErrors] = useState>({}); // v1.23 — passkey + second-factor management moved to the dedicated // /settings/security hub so "how I secure my account" reads as one place. @@ -172,6 +179,7 @@ export function AccountSection() { setSaving(true); setSaveMsg(null); setSaveMsgType(null); + setFieldErrors({}); const savedProfile = { email: email.trim(), height, @@ -234,6 +242,7 @@ export function AccountSection() { params: { field: describeRejectedProfileField(rejected, t) ?? "" }, }); setSaveMsgType("warning"); + setFieldErrors(describeRejectedProfileFields(rejected, t)); await refetch(); setSeededUserId(null); } else { @@ -272,6 +281,7 @@ export function AccountSection() { } | null; const errorCode = json?.meta?.errorCode; const field = describeRejectedProfileField(json?.details?.issues, t); + setFieldErrors(describeRejectedProfileFields(json?.details?.issues, t)); if (errorCode === "profile.update.nothingSaved" && field) { setSaveMsg({ key: "settings.profileNothingSaved", params: { field } }); } else if (errorCode) { @@ -341,7 +351,10 @@ export function AccountSection() { maxLength={320} autoComplete="email" enterKeyHint="next" + aria-invalid={fieldErrors.email ? true : undefined} + aria-describedby={fieldErrors.email ? "email-error" : undefined} /> + @@ -352,6 +365,10 @@ export function AccountSection() { id="gender" value={gender} onChange={(e) => setGender(e.target.value)} + aria-invalid={fieldErrors.gender ? true : undefined} + aria-describedby={ + fieldErrors.gender ? "gender-error" : undefined + } > @@ -361,6 +378,7 @@ export function AccountSection() {

{t("settings.genderHint")}

+
@@ -392,10 +413,15 @@ export function AccountSection() { value={dateOfBirth} onChange={setDateOfBirth} max={new Date().toISOString().slice(0, 10)} + aria-invalid={fieldErrors.dateOfBirth ? true : undefined} + aria-describedby={ + fieldErrors.dateOfBirth ? "dob-error" : undefined + } />

{t("settings.dateOfBirthHint")}

+
@@ -455,7 +481,12 @@ export function AccountSection() { placeholder={t("settings.identity.fullNamePlaceholder")} maxLength={120} autoComplete="name" + aria-invalid={fieldErrors.fullName ? true : undefined} + aria-describedby={ + fieldErrors.fullName ? "full-name-error" : undefined + } /> +
@@ -468,6 +499,14 @@ export function AccountSection() { onChange={(e) => setInsurerName(e.target.value)} placeholder={t("settings.identity.insurerPlaceholder")} maxLength={120} + aria-invalid={fieldErrors.insurerName ? true : undefined} + aria-describedby={ + fieldErrors.insurerName ? "insurer-error" : undefined + } + /> +
@@ -484,10 +523,20 @@ export function AccountSection() { maxLength={10} autoCapitalize="characters" spellCheck={false} + aria-invalid={fieldErrors.insuranceNumber ? true : undefined} + aria-describedby={ + fieldErrors.insuranceNumber + ? "insurance-number-error" + : undefined + } />

{t("settings.identity.insuranceNumberHint")}

+