Files
MealQuest/web/src/home/RotationCheck.tsx
T
dev_agentandClaude Opus 5.5 be8a658742
CI / ci (push) Successful in 8m20s
CI / ci (pull_request) Successful in 8m1s
Add the Keep it in rotation card to Home (Feature 21)
Home shows the oldest pending Rotation check (GET /api/v1/rotation-reviews)
above the tiles: washed photo, "Last night: {dinner}", "Keep it in
rotation?" and Keep it / Not for us / We didn't make it. An answer posts
to /api/v1/dinners/{id}/rotation-review optimistically, confirms it and
reveals the next check; a failure puts the check back with an ErrorBanner.
Nothing is decided automatically.

Task: 4647eadf-4545-4d3f-849f-ffa5e9fcbd1b

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:55:08 -04:00

80 lines
3.2 KiB
TypeScript

import { Button } from '../ui/Button.tsx'
import { ErrorBanner } from '../ui/ErrorBanner.tsx'
import { Photo } from '../ui/Photo.tsx'
import { useDecideRotationReview, useRotationReviews, type Decision, type RotationReview } from './rotation.ts'
import styles from './RotationCheck.module.css'
// CONFIRMATION is what Home says once the household has answered.
const CONFIRMATION: Record<Decision, string> = {
keep: 'Added to your Regulars',
drop: "Got it — we won't suggest it again",
not_made: 'Back in your Quest log',
}
type RotationCardProps = { review: RotationReview; disabled: boolean; onDecide: (decision: Decision) => void }
// RotationCard asks "Keep it in rotation?" about a tried dinner: its washed
// photo, "Last night: {dinner}", and the three answers.
function RotationCard({ review, disabled, onDecide }: RotationCardProps) {
const { dinner } = review
return (
<section aria-label="Keep it in rotation?" className={styles.card}>
<Photo id={dinner.photo_id} name={dinner.name} inset="10px" sizes="400px" className={styles.photo} />
<div className={styles.body}>
<p className={styles.kicker}>Last night: {dinner.name}</p>
<h2 className={styles.question}>Keep it in rotation?</h2>
<div className={styles.actions}>
<Button disabled={disabled} onClick={() => onDecide('keep')}>
Keep it
</Button>
<Button variant="outline" disabled={disabled} onClick={() => onDecide('drop')}>
Not for us
</Button>
</div>
<button type="button" className={styles.notMade} disabled={disabled} onClick={() => onDecide('not_made')}>
We didn't make it
</button>
</div>
</section>
)
}
// RotationCheck is the Rotation check on Home: the card for the oldest
// pending check (GET /api/v1/rotation-reviews), which stays until the
// household answers it — nothing is decided for them. An answer is applied
// at once: Home confirms it and shows the next pending check, if any, while
// the answer is saved; the answers are locked until it settles. A failed
// answer puts its check back with an ErrorBanner.
export function RotationCheck() {
const reviews = useRotationReviews()
const decide = useDecideRotationReview()
if (reviews.isError) {
return (
<ErrorBanner onRetry={() => void reviews.refetch()}>
Couldn't load the Rotation check. {reviews.error.message}
</ErrorBanner>
)
}
const deciding = decide.isPending ? decide.variables.review.dinner.id : null
const next = reviews.data?.find((review) => review.dinner.id !== deciding)
const answered = decide.isPending || decide.isSuccess ? decide.variables : undefined
return (
<>
{answered !== undefined && (
<p role="status" className={styles.confirmation}>
{CONFIRMATION[answered.decision]}
</p>
)}
{decide.isError && <ErrorBanner>Couldn't save that answer. {decide.error.message}</ErrorBanner>}
{next !== undefined && (
<RotationCard
key={next.dinner.id}
review={next}
disabled={decide.isPending}
onDecide={(decision) => decide.mutate({ review: next, decision })}
/>
)}
</>
)
}