Public Access
feat(ui): close 3 P2 audit findings + a11y sweep (Sprint 3)
- lib/toast.tsx (renamed from .ts for JSX): new showToast.undo(message, onUndo, ms=5000) helper. Inline 'Undo' button dismisses the toast and fires onUndo. Note: react-hot-toast 2.6 lacks onClose/onDismiss, so expiry is silent — same effective behavior as confirm() declined. - Dashboard.handleDelete: captures the full MealPlanItem before the DELETE so Undo can re-fire meals.generateItem(planId, dayOfWeek, mealType) and refill the slot (recipe may differ — see plan R4). - Pantry.handleRemove: fully reversible — Undo re-fires pantry.add with the original ingredient_id, quantity, and unit. New removeId state scopes the spinner to the clicked row. - Both confirm() call sites removed. - App.tsx Navigation: whitespace-nowrap + px-2 sm:px-3 so all 4 links fit on one line down to 360 px. aria-current='page' on the active link. <nav aria-label='Primary'>, <main id='main-content'>. - components/ui/Badge: optional icon and aria-label props. Dashboard approval-status Badge passes aria-label='Approval status: approved' (or the current value) so screen readers don't rely on color alone. - ErrorBoundary already mounted at App.tsx:42 — verified, no code change. - Review/sprint3-verification.md (new) + Review/ui-nielsen-audit.md and fix-ui-audit.md updated with Sprint 3 status and deploy steps. Build: npm run build (tsc + vite) green. tsc 0 errors.
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
GripVertical, X
|
||||
} from 'lucide-react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { showToast } from '../lib/toast'
|
||||
import {
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
@@ -93,7 +94,11 @@ function MealCard({ item, dragHandleProps, isDragging, onApprove: _onApprove, on
|
||||
{item.recipe?.servings} servings
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5 mt-1">
|
||||
<Badge variant={statusVariant} className="text-[10px] px-1.5 py-0.5">
|
||||
<Badge
|
||||
variant={statusVariant}
|
||||
className="text-[10px] px-1.5 py-0.5"
|
||||
aria-label={`Approval status: ${item.approval_status}`}
|
||||
>
|
||||
{item.approval_status}
|
||||
</Badge>
|
||||
{item.estimated_cost && (
|
||||
@@ -345,11 +350,28 @@ export default function Dashboard() {
|
||||
}
|
||||
|
||||
async function handleDelete(itemId: string) {
|
||||
if (!confirm('Delete this meal from the plan?')) return
|
||||
if (!mealPlan) return
|
||||
const item = mealPlan.items.find(i => i.id === itemId)
|
||||
if (!item) return
|
||||
try {
|
||||
await mealPlannerApi.meals.deleteItem(itemId)
|
||||
toast.success('Meal deleted')
|
||||
queryClient.invalidateQueries({ queryKey: ['mealPlan'] })
|
||||
showToast.undo(
|
||||
'Meal deleted',
|
||||
async () => {
|
||||
try {
|
||||
await mealPlannerApi.meals.generateItem(
|
||||
mealPlan.id,
|
||||
item.day_of_week,
|
||||
item.meal_type
|
||||
)
|
||||
queryClient.invalidateQueries({ queryKey: ['mealPlan'] })
|
||||
toast.success('Slot filled with a new meal')
|
||||
} catch (err: any) {
|
||||
toast.error(err?.response?.data?.detail || 'Failed to refill slot')
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (err: any) {
|
||||
toast.error(err?.response?.data?.detail || 'Failed to delete meal')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user