diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index e9be839..099b417 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -2,6 +2,7 @@ import { act, cleanup, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createApiClient, createVerifiedSession } from './api/index.ts' import { createNetworkStatus } from './pwa/networkStatus.ts' +import { stubAnimate } from './test/animate.ts' import { mockViewport } from './test/matchMedia.ts' import { errorResponse, fakeLocation, jsonResponse, ME, request, stubFetch, stubFetchByPath } from './test/fetchStub.ts' import App, { routes } from './App.tsx' @@ -12,6 +13,7 @@ import { defaultWeek } from './plan/week.ts' afterEach(() => { Reflect.deleteProperty(window, 'matchMedia') + Reflect.deleteProperty(Element.prototype, 'animate') vi.restoreAllMocks() vi.unstubAllGlobals() }) @@ -30,6 +32,7 @@ function renderAppAt(path: string, location = fakeLocation(path)) { // The routes that render a built screen rather than a placeholder page. const SCREENS = { + '/': true, '/onboarding': true, '/plan': true, '/plan/shopping': true, @@ -84,6 +87,28 @@ describe('App routes', () => { }, ) + it.each([ + ['Quick dinner', '/quick', 'Quick dinners'], + ['Cook from what I have', '/cook-from-pantry', 'Cook from what I have'], + ])('Home links %s to %s', async (link, path, heading) => { + mockViewport(390) + stubAnimate({ autoFinish: true }) + // The idea screen's own requests stay pending: this checks only that Home + // navigates to it. + const fetchMock = vi.fn((input) => { + const respond = Object.hasOwn(SIGNED_IN, String(input)) ? SIGNED_IN[String(input) as keyof typeof SIGNED_IN] : undefined + return respond === undefined ? new Promise(() => {}) : Promise.resolve(respond()) + }) + vi.stubGlobal('fetch', fetchMock) + renderAt('/') + await screen.findByRole('heading', { level: 1, name: 'Home' }) + const shortcut = screen.getByRole('link', { name: link }) + expect(shortcut).toHaveAttribute('href', path) + act(() => shortcut.click()) + expect(await screen.findByRole('heading', { level: 1, name: heading })).toBeInTheDocument() + expect(screen.queryByRole('heading', { level: 1, name: 'Home' })).not.toBeInTheDocument() + }) + it('/settings renders the settings home inside the shell', async () => { mockViewport(390) stubFetchByPath({ diff --git a/web/src/App.tsx b/web/src/App.tsx index 8fb3ea3..7c4897a 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -8,6 +8,7 @@ import { CookFromPantryPage } from './ideas/CookFromPantryPage.tsx' import { QuickDinnersPage } from './ideas/QuickDinnersPage.tsx' import { ONBOARDING_PATH, OnboardingGate } from './onboarding/OnboardingGate.tsx' import { OnboardingPage } from './onboarding/OnboardingPage.tsx' +import { HomePage } from './pages/HomePage.tsx' import { PlaceholderPage } from './pages/PlaceholderPage.tsx' import { PlanPage } from './plan/PlanPage.tsx' import { OfflinePill } from './pwa/OfflinePill.tsx' @@ -43,6 +44,7 @@ export const routes = [ // The screens built so far; every other route shows its placeholder page. const screens: Partial> = { + '/': , '/plan': , '/plan/shopping': , '/plan/shopping/archive': , diff --git a/web/src/pages/HomePage.module.css b/web/src/pages/HomePage.module.css new file mode 100644 index 0000000..33e2043 --- /dev/null +++ b/web/src/pages/HomePage.module.css @@ -0,0 +1,68 @@ +/* Placeholder Home with the idea-screen shortcuts. */ +.page { + max-width: 560px; + display: flex; + flex-direction: column; + gap: 14px; +} + +.title { + margin: 0; + padding-top: 6px; + font-family: var(--font-heading); + font-size: 28px; + font-weight: 400; +} + +.rows { + margin: 0; + padding: 0; + list-style: none; +} + +/* Rows in one card are split by a hairline inset to the row padding. */ +.item + .item { + border-top: 1px solid var(--color-neutral-300); + margin: 0 16px; +} + +.item + .item .row { + padding-left: 0; + padding-right: 0; +} + +.row { + box-sizing: border-box; + display: flex; + align-items: center; + gap: 14px; + min-height: 44px; + padding: 14px 16px; + border-radius: 26px; + color: var(--color-text); + text-decoration: none; +} + +.icon { + flex: none; + display: flex; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + border-radius: 50%; + background: var(--color-bg); +} + +.icon svg { + width: 18px; + height: 18px; +} + +.rowTitle { + flex: 1; + min-width: 0; + font-family: var(--font-body); + font-size: 15px; + font-weight: 700; +} diff --git a/web/src/pages/HomePage.tsx b/web/src/pages/HomePage.tsx new file mode 100644 index 0000000..347ff22 --- /dev/null +++ b/web/src/pages/HomePage.tsx @@ -0,0 +1,40 @@ +import { ChevronRight, Refrigerator, Zap, type LucideIcon } from 'lucide-react' +import { Link } from 'react-router' +import { Card, Icon } from '../ui/index.ts' +import styles from './HomePage.module.css' + +// Home stands in for the Home screen until its feature is built, with the +// shortcuts to the idea screens that have no other entry point. +export function HomePage() { + return ( +
+

Home

+ +
    + + +
+
+
+ ) +} + +type ShortcutProps = { + to: string + icon: LucideIcon + title: string +} + +function Shortcut({ to, icon, title }: ShortcutProps) { + return ( +
  • + + + + + {title} + + +
  • + ) +}