mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
Add the setup flow's welcome screen and shell
The front door reads the same ledger every screen reads: a record that never entered the flow sees the welcome, one mid-way is sent to the step it owes, a finished one is offered the questions again. The shell prints the counter and an ordered step list off the step machine, so it shows exactly the screens this flow has, and every screen moves focus to its heading on arrival. "Skip for now" is a real exit — it stamps the account past the first-run redirect and opens the checklist on the dashboard — and the one-time medical disclaimer now gates it as well as "Set up", because a skip that reached the dashboard without it would be a way round the acknowledgment. The five-step wizard's welcome carousel, goal picker and source grid go with it; the numbered step route follows in the next commit.
This commit is contained in:
1 parent
13f11ed06c
commit
1c2e7c29de
19 files changed
+743
-1527
No files matched your search
@@ -360,6 +360,11 @@ describe("canVisitScreen", () => {
|
||||
expect(canVisitScreen(midway, "done")).toBe(false);
|
||||
});
|
||||
|
||||
it("always admits the first question, which is where the welcome screen goes", () => {
|
||||
expect(canVisitScreen(state(), "who")).toBe(true);
|
||||
expect(canVisitScreen(state(), "areas")).toBe(false);
|
||||
});
|
||||
|
||||
it("refuses a screen the order does not contain", () => {
|
||||
const noUnits = state(QUESTIONS_DONE, {
|
||||
recordTarget: "me",
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* The getting-started checklist's per-browser state.
|
||||
*
|
||||
* Three keys, all of them conveniences rather than record: which rows this
|
||||
* browser hid, whether the whole card was hidden, and whether it is open.
|
||||
* They were private to the card until the setup flow needed two of them —
|
||||
* the done screen and "skip for now" open the list, and Settings brings a
|
||||
* hidden list back — and a second copy of a storage key is how two surfaces
|
||||
* end up writing different ones.
|
||||
*
|
||||
* Every access is wrapped: storage can be full, disabled, or absent, and the
|
||||
* card renders correctly with no stored value.
|
||||
*/
|
||||
export const CHECKLIST_DISMISSED_ITEMS_KEY =
|
||||
"healthlog-getting-started-dismissed";
|
||||
export const CHECKLIST_DISMISSED_ALL_KEY = "healthlog-getting-started-hidden";
|
||||
export const CHECKLIST_EXPANDED_KEY = "healthlog-getting-started-expanded";
|
||||
|
||||
function storage(): Storage | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
return window.localStorage;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Open the checklist on the next dashboard visit. */
|
||||
export function markChecklistExpanded(): void {
|
||||
try {
|
||||
storage()?.setItem(CHECKLIST_EXPANDED_KEY, "1");
|
||||
} catch {
|
||||
/* storage may be full or disabled */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "Show the checklist again": forget every dismissal and open the list.
|
||||
* Returns whether anything was stored to forget, so the caller can say
|
||||
* "it is back" rather than "done" to somebody who never hid it.
|
||||
*/
|
||||
export function resetChecklistDismissals(): boolean {
|
||||
const store = storage();
|
||||
if (!store) return false;
|
||||
try {
|
||||
const hadState =
|
||||
store.getItem(CHECKLIST_DISMISSED_ALL_KEY) === "1" ||
|
||||
(store.getItem(CHECKLIST_DISMISSED_ITEMS_KEY) ?? "[]") !== "[]";
|
||||
store.removeItem(CHECKLIST_DISMISSED_ITEMS_KEY);
|
||||
store.removeItem(CHECKLIST_DISMISSED_ALL_KEY);
|
||||
store.setItem(CHECKLIST_EXPANDED_KEY, "1");
|
||||
return hadState;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* v1.39 (C2) — the setup flow's state, for the server-rendered pages.
|
||||
*
|
||||
* One read per request: the page that guards a URL and the metadata that
|
||||
* titles it both ask for the same row, and `cache` makes the second ask free.
|
||||
* The DTO is the same one `GET /api/auth/me` publishes (`toOnboardingStateDto`
|
||||
* with the account's own unit columns), so the screen the server paints and
|
||||
* the screen the client reasons about after hydration are built from one
|
||||
* shape.
|
||||
*
|
||||
* The caller's OWN record, always. The flow's three writes refuse under an
|
||||
* acting-account switch, so a page that read the switched record's state would
|
||||
* show questions nobody could answer.
|
||||
*/
|
||||
import { cache } from "react";
|
||||
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { readHeldUnitPreferences, type OnboardingStateDto } from "./needs";
|
||||
import { loadOnboardingRecordRow, toOnboardingStateDto } from "./needs-store";
|
||||
|
||||
export interface OnboardingFlowPageState {
|
||||
userId: string;
|
||||
userLocale: string | null;
|
||||
state: OnboardingStateDto;
|
||||
}
|
||||
|
||||
export const loadOnboardingFlowState = cache(
|
||||
async (): Promise<OnboardingFlowPageState | null> => {
|
||||
const session = await getSession();
|
||||
if (!session) return null;
|
||||
const { user } = session;
|
||||
const row = await loadOnboardingRecordRow(prisma, user.id);
|
||||
return {
|
||||
userId: user.id,
|
||||
userLocale: user.locale ?? null,
|
||||
state: toOnboardingStateDto(row, readHeldUnitPreferences(user)),
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -211,6 +211,10 @@ export function previousScreen(
|
||||
* actually is. A screen the order does not contain (units for someone who
|
||||
* ticked no unit-bearing area) is refused too, so a stale link cannot show
|
||||
* a question whose answer would mean nothing.
|
||||
*
|
||||
* The first question is the one exception: it is where the welcome screen's
|
||||
* "Set up" goes, and a record that has not entered the flow resumes on the
|
||||
* welcome, so without this arm nobody could ever start.
|
||||
*/
|
||||
export function canVisitScreen(
|
||||
state: FlowState,
|
||||
@@ -219,7 +223,7 @@ export function canVisitScreen(
|
||||
const order = screenOrder(state);
|
||||
const index = order.indexOf(screen);
|
||||
if (index === -1) return false;
|
||||
return index <= order.indexOf(resumeScreen(state));
|
||||
return index <= Math.max(order.indexOf(resumeScreen(state)), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user