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:
@@ -9,6 +9,38 @@ import { Card, CardBody } from '../components/ui/Card'
|
||||
import { Skeleton, SkeletonText } from '../components/ui/Skeleton'
|
||||
import { EmptyState } from '../components/ui/EmptyState'
|
||||
|
||||
const AISLE_LABEL: Record<string, string> = {
|
||||
produce: 'Produce',
|
||||
meat: 'Meat & Seafood',
|
||||
seafood: 'Meat & Seafood',
|
||||
meat_seafood: 'Meat & Seafood',
|
||||
chicken: 'Meat & Seafood',
|
||||
beef: 'Meat & Seafood',
|
||||
pork: 'Meat & Seafood',
|
||||
dairy: 'Dairy & Eggs',
|
||||
eggs: 'Dairy & Eggs',
|
||||
cheese: 'Dairy & Eggs',
|
||||
milk: 'Dairy & Eggs',
|
||||
yogurt: 'Dairy & Eggs',
|
||||
pantry: 'Pantry',
|
||||
canned: 'Pantry',
|
||||
canned_goods: 'Pantry',
|
||||
dry: 'Pantry',
|
||||
snacks: 'Pantry',
|
||||
frozen: 'Frozen',
|
||||
bakery: 'Bakery',
|
||||
bread: 'Bakery',
|
||||
beverages: 'Beverages',
|
||||
drinks: 'Beverages',
|
||||
spices: 'Spices',
|
||||
seasoning: 'Spices',
|
||||
other: 'Other',
|
||||
}
|
||||
|
||||
function aisleDisplay(key: string): string {
|
||||
return AISLE_LABEL[key.toLowerCase()] ?? key
|
||||
}
|
||||
|
||||
function ShoppingListSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
@@ -156,46 +188,46 @@ export default function ShoppingListPage() {
|
||||
</div>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-3 gap-2 sm:gap-4">
|
||||
<Card className="bg-gradient-to-br from-primary-50 to-primary-100/50 border-primary-200">
|
||||
<CardBody>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-primary-100 flex items-center justify-center">
|
||||
<Receipt className="w-5 h-5 text-primary-600" />
|
||||
<CardBody className="p-3 sm:p-6">
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-xl bg-primary-100 flex items-center justify-center flex-shrink-0">
|
||||
<Receipt className="w-4 h-4 sm:w-5 sm:h-5 text-primary-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-surface-900">
|
||||
<div className="min-w-0">
|
||||
<div className="text-base sm:text-2xl font-bold text-surface-900 truncate">
|
||||
${shoppingList.total_estimated_cost.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-sm text-surface-500">Estimated Total</div>
|
||||
<div className="text-[10px] sm:text-sm text-surface-500 truncate">Estimated</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardBody>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-surface-100 flex items-center justify-center">
|
||||
<Package className="w-5 h-5 text-surface-600" />
|
||||
<CardBody className="p-3 sm:p-6">
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-xl bg-surface-100 flex items-center justify-center flex-shrink-0">
|
||||
<Package className="w-4 h-4 sm:w-5 sm:h-5 text-surface-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-surface-900">{shoppingList.items.length}</div>
|
||||
<div className="text-sm text-surface-500">Total Items</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-base sm:text-2xl font-bold text-surface-900 truncate">{shoppingList.items.length}</div>
|
||||
<div className="text-[10px] sm:text-sm text-surface-500 truncate">Items</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-success-50 to-success-100/50 border-success-200">
|
||||
<CardBody>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-success-100 flex items-center justify-center">
|
||||
<Tag className="w-5 h-5 text-success-600" />
|
||||
<CardBody className="p-3 sm:p-6">
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-xl bg-success-100 flex items-center justify-center flex-shrink-0">
|
||||
<Tag className="w-4 h-4 sm:w-5 sm:h-5 text-success-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-success-700">{shoppingList.sale_items_count}</div>
|
||||
<div className="text-sm text-surface-500">Items on Sale</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-base sm:text-2xl font-bold text-success-700 truncate">{shoppingList.sale_items_count}</div>
|
||||
<div className="text-[10px] sm:text-sm text-surface-500 truncate">On Sale</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
@@ -207,7 +239,7 @@ export default function ShoppingListPage() {
|
||||
{Object.entries(shoppingList.by_aisle).map(([aisle, items]) => (
|
||||
<Card key={aisle} className="overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-surface-200 bg-surface-50 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-surface-900">{aisle}</h3>
|
||||
<h3 className="text-sm font-semibold text-surface-900">{aisleDisplay(aisle)}</h3>
|
||||
<Badge variant="neutral">{items.length} items</Badge>
|
||||
</div>
|
||||
<ul className="divide-y divide-surface-200">
|
||||
|
||||
Reference in New Issue
Block a user