mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
refactor(ui): adopt shared readError + ResearchModeStatus + design-system Select
- advanced-section: drops the local ResearchModeStatus interface in favour of the shared DTO, replaces the nested ternary that built the toggle's status caption with a researchModeStatusLabel helper threaded through the shared gate-state classifier. - WelcomeCarousel / GoalsChipPicker / SourceCardGrid / BaselineForm: import the shared readError helper instead of the four hand-rolled copies — the request-failed fallback shape now evolves in one place. - BaselineForm: swap the gender native <select> for the shadcn-radix Select component so the picker matches the rest of the form library (focus rings, keyboard nav, theme tokens).
This commit is contained in:
1 parent
d6456a30eb
commit
2b712429c0
5 files changed
+64
-73
No files matched your search
@@ -11,7 +11,15 @@ import { Button } from "@/components/ui/button";
|
||||
import { DateInput } from "@/components/ui/date-input";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { readError } from "@/lib/api/read-error";
|
||||
|
||||
/**
|
||||
* v1.4.25 W14b-Content — onboarding step 3 (baseline).
|
||||
@@ -166,16 +174,32 @@ export function BaselineForm() {
|
||||
<Label htmlFor="ob-baseline-gender">
|
||||
{t("onboarding.baseline.genderLabel")}
|
||||
</Label>
|
||||
<select
|
||||
id="ob-baseline-gender"
|
||||
value={form.gender}
|
||||
onChange={(e) => patch("gender", e.target.value)}
|
||||
className="border-input bg-background text-foreground ring-offset-background focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm focus-visible:ring-[3px] focus-visible:outline-none"
|
||||
<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)}
|
||||
>
|
||||
<option value="">{t("onboarding.baseline.genderNone")}</option>
|
||||
<option value="MALE">{t("onboarding.baseline.genderMale")}</option>
|
||||
<option value="FEMALE">{t("onboarding.baseline.genderFemale")}</option>
|
||||
</select>
|
||||
<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>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -225,14 +249,3 @@ export function BaselineForm() {
|
||||
);
|
||||
}
|
||||
|
||||
async function readError(res: Response): Promise<string> {
|
||||
try {
|
||||
const json = (await res.json()) as { error?: string };
|
||||
if (typeof json.error === "string" && json.error.length > 0) {
|
||||
return json.error;
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
return `Request failed (${res.status})`;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { readError } from "@/lib/api/read-error";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
@@ -230,14 +231,3 @@ export function GoalsChipPicker({ userId: _userId }: GoalsChipPickerProps) {
|
||||
);
|
||||
}
|
||||
|
||||
async function readError(res: Response): Promise<string> {
|
||||
try {
|
||||
const json = (await res.json()) as { error?: string };
|
||||
if (typeof json.error === "string" && json.error.length > 0) {
|
||||
return json.error;
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
return `Request failed (${res.status})`;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { readError } from "@/lib/api/read-error";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
@@ -309,14 +310,3 @@ function SourceCardItem({
|
||||
);
|
||||
}
|
||||
|
||||
async function readError(res: Response): Promise<string> {
|
||||
try {
|
||||
const json = (await res.json()) as { error?: string };
|
||||
if (typeof json.error === "string" && json.error.length > 0) {
|
||||
return json.error;
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
return `Request failed (${res.status})`;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { readError } from "@/lib/api/read-error";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
@@ -294,14 +295,3 @@ export function WelcomeCarousel() {
|
||||
);
|
||||
}
|
||||
|
||||
async function readError(res: Response): Promise<string> {
|
||||
try {
|
||||
const json = (await res.json()) as { error?: string };
|
||||
if (typeof json.error === "string" && json.error.length > 0) {
|
||||
return json.error;
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
return `Request failed (${res.status})`;
|
||||
}
|
||||
@@ -20,6 +20,10 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { ResearchModeAcknowledgmentDialog } from "@/components/medications/ResearchModeAcknowledgmentDialog";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import {
|
||||
type ResearchModeStatus,
|
||||
researchModeGateState,
|
||||
} from "@/lib/medications/research-mode-types";
|
||||
|
||||
/**
|
||||
* Settings → Advanced.
|
||||
@@ -67,11 +71,27 @@ export function AdvancedSection() {
|
||||
);
|
||||
}
|
||||
|
||||
interface ResearchModeStatus {
|
||||
enabled: boolean;
|
||||
acknowledgedAt: string | null;
|
||||
acknowledgedVersion: string | null;
|
||||
currentDisclaimerVersion: string;
|
||||
/**
|
||||
* v1.4.25 W21 Fix-N (simp-H3) — pull the nested ternary that decided
|
||||
* the toggle's caption out of the JSX. The four-way ladder (loading /
|
||||
* enabled-open / enabled-stale / disabled) is dense to read inline,
|
||||
* and the surface stays open to a fifth state ("never queried") when
|
||||
* we wire the offline shell in v1.4.26.
|
||||
*/
|
||||
function researchModeStatusLabel(
|
||||
status: ResearchModeStatus | null | undefined,
|
||||
isLoading: boolean,
|
||||
t: (key: string, params?: Record<string, string | number>) => string,
|
||||
): string {
|
||||
if (isLoading) return t("common.loading");
|
||||
const gate = researchModeGateState(status);
|
||||
if (gate === "off") return t("settings.researchMode.disabledStatus");
|
||||
if (gate === "stale") return t("settings.researchMode.enabledStaleStatus");
|
||||
return t("settings.researchMode.acknowledgedOn", {
|
||||
date: status?.acknowledgedAt
|
||||
? formatDateTime(status.acknowledgedAt)
|
||||
: "—",
|
||||
});
|
||||
}
|
||||
|
||||
function ResearchModeCard() {
|
||||
@@ -92,10 +112,8 @@ function ResearchModeCard() {
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
const versionsAligned =
|
||||
!!status &&
|
||||
status.acknowledgedVersion === status.currentDisclaimerVersion;
|
||||
const showRePrompt = !!status?.enabled && !versionsAligned;
|
||||
const gateState = researchModeGateState(status);
|
||||
const showRePrompt = gateState === "stale";
|
||||
// The Switch's `checked` mirrors the server flag exactly — even when
|
||||
// the version is stale we keep the toggle "on" so the user sees that
|
||||
// their previous choice is preserved; the banner above the toggle
|
||||
@@ -178,17 +196,7 @@ function ResearchModeCard() {
|
||||
className="text-muted-foreground text-xs"
|
||||
data-slot="settings-research-mode-status"
|
||||
>
|
||||
{isLoading
|
||||
? t("common.loading")
|
||||
: status?.enabled
|
||||
? versionsAligned
|
||||
? t("settings.researchMode.acknowledgedOn", {
|
||||
date: status.acknowledgedAt
|
||||
? formatDateTime(status.acknowledgedAt)
|
||||
: "—",
|
||||
})
|
||||
: t("settings.researchMode.enabledStaleStatus")
|
||||
: t("settings.researchMode.disabledStatus")}
|
||||
{researchModeStatusLabel(status, isLoading, t)}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
|
||||
Reference in new issue
Block a user