Public Access
feat(ui): close 6 P1 audit findings + 1 bonus mobile fix (Sprint 2)
- Dashboard MealCard: title truncate -> line-clamp-2, image shrinks to 40x40 on <md to give the title room (B6). - MealDetail: hero reworked to normal flow with stronger gradient; description runs through new cleanDescription() helper that strips 14 spoonacular SEO patterns and trims to the last full sentence. Raw description moved to a 'Notes from source' disclosure (B7). - Pantry: free-text aisle/unit replaced with <Select> populated from the new PANTRY_AISLES canonical enum; ingredient name field marked required. New PANTRY_AISLES export + PantryAisle type in types (B8). - backend: alembic 0015_normalize_pantry_aisles maps free-text ingredient.aisle and grocery_item.aisle to canonical labels in a single transaction; downgrade raises (restore from snapshot). backend/scripts/dry_run_aisle_migration.sql is the read-only preview helper. - ShoppingList: human-readable AISLE_LABEL map replaces raw snake_case aisle keys; 3-col stat grid with compact mobile sizing (B9 + S3.3). - Pantry table: role/aria-label region and a right-edge white gradient hint at mobile horizontal overflow (B10). - Recipes: pending/applied filter split, Apply and Reset buttons, active-count chip on the Filters button, role=region + aria-label on the panel (B11). - Review/sprint2-verification.md and fix-ui-audit.md updated. Build: npm run build (tsc + vite) green. tsc emits 0 errors. Co-located audit + plan docs kept in sync: Review/ui-nielsen-audit.md gains a Sprint 2 status block; fix-ui-audit.md has implementation notes for each Sprint 2 task.
This commit is contained in:
@@ -2,14 +2,20 @@ import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Plus, Search, Trash2, Package, AlertTriangle } from 'lucide-react'
|
||||
import { mealPlannerApi } from '../api'
|
||||
import type { HomePantryItem, Ingredient } from '../types'
|
||||
import { PANTRY_AISLES, type HomePantryItem, type Ingredient } from '../types'
|
||||
import { Button } from '../components/ui/Button'
|
||||
import { Card, CardBody } from '../components/ui/Card'
|
||||
import { Input } from '../components/ui/Input'
|
||||
import { Select } from '../components/ui/Select'
|
||||
import { Skeleton, SkeletonText } from '../components/ui/Skeleton'
|
||||
import { EmptyState } from '../components/ui/EmptyState'
|
||||
import { showToast } from '../lib/toast'
|
||||
|
||||
const AISLE_OPTIONS = [
|
||||
{ value: '', label: 'Select aisle…' },
|
||||
...PANTRY_AISLES.map(a => ({ value: a, label: a })),
|
||||
]
|
||||
|
||||
export default function Pantry() {
|
||||
const queryClient = useQueryClient()
|
||||
const [showAddForm, setShowAddForm] = useState(false)
|
||||
@@ -174,10 +180,11 @@ export default function Pantry() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<div className="md:col-span-2">
|
||||
<Input
|
||||
label="Ingredient name"
|
||||
label="Ingredient name *"
|
||||
value={ingredientName}
|
||||
onChange={(e) => setIngredientName(e.target.value)}
|
||||
placeholder="e.g., Avocado"
|
||||
required
|
||||
/>
|
||||
{matchedIngredient && (
|
||||
<p className="mt-1 text-xs text-success-600">Matched existing ingredient ✓</p>
|
||||
@@ -190,17 +197,32 @@ export default function Pantry() {
|
||||
onChange={(e) => setQuantity(e.target.value)}
|
||||
placeholder="e.g., 5"
|
||||
/>
|
||||
<Input
|
||||
<Select
|
||||
label="Unit"
|
||||
value={unit}
|
||||
onChange={(e) => setUnit(e.target.value)}
|
||||
placeholder="cans, lbs, etc."
|
||||
options={[
|
||||
{ value: '', label: 'Select unit…' },
|
||||
{ value: 'each', label: 'each' },
|
||||
{ value: 'g', label: 'g' },
|
||||
{ value: 'kg', label: 'kg' },
|
||||
{ value: 'oz', label: 'oz' },
|
||||
{ value: 'lb', label: 'lb' },
|
||||
{ value: 'ml', label: 'ml' },
|
||||
{ value: 'l', label: 'l' },
|
||||
{ value: 'cup', label: 'cup' },
|
||||
{ value: 'tbsp', label: 'tbsp' },
|
||||
{ value: 'tsp', label: 'tsp' },
|
||||
{ value: 'can', label: 'can' },
|
||||
{ value: 'bunch', label: 'bunch' },
|
||||
{ value: 'clove', label: 'clove' },
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
<Select
|
||||
label="Aisle"
|
||||
value={aisle}
|
||||
onChange={(e) => setAisle(e.target.value)}
|
||||
placeholder="e.g., Produce"
|
||||
options={AISLE_OPTIONS}
|
||||
/>
|
||||
<div className="flex items-end">
|
||||
<Button
|
||||
@@ -233,17 +255,22 @@ export default function Pantry() {
|
||||
{/* Items List */}
|
||||
{filteredItems && filteredItems.length > 0 ? (
|
||||
<Card className="overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-surface-200 bg-surface-50">
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Item</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Aisle</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Quantity</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Expires</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-semibold text-surface-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<div className="relative">
|
||||
<div
|
||||
className="overflow-x-auto"
|
||||
role="region"
|
||||
aria-label="Pantry items, scroll horizontally to see all columns"
|
||||
>
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-surface-200 bg-surface-50">
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Item</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Aisle</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Quantity</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Expires</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-semibold text-surface-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-surface-200">
|
||||
{filteredItems.map((item) => {
|
||||
const isExpiringSoon = item.expires_at &&
|
||||
@@ -302,6 +329,11 @@ export default function Pantry() {
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-white to-transparent md:hidden"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user