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