From 36038bb9cbc24c7d7b05c59b2cf11b32e13d5290 Mon Sep 17 00:00:00 2001 From: Peter Woolery Date: Tue, 2 Jun 2026 10:56:19 -0700 Subject: [PATCH] docs(review): mark Sprint 1 P0 fixes addressed in commit f3e4a44 --- Review/ui-nielsen-audit.md | 278 +++++++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 Review/ui-nielsen-audit.md diff --git a/Review/ui-nielsen-audit.md b/Review/ui-nielsen-audit.md new file mode 100644 index 0000000..12db613 --- /dev/null +++ b/Review/ui-nielsen-audit.md @@ -0,0 +1,278 @@ +# UI/UX Audit — Nielsen's 10 Heuristics + +**Scope:** Live deployment at `http://100.108.208.56:8082/`, React frontend at `frontend/src/`, complementing the existing docs/repo reviews in this folder. +**Method:** Playwright (system Chromium) navigated 20 routes/viewports; findings triangulated against source code with `file:line` references. +**Screenshots:** `/tmp/opencode/mp-review/screenshots/` (20 PNGs referenced inline). +**Severity scale:** **P0 (blocker)** — broken core flow · **P1 (major)** — wrong or misleading · **P2 (minor)** — polish/aa. + +--- + +## Executive summary + +The app looks polished on the surface (Tailwind palette, clean cards, working toasts, working focus rings), but a live walkthrough surfaces **multiple silent failures and three outright broken data-rendering bugs**. The most damaging issues are not visual — they are *unmistakable data inconsistencies* the user is expected to read and act on (`$N/A per serving`, blank `lb Pork Chops` rows, hidden empty meal slots on mobile, snake_case aisle labels). They erode trust faster than a missing button. + +**Top 5 to fix first** (P0): + +1. **Meal detail ingredients render without quantities** (field-name bug, `MealDetail.tsx:249-250`) — a core function of the page is unreadable. +2. **`$N/A per serving`** displayed literally (`MealDetail.tsx:191`). +3. **Recipe detail ingredients collapse unit and name** (`RecipeDetail.tsx:161` — `2 canBlack Beans`). +4. **`/recommended` returns a blank page** (missing route + no 404 catch-all in `App.tsx`). +5. **Mobile dashboard hides empty meal slots** (`Dashboard.tsx:164,219` — users on phones cannot *plan* meals, only view them). + +> **Sprint 1 status (commit `f3e4a44`):** Items 1, 2, 3, 4, 5 all addressed in the frontend source. ⚠️ **Not yet deployed to the live server at `100.108.208.56:8082/`** — that host is a different machine (Tailscale IP `100.108.224.12`) than the one I edited on. The user must `git pull` and `docker compose up -d --build frontend` on the deployment host. Verification screenshots in `/tmp/opencode/mp-review/screenshots/fix-sprint1/`. + +--- + +## Findings mapped to Nielsen's 10 Heuristics + +### H1 · Visibility of system status — *Partial* + +✅ **Works well** +- Toasts (`react-hot-toast`) for generate/delete are top-right and persist. +- Status badges on the dashboard (e.g. `$206.21` total) update reactively. +- Loading skeletons render on data fetch. + +⚠️ **Gaps** +- **Filters (P2).** Active filter count is not shown when the filter panel is collapsed (`Recipes.tsx:99` area, `16-recipes-filters-open.png`). User has no way to know a filter is on. +- **Pantry search (P2).** No "X of N results" indicator. +- **Sync status (P2).** When a meal is being generated, no spinner on the slot itself — only the global toast after success. + +**Fix:** Render an `activeFilters.length` chip on the Filters button; add a small "Searching…" indicator inside the Pantry search input. + +--- + +### H2 · Match between system and the real world — *Multiple violations* + +🚨 **P1 · Snake-case aisle labels on Shopping List** (`page-shopping-list.png`) +- Sections display `meat_seafood`, `produce`, `pantry`, `dairy`. +- Fix: human-readable map in `ShoppingList.tsx`: + ```ts + const AISLE_LABEL: Record = { + meat_seafood: 'Meat & Seafood', produce: 'Produce', + pantry: 'Pantry', dairy: 'Dairy & Eggs', + }; + ``` + +🚨 **P1 · `$N/A per serving`** (`MealDetail.tsx:191`, `14-meal-detail.png`) +- `${item.estimated_cost?.toFixed(2) || 'N/A'}` renders `$N/A` literally because the `$` is outside the conditional. +- Fix: + ```tsx + {item.estimated_cost != null + ? `$${item.estimated_cost.toFixed(2)} per serving` + : 'No price estimate yet'} + ``` + +🚨 **P0 · Ingredients render without quantities on the Meal page** (`MealDetail.tsx:249-252`, `14-meal-detail.png`) +- Code reads `ing.quantity` / `ing.unit` but the backend returns `qty` (per `RecipeDetail.tsx:161` working correctly). Result: `lb Pork Chops, Bone-In` instead of `1 lb Pork Chops, Bone-In`. +- Fix: rename both fields to a single canonical name (recommend `qty` to match backend), or apply a compatibility shim: + ```tsx + const qty = ing.qty ?? ing.quantity; + const unit = ing.unit ?? ing.unit; + ``` + and update the type definition. + +🚨 **P1 · Spoonacular marketing copy leaks into meal description** (`14-meal-detail.png`) +- The meal page description includes: *"Featured In Group could be just the gluten free, dairy free, and ketogenic recipe you've been looking for… users who liked this recipe also liked Baked Chicken In Avocado Boat…"* +- Fix: backend `Meal.description` should be truncated to ~280 chars on import, with a regex strip of the "Featured In Group…" / "users who liked…" boilerplate. Alternatively, render `description.split('. ').slice(0,2).join('. ')+'.'` on the frontend with a `line-clamp-3` parent. + +🚨 **P1 · Hero title overlaps description** (`MealDetail.tsx:168-197`, `14-meal-detail.png`) +- Long description text (no `line-clamp`) sits over the absolute-positioned title block, making the title literally unreadable. +- Fix: add `line-clamp-3` and `max-w-2xl` on the description; ensure the title is in normal flow (not absolute) on this view. + +--- + +### H3 · User control and freedom — *Partial* + +✅ **Works** +- Back links on Recipe and Meal detail pages. +- Drag-and-drop on dashboard (via `@hello-pangea/dnd`) is reversible. + +⚠️ **Gaps** +- **P1 · Native `confirm()` dialogs for delete** — jarring, breaks visual continuity. Replace with an inline "Undo" toast (e.g. `react-hot-toast` with a 5s undo that re-fires the create query). See `Dashboard.tsx` meal delete and `Pantry.tsx` row delete. +- **P2 · No keyboard shortcut to focus search** on Recipes/Pantry/Shopping List. Convention is `/` or `Cmd+K`. +- **P2 · Filters have no Reset button** (`16-recipes-filters-open.png`). + +--- + +### H4 · Consistency and standards — *Multiple violations* + +🚨 **P1 · Aisle casing inconsistency in Pantry** (`page-pantry.png`, `Pantry.tsx:200`) +- Rows show `Canned Goods`, `Pantry`, `pantry`, `Produce`, `Freezer` — all derived from free-text input. Aisle should be a fixed enum. +- Fix: replace the free-text input with a `` with canonical list | +| B9 | `ShoppingList.tsx` | snake_case aisle names | P1 | Human-readable map | +| B10 | `Pantry.tsx:236` | `overflow-x-auto` without scroll hint on mobile | P1 | Add a faded right-edge gradient + aria `role="region"` with descriptive label | +| B11 | `Recipes.tsx` | Filters have no Apply/Reset/active count | P1 | Add Reset, Apply, and an `activeCount` chip on the Filters button | +| B12 | various | Native `confirm()` for delete | P2 | Replace with `react-hot-toast` undo pattern | +| B13 | `Navigation.tsx:14-38` | "Shopping List" wraps on mobile | P2 | Add `whitespace-nowrap` | +| B14 | dashboard/Shopping List | Stat cards stack full-width on mobile | P2 | Use 3-col compact layout for `