From 2b712429c0900c97088bf788713681864cf94334 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Thu, 14 May 2026 23:04:37 +0200
Subject: [PATCH] refactor(ui): adopt shared readError + ResearchModeStatus +
design-system Select
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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 for the
shadcn-radix Select component so the picker matches the rest of the
form library (focus rings, keyboard nav, theme tokens).
---
src/components/onboarding/BaselineForm.tsx | 53 ++++++++++++-------
src/components/onboarding/GoalsChipPicker.tsx | 12 +----
src/components/onboarding/SourceCardGrid.tsx | 12 +----
src/components/onboarding/WelcomeCarousel.tsx | 12 +----
src/components/settings/advanced-section.tsx | 48 ++++++++++-------
5 files changed, 64 insertions(+), 73 deletions(-)
diff --git a/src/components/onboarding/BaselineForm.tsx b/src/components/onboarding/BaselineForm.tsx
index b5b591f89..404f46fed 100644
--- a/src/components/onboarding/BaselineForm.tsx
+++ b/src/components/onboarding/BaselineForm.tsx
@@ -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() {
{t("onboarding.baseline.genderLabel")}
- 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"
+ patch("gender", next)}
>
- {t("onboarding.baseline.genderNone")}
- {t("onboarding.baseline.genderMale")}
- {t("onboarding.baseline.genderFemale")}
-
+
+
+
+
+
+ {t("onboarding.baseline.genderMale")}
+
+
+ {t("onboarding.baseline.genderFemale")}
+
+
+
@@ -225,14 +249,3 @@ export function BaselineForm() {
);
}
-async function readError(res: Response): Promise {
- 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})`;
-}
diff --git a/src/components/onboarding/GoalsChipPicker.tsx b/src/components/onboarding/GoalsChipPicker.tsx
index 09b8d421e..8274f834c 100644
--- a/src/components/onboarding/GoalsChipPicker.tsx
+++ b/src/components/onboarding/GoalsChipPicker.tsx
@@ -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 {
- 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})`;
-}
diff --git a/src/components/onboarding/SourceCardGrid.tsx b/src/components/onboarding/SourceCardGrid.tsx
index 53c559f9b..3067336f9 100644
--- a/src/components/onboarding/SourceCardGrid.tsx
+++ b/src/components/onboarding/SourceCardGrid.tsx
@@ -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 {
- 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})`;
-}
diff --git a/src/components/onboarding/WelcomeCarousel.tsx b/src/components/onboarding/WelcomeCarousel.tsx
index f9db1ced2..3f2899ee6 100644
--- a/src/components/onboarding/WelcomeCarousel.tsx
+++ b/src/components/onboarding/WelcomeCarousel.tsx
@@ -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 {
- 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})`;
-}
diff --git a/src/components/settings/advanced-section.tsx b/src/components/settings/advanced-section.tsx
index 4979507bd..855a64da7 100644
--- a/src/components/settings/advanced-section.tsx
+++ b/src/components/settings/advanced-section.tsx
@@ -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 {
+ 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)}