Add Quick dinner and Cook from what I have shortcuts to Home
CI / ci (push) Successful in 5m39s
CI / ci (pull_request) Successful in 5m43s

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:
2026-10-10 14:14:33 -04:00
co-authored by Claude Opus 5.5
parent 01038b376a
commit 2bc412ae06
4 changed files with 135 additions and 0 deletions
+25
View File
@@ -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<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 () => {
mockViewport(390)
stubFetchByPath({
+2
View File
@@ -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<Record<(typeof routes)[number]['path'], ReactNode>> = {
'/': <HomePage />,
'/plan': <PlanPage />,
'/plan/shopping': <SupplyRunPage />,
'/plan/shopping/archive': <SupplyArchivePage />,
+68
View File
@@ -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;
}
+40
View File
@@ -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>
)
}