Split the onboarding baseline inputs into their own component

The step's profile fieldset moves to `baseline-fields.tsx` unchanged.
The markup can now be rendered against a server answer on its own,
which is where the next commit puts a test.
This commit is contained in:
Marc-André Bombeck committed 2026-09-09 23:16:50 +02:00
1 parent 1d372df3a0
commit ae20ee3d9d
2 files changed
+149 -96

No files matched your search

@@ -0,0 +1,143 @@
"use client";
import { Input } from "@/components/ui/input";
import { DateField } from "@/components/ui/date-field";
import { FieldGroup } from "@/components/ui/field-group";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { HeightFieldControl } from "@/components/profile/height-field-control";
import { useTranslations } from "@/lib/i18n/context";
import type {
HeightDraft,
HeightUnitAdapter,
} from "@/lib/profile/height-unit-display";
/**
* The four profile inputs of the onboarding baseline step.
*
* Split out of `baseline-form.tsx` so the markup can be rendered on its
* own against a server answer. The step's whole defect lived between
* the response and these inputs — the server named a field it refused
* and nothing on screen changed — so the seam a test needs is exactly
* here: field values in, markup out.
*/
export interface BaselineFieldValues {
displayName: string;
height: HeightDraft;
dateOfBirth: string;
gender: string;
}
const IDS = {
displayName: "ob-baseline-display-name",
heightCm: "ob-baseline-height",
dateOfBirth: "ob-baseline-dob",
gender: "ob-baseline-gender",
} as const;
export function BaselineFields({
value,
onChange,
heightAdapter,
}: {
value: BaselineFieldValues;
onChange: <K extends keyof BaselineFieldValues>(
key: K,
next: BaselineFieldValues[K],
) => void;
heightAdapter: HeightUnitAdapter;
}) {
const { t } = useTranslations();
return (
<fieldset className="bg-card border-border space-y-4 rounded-xl border p-4 md:p-6">
<legend className="sr-only">{t("onboarding.baseline.title")}</legend>
<FieldGroup
htmlFor={IDS.displayName}
label={t("onboarding.baseline.displayNameLabel")}
hint={t("onboarding.baseline.displayNameHint")}
>
<Input
id={IDS.displayName}
value={value.displayName}
onChange={(e) => onChange("displayName", e.target.value)}
autoComplete="nickname"
maxLength={50}
placeholder={t("onboarding.baseline.displayNamePlaceholder")}
/>
</FieldGroup>
<div className="grid gap-4 sm:grid-cols-2">
<FieldGroup
htmlFor={IDS.heightCm}
label={
heightAdapter.usesFeetInches
? t("onboarding.baseline.heightLabelFtIn")
: t("onboarding.baseline.heightLabel")
}
>
<HeightFieldControl
idPrefix={IDS.heightCm}
adapter={heightAdapter}
value={value.height}
onChange={(next) => onChange("height", next)}
autoComplete="off"
/>
</FieldGroup>
<FieldGroup
htmlFor={IDS.gender}
label={t("onboarding.baseline.genderLabel")}
>
<Select
// The design system's Radix Select uses an empty-string
// sentinel to mean "no selection"; map back and forth so
// the form state ("") and the Select's value (undefined-
// adjacent) stay aligned. v1.4.25 W21 Fix-N (design-M1).
value={value.gender === "" ? undefined : value.gender}
onValueChange={(next) => onChange("gender", next)}
>
<SelectTrigger
id={IDS.gender}
className="w-full"
data-slot="onboarding-baseline-gender"
>
<SelectValue placeholder={t("onboarding.baseline.genderNone")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="MALE">
{t("onboarding.baseline.genderMale")}
</SelectItem>
<SelectItem value="FEMALE">
{t("onboarding.baseline.genderFemale")}
</SelectItem>
<SelectItem value="OTHER">
{t("onboarding.baseline.genderOther")}
</SelectItem>
</SelectContent>
</Select>
</FieldGroup>
</div>
<FieldGroup
htmlFor={IDS.dateOfBirth}
label={t("onboarding.baseline.dateOfBirthLabel")}
hint={t("onboarding.baseline.dateOfBirthHint")}
>
<DateField
id={IDS.dateOfBirth}
value={value.dateOfBirth}
onChange={(next) => onChange("dateOfBirth", next)}
max={new Date().toISOString().slice(0, 10)}
autoComplete="bday"
/>
</FieldGroup>
</fieldset>
);
}
+6 -96
View File
@@ -8,17 +8,7 @@ import { toast } from "sonner";
import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { DateField } from "@/components/ui/date-field";
import { FieldGroup } from "@/components/ui/field-group";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { HeightFieldControl } from "@/components/profile/height-field-control";
import { BaselineFields } from "@/components/onboarding/baseline-fields";
import { useUnitDisplay } from "@/hooks/use-unit-display";
import {
EMPTY_HEIGHT_DRAFT,
@@ -201,91 +191,11 @@ export function BaselineForm() {
</p>
</header>
<fieldset className="bg-card border-border space-y-4 rounded-xl border p-4 md:p-6">
<legend className="sr-only">{t("onboarding.baseline.title")}</legend>
<FieldGroup
htmlFor="ob-baseline-display-name"
label={t("onboarding.baseline.displayNameLabel")}
hint={t("onboarding.baseline.displayNameHint")}
>
<Input
id="ob-baseline-display-name"
value={form.displayName}
onChange={(e) => patch("displayName", e.target.value)}
autoComplete="nickname"
maxLength={50}
placeholder={t("onboarding.baseline.displayNamePlaceholder")}
/>
</FieldGroup>
<div className="grid gap-4 sm:grid-cols-2">
<FieldGroup
htmlFor="ob-baseline-height"
label={
heightAdapter.usesFeetInches
? t("onboarding.baseline.heightLabelFtIn")
: t("onboarding.baseline.heightLabel")
}
>
<HeightFieldControl
idPrefix="ob-baseline-height"
adapter={heightAdapter}
value={form.height}
onChange={(next) => patch("height", next)}
autoComplete="off"
/>
</FieldGroup>
<FieldGroup
htmlFor="ob-baseline-gender"
label={t("onboarding.baseline.genderLabel")}
>
<Select
// The design system's Radix Select uses an empty-string
// sentinel to mean "no selection"; map back and forth so
// the form state ("") and the Select's value (undefined-
// adjacent) stay aligned. v1.4.25 W21 Fix-N (design-M1).
value={form.gender === "" ? undefined : form.gender}
onValueChange={(next) => patch("gender", next)}
>
<SelectTrigger
id="ob-baseline-gender"
className="w-full"
data-slot="onboarding-baseline-gender"
>
<SelectValue
placeholder={t("onboarding.baseline.genderNone")}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="MALE">
{t("onboarding.baseline.genderMale")}
</SelectItem>
<SelectItem value="FEMALE">
{t("onboarding.baseline.genderFemale")}
</SelectItem>
<SelectItem value="OTHER">
{t("onboarding.baseline.genderOther")}
</SelectItem>
</SelectContent>
</Select>
</FieldGroup>
</div>
<FieldGroup
htmlFor="ob-baseline-dob"
label={t("onboarding.baseline.dateOfBirthLabel")}
hint={t("onboarding.baseline.dateOfBirthHint")}
>
<DateField
id="ob-baseline-dob"
value={form.dateOfBirth}
onChange={(value) => patch("dateOfBirth", value)}
max={new Date().toISOString().slice(0, 10)}
autoComplete="bday"
/>
</FieldGroup>
</fieldset>
<BaselineFields
value={form}
onChange={patch}
heightAdapter={heightAdapter}
/>
<AnamnesisCard
value={anamnesis}