# Fix Plan — UI/UX Audit (Nielsen 10 Heuristics) ## Goal Resolve the 14 issues (5 P0, 6 P1, 3 P2) from `Review/ui-nielsen-audit.md` in three sprints, ending each sprint with a deployable, demonstrable improvement on `http://100.108.208.56:8082/`. ## Scope boundaries - **In:** frontend React/TS fixes in `frontend/src/`. Backend one-off data migrations only when required (B8 aisle normalization, B4/Recommended route). - **Out:** New features (bulk add, keyboard shortcuts, onboarding tour) — those are future work, listed in §Future. - **Reuse:** `components/ErrorBoundary.tsx` already exists (verified). `components/ui/*` (Button, Card, Select, Input, EmptyState, Badge, Skeleton, LoadingSpinner) are the building blocks — use them, don't roll new ones. - **Stack confirmed:** React 18 + TS + Vite + Tailwind + react-router-dom 6 + @tanstack/react-query + react-hot-toast + @hello-pangea/dnd + lucide-react + framer-motion. **No new deps** in Sprint 1/2. Sprint 3 may add `react-joyride` only if approved (defer to §Future). ## Conventions - One commit per task: `fix(ui): ` / `feat(ui): ` / `refactor(frontend): `. - Before each commit: `cd frontend && npm run lint && npm run build` (the `build` script runs `tsc` first — type-checks the project). - After each task, re-screenshot the affected page in the same playwright session and diff against `/tmp/opencode/mp-review/screenshots/`. Save new shots in `/tmp/opencode/mp-review/screenshots/fix-sprintN/`. - All UI text in sentence case. New copy matches existing `lib/toast.ts` style. - Type updates go in `frontend/src/types/index.ts`; do not duplicate shapes inline. --- ## Sprint 1 — Stop the bleeding (P0s) **Goal:** Every P0 bug is gone. Each is independently demoable on the live deployment. ### S1.1 · B1 — RecipeDetail ingredients: drop `.trim()` so unit + name don't fuse - **File:** `frontend/src/pages/RecipeDetail.tsx:161` - **Change:** Replace ```tsx {ing.qty != null && `${ing.qty} ${ing.unit || ''} `.trim()} ``` with ```tsx {ing.qty != null && `${ing.qty}${ing.unit ? ` ${ing.unit}` : ''}`} ``` followed by a literal `' '` before `{ing.name}`. - **Verify:** Open `/recipes/eae6591f...` (Black Bean Tacos). Ingredient row reads `2 can Black Beans, Canned` (with space). Re-run `npm run build`. ### S1.2 · B2 — MealDetail ingredients: align field name with backend (`qty`) - **Files:** `frontend/src/types/index.ts`, `frontend/src/pages/MealDetail.tsx:249-252` - **Change:** 1. In `types/index.ts` confirm `MealIngredient` shape; align to backend `qty` / `unit`. If the type currently has `quantity`, rename to `qty` (single source of truth). 2. In `MealDetail.tsx:249-252`, switch reads to `ing.qty` / `ing.unit`. Keep the existing null-guard so `qty == null` is skipped cleanly. - **Verify:** Open `/meals/` (e.g. `/meals/f28...` Pork Stir-Fry). Row reads `1 lb Pork Chops, Bone-In` not `lb Pork Chops`. `npm run build` clean. ### S1.3 · B3 — MealDetail cost: fix `$N/A per serving` - **File:** `frontend/src/pages/MealDetail.tsx:191` - **Change:** ```tsx {item.estimated_cost != null ? `$${item.estimated_cost.toFixed(2)} per serving` : 'No price estimate yet'} ``` - **Verify:** Reload `/meals/f28...`. Price line reads either `$X.XX per serving` or `No price estimate yet` — never `$N/A`. ### S1.4 · B4 — `/recommended` blank page: add `*` NotFound + alias - **Files:** `frontend/src/App.tsx`, new `frontend/src/pages/NotFound.tsx` - **Change:** 1. Create `pages/NotFound.tsx` — friendly card with `AlertTriangle` icon, message *"We can't find that page."*, primary `