mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
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.
This commit is contained in:
1 parent
36cbf48286
commit
e74987e819
1 file changed
+49
@@ -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<Record<string, string>>({});
|
||||
|
||||
// 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}
|
||||
/>
|
||||
<FieldError id="email-error" message={fieldErrors.email} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
>
|
||||
<option value="">{t("settings.genderNone")}</option>
|
||||
<option value="MALE">{t("settings.genderMale")}</option>
|
||||
@@ -361,6 +378,7 @@ export function AccountSection() {
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.genderHint")}
|
||||
</p>
|
||||
<FieldError id="gender-error" message={fieldErrors.gender} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="height">
|
||||
@@ -374,7 +392,10 @@ export function AccountSection() {
|
||||
value={height}
|
||||
onChange={setHeight}
|
||||
enterKeyHint="next"
|
||||
invalid={Boolean(fieldErrors.heightCm)}
|
||||
describedBy={fieldErrors.heightCm ? "height-error" : undefined}
|
||||
/>
|
||||
<FieldError id="height-error" message={fieldErrors.heightCm} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.dateOfBirthHint")}
|
||||
</p>
|
||||
<FieldError id="dob-error" message={fieldErrors.dateOfBirth} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language-select">{t("settings.language")}</Label>
|
||||
@@ -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
|
||||
}
|
||||
/>
|
||||
<FieldError id="full-name-error" message={fieldErrors.fullName} />
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
@@ -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
|
||||
}
|
||||
/>
|
||||
<FieldError
|
||||
id="insurer-error"
|
||||
message={fieldErrors.insurerName}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -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
|
||||
}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.identity.insuranceNumberHint")}
|
||||
</p>
|
||||
<FieldError
|
||||
id="insurance-number-error"
|
||||
message={fieldErrors.insuranceNumber}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user