diff --git a/src/app/onboarding/[step]/page.tsx b/src/app/onboarding/[step]/page.tsx new file mode 100644 index 000000000..596584632 --- /dev/null +++ b/src/app/onboarding/[step]/page.tsx @@ -0,0 +1,155 @@ +import { redirect, notFound } from "next/navigation"; + +import { OnboardingShell } from "@/components/onboarding/OnboardingShell"; +import { getSession } from "@/lib/auth/session"; +import { getServerTranslator } from "@/lib/i18n/server-translator"; +import { resolveServerLocale } from "@/lib/i18n/server-locale"; + +/** + * v1.4.25 W14b — onboarding wizard step page (foundation scaffold). + * + * Routes: + * /onboarding/0 welcome → carousel / value-prop intro + * /onboarding/1 goals → "what do you want to track?" + * /onboarding/2 source → Withings / Apple Health / manual cards + * /onboarding/3 baseline → first measurement or sync confirmation + * /onboarding/4 done → success screen + return to dashboard + * + * This page ships the scaffold only — the step body is a placeholder + * `
` that the W14b-Content agent replaces with real step UI. + * + * Step gating: + * - Unauthenticated → `/auth/login` (the proxy also enforces this, + * but the server-side check keeps the contract explicit). + * - `onboardingCompletedAt != null` → `/` (no replaying the wizard + * by URL once the user has finished). + * - Out-of-order requests (e.g. user lands on `/onboarding/3` while + * `onboardingStep == 1`) redirect to the user's current step. The + * "current step" is `User.onboardingStep ?? 0`. + * + * The user MAY navigate backwards (e.g. `/onboarding/0` while on step + * 2). Backwards navigation is non-destructive — the shell's "Back" + * button uses it. Only forward jumps are blocked. + */ + +const VALID_STEPS = [0, 1, 2, 3, 4] as const; +type Step = (typeof VALID_STEPS)[number]; + +export const dynamicParams = false; + +export function generateStaticParams() { + return VALID_STEPS.map((step) => ({ step: String(step) })); +} + +interface PageProps { + params: Promise<{ step: string }>; +} + +export default async function OnboardingStepPage({ params }: PageProps) { + const { step: stepParam } = await params; + const parsedStep = Number.parseInt(stepParam, 10); + + if (!Number.isFinite(parsedStep) || !VALID_STEPS.includes(parsedStep as Step)) { + notFound(); + } + const requested = parsedStep as Step; + + const session = await getSession(); + if (!session) { + redirect("/auth/login"); + } + const { user } = session; + + if (user.onboardingCompletedAt) { + redirect("/"); + } + + const current = clampCurrentStep(user.onboardingStep); + if (requested > current) { + redirect(`/onboarding/${current}`); + } + + const locale = await resolveServerLocale({ userLocale: user.locale }); + const { t } = getServerTranslator(locale); + + // Foundation scaffold body — placeholder per-step copy. The Content + // agent replaces each branch's body with the real carousel / + // goal-chips / source-cards / baseline-form / done-screen UI. + // + // Welcome copy lives under `onboarding.shell.welcome{Title,Body}` + // because the carousel is part of the chrome; every other step + // owns its own namespace (`onboarding.goals.*`, `.source.*`, + // `.baseline.*`, `.done.*`). + const bodyTitle = + requested === 0 + ? t("onboarding.shell.welcomeTitle") + : t(`onboarding.${stepKey(requested)}.title`); + const bodyText = + requested === 0 + ? t("onboarding.shell.welcomeBody") + : t(`onboarding.${stepKey(requested)}.body`); + + // Wire shell navigation by step. Step 0 has no Back; the wizard + // never lets the user navigate forward past their current step, but + // they can advance from the current step's "Next" button — the + // POST /api/onboarding/step mutation flips `onboardingStep` and the + // Content agent will redirect to the new step on success. The + // Foundation scaffold uses plain hrefs so the shell renders end-to- + // end immediately; the Content agent will replace the next link + // with an action button bound to the API. + const backHref = requested > 0 ? `/onboarding/${requested - 1}` : undefined; + const skipHref = requested > 0 && requested < 4 ? "/" : undefined; + const nextHref = + requested < 4 ? `/onboarding/${requested + 1}` : "/"; + + return ( + +
+

+ {bodyTitle} +

+

+ {bodyText} +

+
+
+
+ ); +} + +function clampCurrentStep(value: number | null | undefined): Step { + if (value == null || !Number.isFinite(value)) return 0; + const floor = Math.floor(value); + if (floor <= 0) return 0; + if (floor >= 4) return 4; + return floor as Step; +} + +function stepKey(step: Step): "welcome" | "goals" | "source" | "baseline" | "done" { + switch (step) { + case 0: + return "welcome"; + case 1: + return "goals"; + case 2: + return "source"; + case 3: + return "baseline"; + case 4: + return "done"; + } +} diff --git a/src/app/onboarding/layout.tsx b/src/app/onboarding/layout.tsx new file mode 100644 index 000000000..7e2daca90 --- /dev/null +++ b/src/app/onboarding/layout.tsx @@ -0,0 +1,15 @@ +import type { ReactNode } from "react"; + +/** + * v1.4.25 W14b — onboarding route-segment layout. + * + * Intentionally pass-through. The `OnboardingShell` chrome lives inside + * each `[step]/page.tsx` so the existing `/onboarding` root page + * (`page.tsx` — the legacy v1.4.20 single-file wizard) keeps rendering + * unchanged while the rebuilt multi-step flow ships beside it. The + * Content agent that follows W14b-Foundation will swap the root page's + * redirect target once every step has real content. + */ +export default function OnboardingLayout({ children }: { children: ReactNode }) { + return
{children}
; +}