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:
@@ -19,6 +19,7 @@ const AISLE_OPTIONS = [
|
||||
export default function Pantry() {
|
||||
const queryClient = useQueryClient()
|
||||
const [showAddForm, setShowAddForm] = useState(false)
|
||||
const [removeId, setRemoveId] = useState<string | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
/* ingredient name typed by user */
|
||||
@@ -71,12 +72,46 @@ export default function Pantry() {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pantry'] })
|
||||
showToast.success('Item removed')
|
||||
setRemoveId(null)
|
||||
},
|
||||
onError: () => {
|
||||
showToast.error('Failed to remove item')
|
||||
setRemoveId(null)
|
||||
},
|
||||
})
|
||||
|
||||
async function handleRemove(item: HomePantryItem) {
|
||||
if (!item.ingredient_id) {
|
||||
showToast.error('Cannot remove: missing ingredient link')
|
||||
return
|
||||
}
|
||||
setRemoveId(item.id)
|
||||
try {
|
||||
await mealPlannerApi.pantry.remove(item.id)
|
||||
queryClient.invalidateQueries({ queryKey: ['pantry'] })
|
||||
showToast.undo(
|
||||
'Item removed',
|
||||
async () => {
|
||||
try {
|
||||
await mealPlannerApi.pantry.add({
|
||||
ingredient_id: item.ingredient_id!,
|
||||
quantity: item.quantity,
|
||||
unit: item.unit,
|
||||
})
|
||||
queryClient.invalidateQueries({ queryKey: ['pantry'] })
|
||||
showToast.success('Item restored')
|
||||
} catch {
|
||||
showToast.error('Failed to restore item')
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
showToast.error('Failed to remove item')
|
||||
} finally {
|
||||
setRemoveId(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
const name = ingredientName.trim()
|
||||
if (!name) {
|
||||
@@ -313,12 +348,8 @@ export default function Pantry() {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon={<Trash2 className="w-4 h-4" />}
|
||||
onClick={() => {
|
||||
if (confirm(`Remove ${item.ingredient?.name || 'this item'} from pantry?`)) {
|
||||
removeMutation.mutate(item.id)
|
||||
}
|
||||
}}
|
||||
loading={removeMutation.isPending}
|
||||
onClick={() => handleRemove(item)}
|
||||
loading={removeMutation.isPending && removeId === item.id}
|
||||
disabled={removeMutation.isPending}
|
||||
>
|
||||
Remove
|
||||
|
||||
Reference in New Issue
Block a user