Add Quick dinner and Cook from what I have shortcuts to Home
The placeholder Home now links to /quick and /cook-from-pantry, so Cook from what I have has an in-app entry point. App tests click each link and check the destination screen renders. Task: ab589dee-4cdc-4a17-aff9-7b3cba975548 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { act, cleanup, render, screen } from '@testing-library/react'
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { createApiClient, createVerifiedSession } from './api/index.ts'
|
import { createApiClient, createVerifiedSession } from './api/index.ts'
|
||||||
import { createNetworkStatus } from './pwa/networkStatus.ts'
|
import { createNetworkStatus } from './pwa/networkStatus.ts'
|
||||||
|
import { stubAnimate } from './test/animate.ts'
|
||||||
import { mockViewport } from './test/matchMedia.ts'
|
import { mockViewport } from './test/matchMedia.ts'
|
||||||
import { errorResponse, fakeLocation, jsonResponse, ME, request, stubFetch, stubFetchByPath } from './test/fetchStub.ts'
|
import { errorResponse, fakeLocation, jsonResponse, ME, request, stubFetch, stubFetchByPath } from './test/fetchStub.ts'
|
||||||
import App, { routes } from './App.tsx'
|
import App, { routes } from './App.tsx'
|
||||||
@@ -12,6 +13,7 @@ import { defaultWeek } from './plan/week.ts'
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
Reflect.deleteProperty(window, 'matchMedia')
|
Reflect.deleteProperty(window, 'matchMedia')
|
||||||
|
Reflect.deleteProperty(Element.prototype, 'animate')
|
||||||
vi.restoreAllMocks()
|
vi.restoreAllMocks()
|
||||||
vi.unstubAllGlobals()
|
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.
|
// The routes that render a built screen rather than a placeholder page.
|
||||||
const SCREENS = {
|
const SCREENS = {
|
||||||
|
'/': true,
|
||||||
'/onboarding': true,
|
'/onboarding': true,
|
||||||
'/plan': true,
|
'/plan': true,
|
||||||
'/plan/shopping': 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<typeof fetch>((input) => {
|
||||||
|
const respond = Object.hasOwn(SIGNED_IN, String(input)) ? SIGNED_IN[String(input) as keyof typeof SIGNED_IN] : undefined
|
||||||
|
return respond === undefined ? new Promise<Response>(() => {}) : 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 () => {
|
it('/settings renders the settings home inside the shell', async () => {
|
||||||
mockViewport(390)
|
mockViewport(390)
|
||||||
stubFetchByPath({
|
stubFetchByPath({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { CookFromPantryPage } from './ideas/CookFromPantryPage.tsx'
|
|||||||
import { QuickDinnersPage } from './ideas/QuickDinnersPage.tsx'
|
import { QuickDinnersPage } from './ideas/QuickDinnersPage.tsx'
|
||||||
import { ONBOARDING_PATH, OnboardingGate } from './onboarding/OnboardingGate.tsx'
|
import { ONBOARDING_PATH, OnboardingGate } from './onboarding/OnboardingGate.tsx'
|
||||||
import { OnboardingPage } from './onboarding/OnboardingPage.tsx'
|
import { OnboardingPage } from './onboarding/OnboardingPage.tsx'
|
||||||
|
import { HomePage } from './pages/HomePage.tsx'
|
||||||
import { PlaceholderPage } from './pages/PlaceholderPage.tsx'
|
import { PlaceholderPage } from './pages/PlaceholderPage.tsx'
|
||||||
import { PlanPage } from './plan/PlanPage.tsx'
|
import { PlanPage } from './plan/PlanPage.tsx'
|
||||||
import { OfflinePill } from './pwa/OfflinePill.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.
|
// The screens built so far; every other route shows its placeholder page.
|
||||||
const screens: Partial<Record<(typeof routes)[number]['path'], ReactNode>> = {
|
const screens: Partial<Record<(typeof routes)[number]['path'], ReactNode>> = {
|
||||||
|
'/': <HomePage />,
|
||||||
'/plan': <PlanPage />,
|
'/plan': <PlanPage />,
|
||||||
'/plan/shopping': <SupplyRunPage />,
|
'/plan/shopping': <SupplyRunPage />,
|
||||||
'/plan/shopping/archive': <SupplyArchivePage />,
|
'/plan/shopping/archive': <SupplyArchivePage />,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className={styles.page}>
|
||||||
|
<h1 className={styles.title}>Home</h1>
|
||||||
|
<Card>
|
||||||
|
<ul className={styles.rows}>
|
||||||
|
<Shortcut to="/quick" icon={Zap} title="Quick dinner" />
|
||||||
|
<Shortcut to="/cook-from-pantry" icon={Refrigerator} title="Cook from what I have" />
|
||||||
|
</ul>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShortcutProps = {
|
||||||
|
to: string
|
||||||
|
icon: LucideIcon
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function Shortcut({ to, icon, title }: ShortcutProps) {
|
||||||
|
return (
|
||||||
|
<li className={styles.item}>
|
||||||
|
<Link to={to} className={styles.row}>
|
||||||
|
<span className={styles.icon}>
|
||||||
|
<Icon icon={icon} />
|
||||||
|
</span>
|
||||||
|
<span className={styles.rowTitle}>{title}</span>
|
||||||
|
<Icon icon={ChevronRight} />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user