import { useRef, useState, type FormEvent } from 'react' import { Plus } from 'lucide-react' import { useNavigate } from 'react-router' import { useAddRegular, useRegulars, useRemoveRegular, type Regular } from '../dinners/api.ts' import { Button } from '../ui/Button.tsx' import { Chip } from '../ui/Chip.tsx' import { ErrorBanner } from '../ui/ErrorBanner.tsx' import { Icon } from '../ui/Icon.tsx' import { Skeleton } from '../ui/Skeleton.tsx' import { TextInput } from '../ui/TextInput.tsx' import { OnboardingStep } from './OnboardingStep.tsx' import styles from './Onboarding.module.css' // MIN_REGULARS is how many Regulars the server has saved before the // household moves on. export const MIN_REGULARS = 3 // commonPicks are offered as one-tap Regulars until the household has them. export const commonPicks = [ 'Beef tacos', 'Spaghetti bolognese', 'Chicken stir-fry', 'Pizza night', 'Chili', 'Pot roast', 'Burgers', 'Fried rice', ] function sameName(a: string, b: string): boolean { return a.trim().toLowerCase() === b.trim().toLowerCase() } // Refusal is one add or removal the server turned down. key is unique to // that attempt, so two attempts of the same change in flight each keep their // own refusal. change names what was tried, so trying it again once it has // been refused clears the earlier refusal and no other. type Refusal = { key: number; change: string; text: string } // RegularsStep is onboarding step 2, handoff mock 1h: the household adds // the dinners it makes on repeat. Each add and remove shows at once and is // undone if the server refuses it. export function RegularsStep() { const navigate = useNavigate() const regulars = useRegulars() const add = useAddRegular() const remove = useRemoveRegular() const [draft, setDraft] = useState('') // Each change keeps its own refusal: the mutation hooks only report their // latest change, which would hide a refusal while another change is in // flight. const [refusals, setRefusals] = useState([]) const attempts = useRef(0) const list = regulars.data ?? [] const picks = commonPicks.filter((pick) => !list.some((regular) => sameName(regular.name, pick))) // Next waits for every add to be answered, so a refused add's error shows // here and an add the server takes back never counts towards the three. const adding = list.some((regular) => regular.pending) const saved = list.filter((regular) => !regular.pending).length function track(change: string, what: string, answer: Promise) { const key = ++attempts.current setRefusals((all) => all.filter((refusal) => refusal.change !== change)) answer.catch((error: Error) => { setRefusals((all) => [...all, { key, change, text: `Could not ${what}: ${error.message}` }]) }) } function addRegular(name: string) { track(`add:${name.toLowerCase()}`, `add ${name}`, add.mutateAsync(name)) } function removeRegular(regular: Regular) { track(`remove:${regular.id}`, `remove ${regular.name}`, remove.mutateAsync(regular)) } function onSubmit(event: FormEvent) { event.preventDefault() const name = draft.trim() if (name === '') return addRegular(name) setDraft('') } return ( void navigate('/onboarding/notifications')} > Next: notifications } >

Add the dinners your household makes on repeat. MealQuest builds suggestions from these.

setDraft(event.target.value)} /> {refusals.map((refusal) => (

{refusal.text}

))}

Your regulars ยท {list.length}

{regulars.isPending && } {regulars.isError && (
void regulars.refetch()}> Your Regulars could not load: {regulars.error.message}
)} {list.length > 0 && (
    {list.map((regular: Regular) => (
  • removeRegular(regular)} />
  • ))}
)} {regulars.isSuccess && picks.length > 0 && ( <>

Common picks

    {picks.map((pick) => (
  • ))}
)}
) }