From fa6c5e4a9c7e58c4d7542a34f741158e07553292 Mon Sep 17 00:00:00 2001 From: Teksiak Date: Sun, 4 Oct 2026 05:29:18 +0200 Subject: [PATCH 01/16] feat(website): replace the demo with links into the web app The landing page no longer ships the clickable fake demo. Its calls to action open the real app instead: "Try it as a guest" goes to app/guest and "Sign in" to app/welcome, relative to the site root so they work under the GitHub Pages project path. VITE_APP_URL points them at a local dev server. Old #/try links forward to the guest entry. Co-Authored-By: Claude Opus 5.5 --- apps/website/README.md | 60 +++-- apps/website/index.html | 4 +- apps/website/src/App.tsx | 126 ++------- apps/website/src/a11y.ts | 35 +-- apps/website/src/appLinks.ts | 16 ++ .../src/components/chrome/DemoHeader.tsx | 32 --- .../src/components/chrome/SiteHeader.tsx | 20 +- apps/website/src/components/chrome/index.ts | 3 +- .../src/components/common/ChangeCard.tsx | 42 +-- apps/website/src/components/common/Dialog.tsx | 71 ----- .../src/components/common/ExerciseLine.tsx | 22 -- .../src/components/common/PlanStrip.tsx | 61 ----- .../src/components/common/StepDots.tsx | 4 +- .../src/components/common/WeekDots.tsx | 38 --- .../src/components/common/WeekList.tsx | 74 ------ apps/website/src/components/common/index.ts | 9 +- .../src/components/landing/ClosingBand.tsx | 13 +- apps/website/src/components/landing/Hero.tsx | 49 ++-- .../src/components/landing/Landing.tsx | 7 +- .../components/landing/mockups/MockAsk.tsx | 8 +- .../src/components/landing/mockups/data.ts | 2 +- apps/website/src/components/plan/Chat.tsx | 117 -------- .../src/components/plan/ChatMessage.tsx | 79 ------ .../src/components/plan/CompletedSession.tsx | 39 --- apps/website/src/components/plan/DayCard.tsx | 55 ---- .../src/components/plan/FeedbackForm.tsx | 68 ----- .../src/components/plan/OpenSession.tsx | 66 ----- apps/website/src/components/plan/PlanView.tsx | 149 ----------- .../src/components/plan/SessionDialog.tsx | 46 ---- .../src/components/plan/WhyThisPlan.tsx | 33 --- apps/website/src/components/plan/index.ts | 2 - .../src/components/planning/PlanningView.tsx | 101 ------- apps/website/src/components/planning/index.ts | 1 - .../src/components/steps/AnswersPanel.tsx | 80 ------ .../src/components/steps/ComfortStep.tsx | 38 --- .../src/components/steps/PlacesStep.tsx | 53 ---- .../src/components/steps/SliderField.tsx | 42 --- .../src/components/steps/SportStep.tsx | 85 ------ .../src/components/steps/StepShell.tsx | 89 ------- .../website/src/components/steps/StepView.tsx | 21 -- .../website/src/components/steps/TagGroup.tsx | 21 -- .../website/src/components/steps/TimeStep.tsx | 55 ---- apps/website/src/components/steps/content.ts | 7 - apps/website/src/components/steps/index.ts | 2 - apps/website/src/components/steps/types.ts | 20 -- apps/website/src/demo/redirects.ts | 28 -- apps/website/src/demo/reducer.ts | 242 ----------------- apps/website/src/demo/types.ts | 21 -- apps/website/src/demo/useDemo.ts | 115 -------- .../website/src/design-system/core/Button.tsx | 87 +++--- apps/website/src/domain/answers.ts | 50 ---- apps/website/src/domain/catalog.ts | 152 ----------- apps/website/src/domain/chat/effects.ts | 211 --------------- apps/website/src/domain/chat/patterns.ts | 65 ----- apps/website/src/domain/chat/pipeline.ts | 149 ----------- apps/website/src/domain/chat/revision.ts | 129 --------- apps/website/src/domain/chat/suggestions.ts | 46 ---- apps/website/src/domain/chat/types.ts | 65 ----- apps/website/src/domain/copy.ts | 209 --------------- apps/website/src/domain/dates.ts | 61 ----- apps/website/src/domain/index.ts | 30 +-- apps/website/src/domain/onboarding.ts | 23 ++ apps/website/src/domain/plan.ts | 251 ------------------ apps/website/src/domain/reasons.ts | 40 --- apps/website/src/domain/schedule.ts | 29 -- apps/website/src/domain/sessions/fitness.ts | 55 ---- apps/website/src/domain/sessions/football.ts | 68 ----- apps/website/src/domain/sessions/gym.ts | 41 --- apps/website/src/domain/sessions/index.ts | 36 --- apps/website/src/domain/sessions/running.ts | 84 ------ apps/website/src/domain/sessions/shared.ts | 60 ----- apps/website/src/domain/sessions/types.ts | 36 --- apps/website/src/domain/sports.ts | 93 ------- apps/website/src/domain/text.ts | 25 -- apps/website/src/hooks/useMediaQuery.ts | 6 +- apps/website/src/hooks/useRouteFocus.ts | 20 +- apps/website/src/routing.ts | 36 +-- apps/website/src/site.css | 173 +----------- apps/website/src/vite-env.d.ts | 12 + 79 files changed, 235 insertions(+), 4478 deletions(-) create mode 100644 apps/website/src/appLinks.ts delete mode 100644 apps/website/src/components/chrome/DemoHeader.tsx delete mode 100644 apps/website/src/components/common/Dialog.tsx delete mode 100644 apps/website/src/components/common/ExerciseLine.tsx delete mode 100644 apps/website/src/components/common/PlanStrip.tsx delete mode 100644 apps/website/src/components/common/WeekDots.tsx delete mode 100644 apps/website/src/components/common/WeekList.tsx delete mode 100644 apps/website/src/components/plan/Chat.tsx delete mode 100644 apps/website/src/components/plan/ChatMessage.tsx delete mode 100644 apps/website/src/components/plan/CompletedSession.tsx delete mode 100644 apps/website/src/components/plan/DayCard.tsx delete mode 100644 apps/website/src/components/plan/FeedbackForm.tsx delete mode 100644 apps/website/src/components/plan/OpenSession.tsx delete mode 100644 apps/website/src/components/plan/PlanView.tsx delete mode 100644 apps/website/src/components/plan/SessionDialog.tsx delete mode 100644 apps/website/src/components/plan/WhyThisPlan.tsx delete mode 100644 apps/website/src/components/plan/index.ts delete mode 100644 apps/website/src/components/planning/PlanningView.tsx delete mode 100644 apps/website/src/components/planning/index.ts delete mode 100644 apps/website/src/components/steps/AnswersPanel.tsx delete mode 100644 apps/website/src/components/steps/ComfortStep.tsx delete mode 100644 apps/website/src/components/steps/PlacesStep.tsx delete mode 100644 apps/website/src/components/steps/SliderField.tsx delete mode 100644 apps/website/src/components/steps/SportStep.tsx delete mode 100644 apps/website/src/components/steps/StepShell.tsx delete mode 100644 apps/website/src/components/steps/StepView.tsx delete mode 100644 apps/website/src/components/steps/TagGroup.tsx delete mode 100644 apps/website/src/components/steps/TimeStep.tsx delete mode 100644 apps/website/src/components/steps/content.ts delete mode 100644 apps/website/src/components/steps/index.ts delete mode 100644 apps/website/src/components/steps/types.ts delete mode 100644 apps/website/src/demo/redirects.ts delete mode 100644 apps/website/src/demo/reducer.ts delete mode 100644 apps/website/src/demo/types.ts delete mode 100644 apps/website/src/demo/useDemo.ts delete mode 100644 apps/website/src/domain/answers.ts delete mode 100644 apps/website/src/domain/catalog.ts delete mode 100644 apps/website/src/domain/chat/effects.ts delete mode 100644 apps/website/src/domain/chat/patterns.ts delete mode 100644 apps/website/src/domain/chat/pipeline.ts delete mode 100644 apps/website/src/domain/chat/revision.ts delete mode 100644 apps/website/src/domain/chat/suggestions.ts delete mode 100644 apps/website/src/domain/chat/types.ts delete mode 100644 apps/website/src/domain/copy.ts delete mode 100644 apps/website/src/domain/dates.ts create mode 100644 apps/website/src/domain/onboarding.ts delete mode 100644 apps/website/src/domain/plan.ts delete mode 100644 apps/website/src/domain/reasons.ts delete mode 100644 apps/website/src/domain/schedule.ts delete mode 100644 apps/website/src/domain/sessions/fitness.ts delete mode 100644 apps/website/src/domain/sessions/football.ts delete mode 100644 apps/website/src/domain/sessions/gym.ts delete mode 100644 apps/website/src/domain/sessions/index.ts delete mode 100644 apps/website/src/domain/sessions/running.ts delete mode 100644 apps/website/src/domain/sessions/shared.ts delete mode 100644 apps/website/src/domain/sessions/types.ts delete mode 100644 apps/website/src/domain/sports.ts delete mode 100644 apps/website/src/domain/text.ts create mode 100644 apps/website/src/vite-env.d.ts diff --git a/apps/website/README.md b/apps/website/README.md index 64ffa5b..1973bb2 100644 --- a/apps/website/README.md +++ b/apps/website/README.md @@ -1,13 +1,31 @@ # Website -Movo's product website: a landing page that presents the app as shipped, and a -clickable demo (questions, sport choice, plan creation, marking a session done, -a chat change with Undo). It is deployed to GitHub Pages at -https://taqix.github.io/hackyeah-2026/. +Movo's product website: a landing page that presents the app and leads into it. It is +deployed to GitHub Pages at https://taqix.github.io/hackyeah-2026/, next to the app's +web build at https://taqix.github.io/hackyeah-2026/app/. -React 18 + TypeScript (`strict`), built with Vite into static files. Routing is hash-based -(`#/try/1`, `#/try/plan`, …), so the build works from any folder. -`#/try/sample` opens a seeded guest plan. +React 18 + TypeScript (`strict`), built with Vite into static files. The page's +sections are hash routes (`#/how`, `#/features`), so the build works from any folder. + +## Into the app + +The site has no demo of its own. Every call to action is a link into the real app: + +- **Try it as a guest** opens `app/guest`, which creates a guest and starts + onboarding. The guest gets the whole app without an account, and their plan + stays in that browser. The answers on the hero's question card open it too. +- **Sign in** opens `app/welcome`. + +Old links into the demo this site used to host (`#/try`, `#/try/1`, +`#/try/sample`, …) are sent on to the guest entry. + +The links are relative to the site (`./app/`), so they work under the Pages project +path. To point them somewhere else, such as an Expo dev server, set `VITE_APP_URL` +in the environment or in `apps/website/.env.local`: + +```bash +VITE_APP_URL=http://localhost:8081/ npm run dev:website +``` ## Commands @@ -21,34 +39,28 @@ npm run typecheck --workspace=@hackyeah/website ## Layout -The layers only depend downwards: `design-system` knows nothing about the demo, +The layers only depend downwards: `design-system` knows nothing about the site, `domain` knows nothing about React, components use both, and `App` wires them up. See [coding principles](../../docs/code-principles.md) for the rules this follows. -- `src/domain/`: the demo's rules, all pure and free of React, the DOM and the - clock, standing in for the API and the AI step. `catalog` holds the option - tables, `answers` the questionnaire's result, `sports` the suggestion scoring, - `sessions/` a session builder per sport, `schedule` which days sessions land on, - `plan` a plan and its versions, `chat/` turning a message into a refusal or a new - version, and `copy` every sentence the views show. `domain/index.ts` re-exports - them, so views import from `"../../domain"`. +- `src/domain/`: plain data the page shows. `onboarding` mirrors the app's + onboarding: how many questions it asks, and the answers to the first one. - `src/components/`: `common/` for shared pieces, then `landing/` (with the phone - `mockups/`), `steps/`, `planning/`, `plan/` and `chrome/`. One component per file. -- `src/demo/`: the demo's state. `reducer.ts` is a pure state machine covering every - action; `useDemo.ts` wraps it and owns the few side effects; `redirects.ts` decides - which routes the current state allows. -- `src/App.tsx`, `src/routing.ts`, `src/hooks/`, `src/a11y.ts`: the shell — the route, - the theme, the breakpoint, per-route focus and scrolling, keyboard helpers. + `mockups/`) and `chrome/`. One component per file. +- `src/App.tsx`, `src/routing.ts`, `src/appLinks.ts`, `src/hooks/`, `src/a11y.ts`: + the shell — the route, the links into the app, the theme, the breakpoint, + per-route focus and scrolling. - `src/design-system/`: a copy of `design/system` (tokens, fonts, styles and the reference components, ported to typed JSX modules that render the same markup). - Keep it in step with `design/system` when the design changes. + Keep it in step with `design/system` when the design changes. The one addition is + `href` on `Button`, which renders a link that looks the same. - `src/site.css`: page styles; every class is prefixed `s-` (`m-` in the phone mockups). The phone mockups on the landing page redraw the app's Home 5, onboarding 2 and chat 8.3 screens from `design/prototype`; update them when those screens change. -The page never mentions plan versions or what the demo leaves out. The website -keeps its guest demo; the mobile app has none. +The hero's question card and the onboarding mockup show the app's first question, +so update `src/domain/onboarding.ts` when the app's onboarding changes. ## Deployment diff --git a/apps/website/index.html b/apps/website/index.html index d85243d..79ee7f5 100644 --- a/apps/website/index.html +++ b/apps/website/index.html @@ -10,7 +10,7 @@ -

Loading the demo…

- +

Loading…

+ diff --git a/apps/website/src/App.tsx b/apps/website/src/App.tsx index 1ef22da..3cdb041 100644 --- a/apps/website/src/App.tsx +++ b/apps/website/src/App.tsx @@ -1,125 +1,33 @@ -/* The demo's shell: which view the route asks for, which one the state allows, and the - page frame around it. All of the demo's own state lives in useDemo. */ +/* The site's shell: the landing page in its frame. Old links into the demo this site used + to host are sent on to the app's guest entry instead. */ -import { useEffect, useLayoutEffect } from "react"; -import { startOfDay, type ComfortValue } from "./domain"; -import { DemoHeader, SiteFooter, SiteHeader } from "./components/chrome"; +import { useLayoutEffect } from "react"; +import { GUEST_ENTRY } from "./appLinks"; +import { SiteFooter, SiteHeader } from "./components/chrome"; import { Landing } from "./components/landing"; -import { PlanView } from "./components/plan"; -import { PlanningView } from "./components/planning"; -import { StepView } from "./components/steps"; -import { redirectFor } from "./demo/redirects"; -import { useDemo, type Demo } from "./demo/useDemo"; -import { useBreakpoint, type Breakpoint } from "./hooks/useMediaQuery"; +import { useBreakpoint } from "./hooks/useMediaQuery"; import { useHashRoute } from "./hooks/useHashRoute"; import { scrollToPageTop, useRouteFocus } from "./hooks/useRouteFocus"; import { useTheme } from "./hooks/useTheme"; import * as route from "./routing"; -import type { Route } from "./routing"; /** The skip link's target, which is also where focus lands on every route. */ const MAIN_ID = "s-main"; -interface ViewProps { - current: Route; - demo: Demo; - breakpoint: Breakpoint; - today: Date; -} - -function CurrentView({ current, demo, breakpoint, today }: ViewProps) { - const { state, ranked, sport, actions } = demo; - switch (current.view) { - case "landing": - return ( - { - actions.answer({ comfort }); - route.navigate(route.stepPath(route.FIRST_STEP + 1)); - }} - /> - ); - - case "step": - return ( - - ); - - case "planning": - return ; - - case "plan": - return state.plan ? ( - - ) : null; - - /* The sample route only seeds the guest plan, then redirects to it. */ - case "sample": - return null; - } -} - export function App() { const { route: current, navigationId } = useHashRoute(); const breakpoint = useBreakpoint(); const { theme, toggle: toggleTheme } = useTheme(); - const demo = useDemo(); - const today = startOfDay(new Date()); - const pendingRedirect = redirectFor(current, demo.state); + const leaving = current.view === "guest"; useLayoutEffect(() => { - if (pendingRedirect) route.redirect(pendingRedirect); - }, [pendingRedirect, current.key]); - - useEffect(() => { - if (current.view === "step" && current.step) demo.actions.visitStep(current.step); - if (current.view === "sample") demo.actions.openSample(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [current.key]); + if (leaving) route.redirect(GUEST_ENTRY); + }, [leaving]); - /* Nothing to focus or scroll to while the view on screen is about to be replaced. */ - const showing = !pendingRedirect && current.view !== "sample"; - useRouteFocus({ route: current, navigationId, active: showing }); + /* Nothing to focus or scroll to while the page is about to be left. */ + useRouteFocus({ route: current, navigationId, active: !leaving }); - const onLanding = current.view === "landing"; + if (leaving) return null; return ( <> Skip to content - {onLanding ? ( - - ) : ( - - )} +
- {showing ? : null} +
- {onLanding ? : null} + ); } diff --git a/apps/website/src/a11y.ts b/apps/website/src/a11y.ts index a30bc34..513ae2d 100644 --- a/apps/website/src/a11y.ts +++ b/apps/website/src/a11y.ts @@ -1,37 +1,4 @@ -/* Keyboard and screen-reader helpers shared by the views. */ - -import type { KeyboardEvent } from "react"; - -const ARROW_STEPS: Record = { - ArrowDown: 1, - ArrowRight: 1, - ArrowUp: -1, - ArrowLeft: -1, -}; - -/** Which way an arrow key moves, or null for any other key. */ -export function arrowStep(key: string): number | null { - return ARROW_STEPS[key] ?? null; -} - -/** The next index in a list that wraps at both ends. */ -export function wrapIndex(index: number, step: number, length: number): number { - return (index + step + length) % length; -} - -/** Arrow keys move focus and selection between the [role=radio] items of a group, wrapping. - For groups whose items own their state; a group driven by React state uses `arrowStep`. */ -export function handleRadioArrows(event: KeyboardEvent): void { - const step = arrowStep(event.key); - if (step === null) return; - const items = [...event.currentTarget.querySelectorAll('[role="radio"]')]; - const current = items.indexOf(document.activeElement as HTMLElement); - if (current < 0) return; - event.preventDefault(); - const next = items[wrapIndex(current, step, items.length)]; - next.focus(); - next.click(); -} +/* Accessibility helpers shared by the components. */ /** Hide a decorative subtree from assistive tech and keyboard focus, as a ref callback. */ export function setInert(element: HTMLElement | null): void { diff --git a/apps/website/src/appLinks.ts b/apps/website/src/appLinks.ts new file mode 100644 index 0000000..6b1042e --- /dev/null +++ b/apps/website/src/appLinks.ts @@ -0,0 +1,16 @@ +/* Links into the Movo app. Its web build is served from app/ beside this site + (https://taqix.github.io/hackyeah-2026/app/), so the default is relative and works under + any base path. VITE_APP_URL points the links at another server, such as a local Expo + dev server. */ + +function withTrailingSlash(url: string): string { + return url.endsWith("/") ? url : `${url}/`; +} + +const APP_URL = withTrailingSlash(import.meta.env.VITE_APP_URL || "./app/"); + +/** Creates a guest and starts onboarding: the whole app, with no account. */ +export const GUEST_ENTRY = `${APP_URL}guest`; + +/** Signs in to an account, or creates one. */ +export const SIGN_IN = `${APP_URL}welcome`; diff --git a/apps/website/src/components/chrome/DemoHeader.tsx b/apps/website/src/components/chrome/DemoHeader.tsx deleted file mode 100644 index a1b6304..0000000 --- a/apps/website/src/components/chrome/DemoHeader.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Badge, Button, IconButton } from "../../design-system"; -import * as route from "../../routing"; -import type { Breakpoint } from "../../hooks/useMediaQuery"; -import type { Theme } from "../../hooks/useTheme"; -import { AppIcon } from "../common"; -import { ThemeButton } from "./ThemeButton"; - -export interface DemoHeaderProps { - onStartOver: () => void; - theme: Theme; - onToggleTheme: () => void; - breakpoint: Breakpoint; -} - -/** The header inside the demo, which trades the nav for a way back to the start. */ -export function DemoHeader({ onStartOver, theme, onToggleTheme, breakpoint }: DemoHeaderProps) { - return ( -
-
- Movo - Demo - - {breakpoint === "phone" ? ( - - ) : ( - - )} - -
-
- ); -} diff --git a/apps/website/src/components/chrome/SiteHeader.tsx b/apps/website/src/components/chrome/SiteHeader.tsx index 94a8853..1b70ee3 100644 --- a/apps/website/src/components/chrome/SiteHeader.tsx +++ b/apps/website/src/components/chrome/SiteHeader.tsx @@ -1,4 +1,5 @@ import { useEffect, useState, type MouseEvent } from "react"; +import { GUEST_ENTRY, SIGN_IN } from "../../appLinks"; import { Button } from "../../design-system"; import * as route from "../../routing"; import { AppIcon } from "../common"; @@ -30,13 +31,13 @@ function sameSectionAgain(hash: string) { } export interface SiteHeaderProps { - hasPlan: boolean; theme: Theme; onToggleTheme: () => void; } -/** The landing page's header. */ -export function SiteHeader({ hasPlan, theme, onToggleTheme }: SiteHeaderProps) { +/** The landing page's header. On a phone it keeps one short way into the app; the hero + right below it offers both. */ +export function SiteHeader({ theme, onToggleTheme }: SiteHeaderProps) { const scrolled = useScrolledPast(HAIRLINE_AT); return (
@@ -48,13 +49,12 @@ export function SiteHeader({ hasPlan, theme, onToggleTheme }: SiteHeaderProps) { What's inside - + +
diff --git a/apps/website/src/components/chrome/index.ts b/apps/website/src/components/chrome/index.ts index 08db08f..1e98bf2 100644 --- a/apps/website/src/components/chrome/index.ts +++ b/apps/website/src/components/chrome/index.ts @@ -1,5 +1,4 @@ -/* The page frame: headers, footer and the theme switch. */ -export { DemoHeader } from "./DemoHeader"; +/* The page frame: header, footer and the theme switch. */ export { SiteFooter } from "./SiteFooter"; export { SiteHeader } from "./SiteHeader"; export { ThemeButton } from "./ThemeButton"; diff --git a/apps/website/src/components/common/ChangeCard.tsx b/apps/website/src/components/common/ChangeCard.tsx index ca455ac..154ff52 100644 --- a/apps/website/src/components/common/ChangeCard.tsx +++ b/apps/website/src/components/common/ChangeCard.tsx @@ -1,5 +1,4 @@ import { Button, Icon } from "../../design-system"; -import type { RevisionSummary } from "../../domain"; import { Fine } from "./Fine"; /** One value as it was and as it is now. */ @@ -20,49 +19,23 @@ export interface ChangeCardRow { /** An applied chat change, as the app's own chat shows it (mobile 8.3). */ export interface ChangeCardProps { - time?: string; summary?: string; rows: ChangeCardRow[]; - /** What the change left alone, which is the promise the demo makes. */ + /** What the change left alone, which is the promise the app makes. */ kept?: string; - undone?: boolean; - onUndo?: (() => void) | null; - onSeePlan?: (() => void) | null; + onUndo?: () => void; + onSeePlan?: () => void; seeLabel?: string; } -function UndoneCard() { - return ( -
-
-
- - Change undone -
-

Back to how it was.

-
-
- ); -} - -export function ChangeCard({ - time = "Just now", - summary, - rows, - kept, - undone, - onUndo, - onSeePlan, - seeLabel = "See plan", -}: ChangeCardProps) { - if (undone) return ; +export function ChangeCard({ summary, rows, kept, onUndo, onSeePlan, seeLabel = "See plan" }: ChangeCardProps) { return (
Plan updated - {time} + Just now
{summary ?

{summary}

: null}
@@ -100,8 +73,3 @@ export function ChangeCard({
); } - -/** A chat change summary as change-card rows: one day, one before-and-after. */ -export function summaryRows(summary: RevisionSummary): ChangeCardRow[] { - return summary.rows.map(row => ({ day: row.day, date: row.date, diffs: [{ from: row.from, to: row.to }] })); -} diff --git a/apps/website/src/components/common/Dialog.tsx b/apps/website/src/components/common/Dialog.tsx deleted file mode 100644 index 7503efe..0000000 --- a/apps/website/src/components/common/Dialog.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useEffect, useRef, type MutableRefObject, type ReactNode } from "react"; - -export interface DialogProps { - open: boolean; - onClose: () => void; - className?: string; - /** The id of the heading that names the dialog. */ - labelledBy: string; - children?: ReactNode; - /** The id of the element to focus on close instead of the opener, set by the caller - just before closing. Read once, then cleared. */ - focusAfter?: MutableRefObject; - /** Where focus goes when the opener is gone from the page, which a plan change can do. */ - fallbackFocus?: string | null; -} - -/** A modal dialog built on , which brings focus trapping and Escape with it. - Focus is returned deliberately, because a change can remove the element that opened it. */ -export function Dialog({ open, onClose, className, labelledBy, children, focusAfter, fallbackFocus }: DialogProps) { - const dialogRef = useRef(null); - const opener = useRef(null); - /* Read through refs: the close listener is attached once, but both can change. */ - const onCloseRef = useRef(onClose); - onCloseRef.current = onClose; - const fallbackRef = useRef(fallbackFocus); - if (fallbackFocus) fallbackRef.current = fallbackFocus; - - useEffect(() => { - const dialog = dialogRef.current; - if (!dialog) return; - const handleClose = () => { - onCloseRef.current(); - const target = focusAfter?.current ?? opener.current; - if (focusAfter) focusAfter.current = null; - /* After the dialog closes the page re-renders, so look the target up next tick. */ - setTimeout(() => { - let element = typeof target === "string" ? document.getElementById(target) : (target as HTMLElement | null); - if ((!element || !element.isConnected) && fallbackRef.current) { - element = document.getElementById(fallbackRef.current); - } - element?.focus(); - }, 0); - }; - dialog.addEventListener("close", handleClose); - return () => dialog.removeEventListener("close", handleClose); - }, [focusAfter]); - - useEffect(() => { - const dialog = dialogRef.current; - if (!dialog) return; - if (open && !dialog.open) { - opener.current = document.activeElement; - dialog.showModal(); - } - if (!open && dialog.open) dialog.close(); - }, [open]); - - return ( - { - /* A click on the backdrop lands on the dialog element itself. */ - if (event.target === dialogRef.current) dialogRef.current?.close(); - }} - > - {open ? children : null} - - ); -} diff --git a/apps/website/src/components/common/ExerciseLine.tsx b/apps/website/src/components/common/ExerciseLine.tsx deleted file mode 100644 index 7c4fc2a..0000000 --- a/apps/website/src/components/common/ExerciseLine.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Icon } from "../../design-system"; -import type { SessionExercise } from "../../domain"; - -/** Whether to show a tick: a completed exercise, or one left open. */ -export type ExerciseCheck = "done" | "open"; - -const CHECK_ICON: Record = { done: "check", open: "circle" }; -const CHECK_COLOUR: Record = { done: "var(--success)", open: "var(--text-tertiary)" }; - -/** One exercise, read-only. The design system's ExerciseRow always renders a toggle, - so sessions that cannot be ticked off use this instead. */ -export function ExerciseLine({ exercise, check }: { exercise: SessionExercise; check?: ExerciseCheck }) { - return ( -
- {check ? : null} -
- {exercise.name} - {exercise.detail} · {exercise.meta} -
-
- ); -} diff --git a/apps/website/src/components/common/PlanStrip.tsx b/apps/website/src/components/common/PlanStrip.tsx deleted file mode 100644 index a8321d4..0000000 --- a/apps/website/src/components/common/PlanStrip.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { activeVersion, dateAt, formatFullDate, SLOTS, sessionAt, weekdayInitial, weekOffsets, - type Plan, type Session, type Version } from "../../domain"; - -/** What a day is, as a class and in words: today, a done session, a session, or a rest day. */ -function dayState(session: Session | null, isToday: boolean, isSelected: boolean): string { - const state = session?.done ? " is-done" : session ? " is-session" : " is-rest"; - return `s-day${isToday ? " is-today" : ""}${state}${isSelected ? " is-sel" : ""}`; -} - -/** Everything a screen reader needs about a day, since the button itself shows only a number. */ -function dayLabel(plan: Plan, version: Version, offset: number, session: Session | null): string { - const parts = [formatFullDate(dateAt(plan, offset))]; - if (offset === plan.todayOffset) parts.push("today"); - if (!session) parts.push("rest day"); - else if (session.done) parts.push(session.title.toLowerCase(), "done"); - else parts.push(session.title.toLowerCase(), `${session.minutes} minutes`, SLOTS[session.slot].inPhrase); - if (session && session.changedIn === version.number && version.number > 1) parts.push("changed"); - return parts.join(", "); -} - -export interface PlanStripProps { - plan: Plan; - selected?: number; - onSelect?: (offset: number) => void; - /** Set in the landing mockups, where the strip is a picture rather than a control. */ - isStatic?: boolean; -} - -/** The week as seven days, each one selectable. */ -export function PlanStrip({ plan, selected, onSelect, isStatic }: PlanStripProps) { - const version = activeVersion(plan); - return ( -
- {weekOffsets().map(offset => { - const date = dateAt(plan, offset); - const session = sessionAt(version, offset); - const className = dayState(session, offset === plan.todayOffset, selected === offset); - const inner = ( - <> - {weekdayInitial(date)} - {date.getDate()} - - ); - if (isStatic) return
{inner}
; - return ( - - ); - })} -
- ); -} diff --git a/apps/website/src/components/common/StepDots.tsx b/apps/website/src/components/common/StepDots.tsx index 130ec25..19fb086 100644 --- a/apps/website/src/components/common/StepDots.tsx +++ b/apps/website/src/components/common/StepDots.tsx @@ -1,7 +1,5 @@ -import { LAST_STEP } from "../../routing"; - /** How far through the questions you are. Decorative: the step is also written out in words. */ -export function StepDots({ filled, total = LAST_STEP }: { filled: number; total?: number }) { +export function StepDots({ filled, total }: { filled: number; total: number }) { return (