Files
MealQuest/web/src/home/api.ts
T
dev_agentandClaude Opus 5.5 3a0459cce3
CI / ci (push) Successful in 8m7s
CI / ci (pull_request) Successful in 7m47s
Build the Home screen (mock 1g) from GET /api/v1/home
Replace the / placeholder with HomePage: the household-local date over
"Tonight's quest", the Recent updates bell with its grape unread dot, the
Tonight card (washed photo, name, "Regular · 40 min" / "Side quest · 30
min", dark Recipe pill to /dinner/:id), the Quick dinner and Cook from what
I have tiles, and the status rows to /discover and the week's Supply run.
With no plan the card is "No quest planned tonight" with "Plan this week";
rows that do not apply are hidden. Loading shows skeletons; a failure
shows an ErrorBanner with Retry.

The offline bootstrap tests now emit their online reset before cleanup:
TanStack's online manager only listens while a query client is mounted,
so the reset was lost and Home's query stayed paused in later tests.

Maestro task 5170be5d-4787-4ea8-8530-c19e98e591bc (Feature 21).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:02:03 -04:00

83 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useQuery } from '@tanstack/react-query'
import { useApi } from '../api/index.ts'
import { isRecord } from '../api/errors.ts'
import { oneOf, photoId } from '../dinners/api.ts'
import { weekLabel } from '../plan/week.ts'
export const HOME_PATH = '/api/v1/home'
export const HOME_KEY = ['home'] as const
export const ORIGINS = ['regular', 'side_quest'] as const
export type Origin = (typeof ORIGINS)[number]
// Tonight is the dinner planned for today: photo_id is its stored photo's
// id, from photo, and minutes_total its minutes from prep to table, null
// when the recipe does not say.
export type Tonight = { id: string; name: string; photo_id: string | undefined; origin: Origin; minutes_total: number | null }
// ShoppingListStatus is the Supply run Home points at: next week's once it
// has one, otherwise this week's, ready when that week has a list.
// weekStart is the week's Monday, YYYY-MM-DD, and dates the week as the
// status row shows it, "Oct 5–11" (the API's label is the long "Week 41 ·
// Oct 5 – 11").
export type ShoppingListStatus = { weekStart: string; dates: string; ready: boolean }
// Home is GET /api/v1/home: dateLabel is today, household-local, "Friday,
// Oct 2"; tonight is null when today has no plan; sideQuestCount is how
// many Side quests wait in the deck; unread says the Recent updates hold
// something not yet seen.
export type Home = {
dateLabel: string
tonight: Tonight | null
sideQuestCount: number
shoppingList: ShoppingListStatus
unread: boolean
}
function parseTonight(where: string, body: unknown): Tonight | null {
if (body === null) return null
if (!isRecord(body) || typeof body.id !== 'string' || typeof body.name !== 'string') {
throw new Error(`${where}: tonight is not a dinner`)
}
if (body.minutes_total !== null && !Number.isInteger(body.minutes_total)) {
throw new Error(`${where}: tonight's minutes_total is not a whole number`)
}
return {
id: body.id,
name: body.name,
photo_id: photoId(where, body.photo),
origin: oneOf(where, 'origin', ORIGINS, body.origin),
minutes_total: body.minutes_total as number | null,
}
}
// parseHome checks a GET /api/v1/home body.
export function parseHome(where: string, body: unknown): Home {
if (!isRecord(body) || typeof body.dateLabel !== 'string') throw new Error(`${where}: response is not the home`)
if (!Number.isInteger(body.sideQuestCount) || (body.sideQuestCount as number) < 0) {
throw new Error(`${where}: sideQuestCount is not a count`)
}
const list = body.shoppingList
if (!isRecord(list) || typeof list.weekStart !== 'string' || typeof list.ready !== 'boolean') {
throw new Error(`${where}: shoppingList is not a week's Supply run status`)
}
const updates = body.recentUpdates
if (!isRecord(updates) || typeof updates.unread !== 'boolean') throw new Error(`${where}: recentUpdates has no unread flag`)
return {
dateLabel: body.dateLabel,
tonight: parseTonight(where, body.tonight),
sideQuestCount: body.sideQuestCount as number,
shoppingList: { weekStart: list.weekStart, dates: weekLabel(list.weekStart).dates.replace(' – ', '–'), ready: list.ready },
unread: updates.unread,
}
}
// useHome is the Home screen's summary (GET /api/v1/home).
export function useHome() {
const { apiFetch } = useApi()
return useQuery({
queryKey: HOME_KEY,
queryFn: async () => parseHome(`GET ${HOME_PATH}`, await apiFetch<unknown>(HOME_PATH)),
})
}