fix(design): pin acknowledgment dialog footer + i18n back-button + drop GoalsChipPicker storage block

Three small surface fixes consolidated because each is a one-touch
change on a separate file:

- `ResearchModeAcknowledgmentDialog`: the entire `DialogContent`
  scrolled at `max-h-[90vh] overflow-y-auto`, which pushed the
  Acknowledge / Cancel CTAs below the fold on phone-class viewports.
  Move scrolling onto the body section only so the header and footer
  stay pinned.
- Medication history page back-button rendered the literal "Zurück"
  instead of going through `t()`. Wire the new `medications.back`
  key across all six locale bundles.
- `GoalsChipPicker` shipped a localStorage persistence block scoped
  by user id, but no consumer reads the same key. Drop the storage
  layer (and the unused `goalsStorageKey` /
  `ONBOARDING_GOALS_STORAGE_PREFIX` exports) until v1.4.26 lands the
  `User.onboardingGoals` column the picker is meant to feed.
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 22:30:58 +02:00
1 parent 7c9b4b6563
commit a238ae48d1
9 files changed
+29 -64

No files matched your search

+1
View File
@@ -444,6 +444,7 @@
"daysFri": "Fr",
"daysSat": "Sa",
"intakeHistoryTitle": "Einnahme-Verlauf",
"back": "Zurück",
"newIntake": "Hinzufügen",
"editIntake": "Einnahme bearbeiten",
"intakeScheduledFor": "Geplant",
+1
View File
@@ -444,6 +444,7 @@
"daysFri": "Fri",
"daysSat": "Sat",
"intakeHistoryTitle": "Intake History",
"back": "Back",
"newIntake": "Add",
"editIntake": "Edit Intake",
"intakeScheduledFor": "Scheduled",
+1
View File
@@ -444,6 +444,7 @@
"daysFri": "Fri",
"daysSat": "Sat",
"intakeHistoryTitle": "Intake History",
"back": "Atrás",
"newIntake": "Añadir",
"editIntake": "Edit Intake",
"intakeScheduledFor": "Scheduled",
+1
View File
@@ -444,6 +444,7 @@
"daysFri": "Fri",
"daysSat": "Sat",
"intakeHistoryTitle": "Intake History",
"back": "Retour",
"newIntake": "Ajouter",
"editIntake": "Edit Intake",
"intakeScheduledFor": "Scheduled",
+1
View File
@@ -444,6 +444,7 @@
"daysFri": "Fri",
"daysSat": "Sat",
"intakeHistoryTitle": "Intake History",
"back": "Indietro",
"newIntake": "Aggiungi",
"editIntake": "Edit Intake",
"intakeScheduledFor": "Scheduled",
+1
View File
@@ -444,6 +444,7 @@
"daysFri": "Fri",
"daysSat": "Sat",
"intakeHistoryTitle": "Intake History",
"back": "Wstecz",
"newIntake": "Dodaj",
"editIntake": "Edit Intake",
"intakeScheduledFor": "Scheduled",
+1 -1
View File
@@ -66,7 +66,7 @@ export default function IntakeHistoryPage({
>
<Link href="/medications">
<ArrowLeft className="h-4 w-4" />
Zurück
{t("medications.back")}
</Link>
</Button>
@@ -134,7 +134,7 @@ export function ResearchModeAcknowledgmentDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
data-slot="research-mode-acknowledgment-dialog"
className="max-h-[90vh] overflow-y-auto sm:max-w-xl"
className="flex max-h-[90vh] flex-col sm:max-w-xl"
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
@@ -146,7 +146,12 @@ export function ResearchModeAcknowledgmentDialog({
</DialogDescription>
</DialogHeader>
<div className="space-y-4 text-sm">
{/* Only the body scrolls; header + footer stay pinned so the
Acknowledge / Cancel CTAs are always reachable without
scrolling on small viewports (the previous markup scrolled
the entire DialogContent, pushing the footer below the
fold on iPhone-class heights). */}
<div className="-mr-2 flex-1 space-y-4 overflow-y-auto pr-2 text-sm">
<section
aria-labelledby="research-mode-what-it-is"
data-slot="research-mode-what-it-is"
+15 -61
View File
@@ -1,6 +1,6 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
@@ -23,15 +23,14 @@ import { cn } from "@/lib/utils";
* v1.4.25 W14b-Content — onboarding step 1 (goals).
*
* Multi-select chip picker for the metrics the user wants to track.
* The goal slugs match a stable enum (used by the eventual
* `User.onboardingGoals` column in v1.4.26 — for now we hold the
* selection in client state and bundle it into the final-step submit).
*
* Persistence — per the W14b-Content brief — happens in localStorage
* scoped by `user.id` (mirrors the family-shared-device pattern from
* `tour-launcher.tsx:59–65`). The Welcome carousel doesn't write here,
* so a user who closes the tab on step 1 sees their previous choice
* pre-selected when they resume.
* The goal slugs match a stable enum that v1.4.26 will land on the
* `User` row (`User.onboardingGoals`). Until that column ships the
* picker holds selection in component state only — there's no
* persistence layer to drop into. The earlier localStorage block
* had no consumer (no other surface reads the same key) and the
* single-step wizard makes pre-selection on resume unnecessary, so
* leaning on component state until the column lands keeps the code
* surface honest.
*
* CTAs:
* - Back → `/onboarding/0`
@@ -42,8 +41,6 @@ import { cn } from "@/lib/utils";
* and `router.push("/onboarding/2")`.
*/
export const ONBOARDING_GOALS_STORAGE_PREFIX = "healthlog.onboarding.goals";
export type OnboardingGoalSlug =
| "weight-management"
| "bp-tracking"
@@ -91,52 +88,23 @@ const GOAL_OPTIONS: ReadonlyArray<GoalOption> = [
},
];
export function goalsStorageKey(userId: string): string {
return `${ONBOARDING_GOALS_STORAGE_PREFIX}:${userId}`;
}
function readGoalsFromStorage(
storageKey: string,
): Set<OnboardingGoalSlug> {
if (typeof window === "undefined") return new Set();
try {
const raw = window.localStorage.getItem(storageKey);
if (!raw) return new Set();
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed)) return new Set();
const allowed = new Set<string>(GOAL_OPTIONS.map((o) => o.slug));
const out = new Set<OnboardingGoalSlug>();
for (const v of parsed) {
if (typeof v === "string" && allowed.has(v)) {
out.add(v as OnboardingGoalSlug);
}
}
return out;
} catch {
/* corrupt entry — ignore */
return new Set();
}
}
export interface GoalsChipPickerProps {
/**
* The current user's id. Threaded in from the server step page
* (`getSession().user.id`) so the localStorage hydration can run
* synchronously in the state initializer and avoid the
* `react-hooks/set-state-in-effect` cascading-render anti-pattern.
* (`getSession().user.id`) so the v1.4.26 column write can reuse
* the same call site without a prop reshuffle.
*/
userId: string;
}
export function GoalsChipPicker({ userId }: GoalsChipPickerProps) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function GoalsChipPicker({ userId: _userId }: GoalsChipPickerProps) {
const { t } = useTranslations();
const router = useRouter();
const queryClient = useQueryClient();
const storageKey = goalsStorageKey(userId);
const [selected, setSelected] = useState<Set<OnboardingGoalSlug>>(() =>
readGoalsFromStorage(storageKey),
const [selected, setSelected] = useState<Set<OnboardingGoalSlug>>(
() => new Set(),
);
const [advancing, setAdvancing] = useState(false);
@@ -149,20 +117,6 @@ export function GoalsChipPicker({ userId }: GoalsChipPickerProps) {
});
}, []);
// Persist on every change. localStorage is cheap and the goal set is
// tiny — no need for a debounced effect here.
useEffect(() => {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(
storageKey,
JSON.stringify(Array.from(selected)),
);
} catch {
/* quota or disabled — best-effort */
}
}, [storageKey, selected]);
async function advance() {
if (advancing) return;
setAdvancing(true);