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")}

+