From 2d79b80298a2d13846259c42e4e7c743a1f35139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 8 Oct 2026 20:41:50 +0200 Subject: [PATCH] Give the account page's save-on-change preferences their own card Language, units, glucose unit and the hour and date formats each save the moment they change, yet sat inside the profile form as if its Save covered them. They move to a Display card with no save button; the profile card keeps its fields, the timezone (which saves with the form) and its Save. --- messages/de.json | 6 +- messages/en.json | 6 +- messages/es.json | 6 +- messages/fr.json | 6 +- messages/it.json | 6 +- messages/ko.json | 6 +- messages/pl.json | 6 +- .../display-preferences-card.test.tsx | 53 +++++++++ .../settings/account-section/index.tsx | 109 ++++++++++-------- 9 files changed, 152 insertions(+), 52 deletions(-) create mode 100644 src/components/settings/account-section/__tests__/display-preferences-card.test.tsx diff --git a/messages/de.json b/messages/de.json index 905b13f71..2baa2f32e 100644 --- a/messages/de.json +++ b/messages/de.json @@ -7173,7 +7173,11 @@ "failed": "Das hat nicht geklappt. Prüfe deine Eingabe und versuche es noch einmal." }, "passwordTitle": "Passwort", - "tokenNameLabel": "Token-Name" + "tokenNameLabel": "Token-Name", + "displayCard": { + "title": "Darstellung", + "description": "Sprache, Einheiten und Formate, gespeichert, sobald du sie änderst." + } }, "admin": { "title": "Administration", diff --git a/messages/en.json b/messages/en.json index 158470039..3f2d82afa 100644 --- a/messages/en.json +++ b/messages/en.json @@ -7173,7 +7173,11 @@ "failed": "That did not work. Check what you entered and try again." }, "passwordTitle": "Password", - "tokenNameLabel": "Token name" + "tokenNameLabel": "Token name", + "displayCard": { + "title": "Display", + "description": "Language, units and formats, saved as soon as you change them." + } }, "admin": { "title": "Administration", diff --git a/messages/es.json b/messages/es.json index c1041cb78..be3c7baf3 100644 --- a/messages/es.json +++ b/messages/es.json @@ -7173,7 +7173,11 @@ "failed": "No ha funcionado. Revisa lo que has introducido e inténtalo de nuevo." }, "passwordTitle": "Contraseña", - "tokenNameLabel": "Nombre del token" + "tokenNameLabel": "Nombre del token", + "displayCard": { + "title": "Visualización", + "description": "Idioma, unidades y formatos; se guardan en cuanto los cambias." + } }, "admin": { "title": "Administration", diff --git a/messages/fr.json b/messages/fr.json index c9d7eb79f..b6cf1a77b 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -7173,7 +7173,11 @@ "failed": "Cela n'a pas fonctionné. Vérifiez votre saisie et réessayez." }, "passwordTitle": "Mot de passe", - "tokenNameLabel": "Nom du jeton" + "tokenNameLabel": "Nom du jeton", + "displayCard": { + "title": "Affichage", + "description": "Langue, unités et formats, enregistrés dès que vous les modifiez." + } }, "admin": { "title": "Administration", diff --git a/messages/it.json b/messages/it.json index d7d68ce9d..d3a70449a 100644 --- a/messages/it.json +++ b/messages/it.json @@ -7173,7 +7173,11 @@ "failed": "Non ha funzionato. Controlla quanto inserito e riprova." }, "passwordTitle": "Password", - "tokenNameLabel": "Nome del token" + "tokenNameLabel": "Nome del token", + "displayCard": { + "title": "Visualizzazione", + "description": "Lingua, unità e formati, salvati appena li modifichi." + } }, "admin": { "title": "Administration", diff --git a/messages/ko.json b/messages/ko.json index cbf9cbc05..8c101e880 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -7173,7 +7173,11 @@ "failed": "확인하지 못했습니다. 입력한 내용을 확인하고 다시 시도하세요." }, "passwordTitle": "비밀번호", - "tokenNameLabel": "토큰 이름" + "tokenNameLabel": "토큰 이름", + "displayCard": { + "title": "표시", + "description": "언어, 단위, 형식은 바꾸는 즉시 저장돼요." + } }, "admin": { "title": "관리자", diff --git a/messages/pl.json b/messages/pl.json index f682b317e..d78363ee2 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -7173,7 +7173,11 @@ "failed": "To się nie udało. Sprawdź wprowadzone dane i spróbuj ponownie." }, "passwordTitle": "Hasło", - "tokenNameLabel": "Nazwa tokena" + "tokenNameLabel": "Nazwa tokena", + "displayCard": { + "title": "Wyświetlanie", + "description": "Język, jednostki i formaty zapisywane od razu po zmianie." + } }, "admin": { "title": "Administracja", diff --git a/src/components/settings/account-section/__tests__/display-preferences-card.test.tsx b/src/components/settings/account-section/__tests__/display-preferences-card.test.tsx new file mode 100644 index 000000000..c9e9a88a2 --- /dev/null +++ b/src/components/settings/account-section/__tests__/display-preferences-card.test.tsx @@ -0,0 +1,53 @@ +/** + * Settings → Account: the controls that save on change (language, units, + * glucose unit, hour and date format) sit in their own card with no save + * button, apart from the profile form whose Save does not cover them. + */ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { renderToStaticMarkup } from "react-dom/server"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { describe, expect, it } from "vitest"; + +import { I18nProvider } from "@/lib/i18n/context"; +import { DisplayPreferencesCard } from "../index"; + +function render(): string { + const client = new QueryClient({ + defaultOptions: { queries: { retry: 0, enabled: false } }, + }); + return renderToStaticMarkup( + + + + + , + ); +} + +describe("Settings → Account display card", () => { + it("holds the language select and carries no form or save button", () => { + const html = render(); + expect(html).toContain('data-testid="settings-display-card"'); + expect(html).toContain('id="language-select"'); + expect(html).not.toContain(" { + const src = readFileSync(join(__dirname, "../index.tsx"), "utf8"); + const form = src.slice(src.indexOf("")); + expect(form.length).toBeGreaterThan(0); + for (const control of [ + "language-select", + "UnitPreferenceSelect", + "GlucoseUnitSelect", + "TimeFormatSelect", + "DateFormatSelect", + ]) { + expect(form).not.toContain(control); + } + // The timezone saves with the form, so it stays inside it. + expect(form).toContain("TimezonePicker"); + }); +}); diff --git a/src/components/settings/account-section/index.tsx b/src/components/settings/account-section/index.tsx index a4cde390e..ab8604334 100644 --- a/src/components/settings/account-section/index.tsx +++ b/src/components/settings/account-section/index.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; -import { Loader2, Save, User } from "lucide-react"; +import { Languages, Loader2, Save, User } from "lucide-react"; import { Button } from "@/components/ui/button"; import { SettingsCardActions } from "@/components/settings/_card-actions"; @@ -62,7 +62,7 @@ import { dateOnlyKey } from "@/lib/tz/date-only"; export { resolveInitialTimezone } from "./account-section-utils"; export function AccountSection() { - const { t, locale, setLocale, pendingLocale } = useTranslations(); + const { t } = useTranslations(); const { user, isLoading, isAuthenticated, refetch } = useAuth(); // v1.16.4 — see `useMounted`: keeps the hydration render identical to // the SSR HTML when this boundary hydrates after `/api/auth/me` @@ -517,12 +517,10 @@ export function AccountSection() { - {/* Date of birth + language share one paired grid row so the - profile form keeps a single rhythm (every row two cells - wide on sm+). Date of birth is the bottom of the - biological-profile block; language is the only UI - preference on this card. They sit together to close the - "single-cell row" gap that broke the form's grid. */} + {/* Date of birth + timezone share one paired grid row. The + timezone stays on this card because it saves with the form + (its own PUT runs beside the profile PUT); the display + preferences that save on change live in their own card below. */}
@@ -544,44 +542,7 @@ export function AccountSection() {

-
- - {/* While a switch waits on its message bundle (dynamic - import per locale), show the target value and lock the - control — the context only flips locale + strings - together once the bundle arrived. */} - setLocale(e.target.value as Locale)} - > - {locales.map((loc) => ( - - ))} - -

- {t("settings.languageDescription")} -

-
-
- - {/* Timezone, unit system, glucose unit and the date/hour formats - share one grid block — all personal display preferences (like - language above). The glucose unit is its own dropdown rather - than a branch of the unit system because metric countries are - split on which unit they read glucose in. Every dropdown here - PATCHes its own endpoint on change; the timezone saves through - the form's submit handler. */} -
- - - -
{/* v1.7.0 — optional patient-identity fields surfaced on the @@ -702,6 +663,8 @@ export function AccountSection() { {recentProof.dialog} + + {/* v1.18.0 (S5) — injection-site exclusions moved to the dedicated Medikamente settings section, where every medication-specific preference now lives. */} @@ -734,3 +697,59 @@ export function AccountSection() { ); } + +/** + * Language, units and formats: every control here saves the moment it + * changes (each PATCHes its own endpoint), so they sit in a card with no save + * button. Inside the profile form they read as if the form's Save covered + * them, and a person who changed the language and then cancelled the form + * found it changed anyway. + */ +export function DisplayPreferencesCard({ + isAuthenticated, +}: { + isAuthenticated: boolean; +}) { + const { t, locale, setLocale, pendingLocale } = useTranslations(); + return ( + + + {/* The glucose unit is its own dropdown rather than a branch of the + unit system because metric countries are split on which unit they + read glucose in. */} +
+
+ + {/* While a switch waits on its message bundle (dynamic import per + locale), show the target value and lock the control — the + context only flips locale + strings together once the bundle + arrived. */} + setLocale(e.target.value as Locale)} + > + {locales.map((loc) => ( + + ))} + +

+ {t("settings.languageDescription")} +

+
+ + + + +
+
+ ); +}