mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
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:
1 parent
7c9b4b6563
commit
a238ae48d1
9 files changed
+29
-64
No files matched your search
@@ -444,6 +444,7 @@
|
||||
"daysFri": "Fr",
|
||||
"daysSat": "Sa",
|
||||
"intakeHistoryTitle": "Einnahme-Verlauf",
|
||||
"back": "Zurück",
|
||||
"newIntake": "Hinzufügen",
|
||||
"editIntake": "Einnahme bearbeiten",
|
||||
"intakeScheduledFor": "Geplant",
|
||||
|
||||
@@ -444,6 +444,7 @@
|
||||
"daysFri": "Fri",
|
||||
"daysSat": "Sat",
|
||||
"intakeHistoryTitle": "Intake History",
|
||||
"back": "Back",
|
||||
"newIntake": "Add",
|
||||
"editIntake": "Edit Intake",
|
||||
"intakeScheduledFor": "Scheduled",
|
||||
|
||||
@@ -444,6 +444,7 @@
|
||||
"daysFri": "Fri",
|
||||
"daysSat": "Sat",
|
||||
"intakeHistoryTitle": "Intake History",
|
||||
"back": "Atrás",
|
||||
"newIntake": "Añadir",
|
||||
"editIntake": "Edit Intake",
|
||||
"intakeScheduledFor": "Scheduled",
|
||||
|
||||
@@ -444,6 +444,7 @@
|
||||
"daysFri": "Fri",
|
||||
"daysSat": "Sat",
|
||||
"intakeHistoryTitle": "Intake History",
|
||||
"back": "Retour",
|
||||
"newIntake": "Ajouter",
|
||||
"editIntake": "Edit Intake",
|
||||
"intakeScheduledFor": "Scheduled",
|
||||
|
||||
@@ -444,6 +444,7 @@
|
||||
"daysFri": "Fri",
|
||||
"daysSat": "Sat",
|
||||
"intakeHistoryTitle": "Intake History",
|
||||
"back": "Indietro",
|
||||
"newIntake": "Aggiungi",
|
||||
"editIntake": "Edit Intake",
|
||||
"intakeScheduledFor": "Scheduled",
|
||||
|
||||
@@ -444,6 +444,7 @@
|
||||
"daysFri": "Fri",
|
||||
"daysSat": "Sat",
|
||||
"intakeHistoryTitle": "Intake History",
|
||||
"back": "Wstecz",
|
||||
"newIntake": "Dodaj",
|
||||
"editIntake": "Edit Intake",
|
||||
"intakeScheduledFor": "Scheduled",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user