diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6411ad1..0870fdf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom' +import { BrowserRouter, Routes, Route, Link, useLocation, Navigate } from 'react-router-dom' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ErrorBoundary } from './components/ErrorBoundary' import Dashboard from './pages/Dashboard' @@ -8,6 +8,7 @@ import Recipes from './pages/Recipes' import Recommended from './pages/Recommended' import RecipeDetail from './pages/RecipeDetail' import ShoppingList from './pages/ShoppingList' +import NotFound from './pages/NotFound' const queryClient = new QueryClient() @@ -50,9 +51,11 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> + } /> diff --git a/frontend/src/components/ui/EmptyState.tsx b/frontend/src/components/ui/EmptyState.tsx index 7af6e35..333f726 100644 --- a/frontend/src/components/ui/EmptyState.tsx +++ b/frontend/src/components/ui/EmptyState.tsx @@ -1,4 +1,5 @@ import { LucideIcon } from 'lucide-react'; +import { Link } from 'react-router-dom'; import { Button } from './Button'; interface EmptyStateProps { @@ -7,7 +8,8 @@ interface EmptyStateProps { description: string; action?: { label: string; - onClick: () => void; + onClick?: () => void; + to?: string; }; } @@ -20,9 +22,13 @@ export function EmptyState({ icon: Icon, title, description, action }: EmptyStat

{title}

{description}

{action && ( - + action.to ? ( + + + + ) : ( + + ) )} ); diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index d336334..9dd15c9 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import { Link } from 'react-router-dom' import { CookingPot, CalendarDays, ShoppingCart, ChevronRight, Sparkles, Loader2, - GripVertical + GripVertical, X } from 'lucide-react' import toast from 'react-hot-toast' import { @@ -31,12 +31,13 @@ const MEAL_TYPES = ['breakfast', 'lunch', 'dinner'] as const /* ------------------------------------------------------------------ */ /* MealCard (draggable) */ /* ------------------------------------------------------------------ */ -function MealCard({ item, dragHandleProps, isDragging, onApprove, onDeny }: { +function MealCard({ item, dragHandleProps, isDragging, onApprove: _onApprove, onDeny: _onDeny, onDelete }: { item: MealPlanItem dragHandleProps?: DraggableProvidedDragHandleProps | null isDragging?: boolean onApprove?: (itemId: string) => void onDeny?: (itemId: string) => void + onDelete?: (itemId: string) => void }) { const totalTime = item.recipe?.total_time_minutes ?? (item.recipe?.prep_time_minutes || 0) + (item.recipe?.cook_time_minutes || 0) @@ -48,13 +49,18 @@ function MealCard({ item, dragHandleProps, isDragging, onApprove, onDeny }: { 'neutral' return ( -
+
+ {/* Keep delete visible on touch devices where hover is unavailable. */} + {onDelete && ( + + )}
{/* drag handle */}
)} -
+

{item.recipe?.name || 'Unknown Recipe'} @@ -96,23 +102,6 @@ function MealCard({ item, dragHandleProps, isDragging, onApprove, onDeny }: { )}

- {/* Approve / Deny buttons */} - {item.approval_status === 'pending' && onApprove && onDeny && ( -
- - -
- )}
@@ -128,6 +117,7 @@ function MealCard({ item, dragHandleProps, isDragging, onApprove, onDeny }: { item, onApprove, onDeny, + onDelete, onGenerate, }: { dayIndex: number @@ -135,6 +125,7 @@ function MealCard({ item, dragHandleProps, isDragging, onApprove, onDeny }: { item?: MealPlanItem onApprove?: (itemId: string) => void onDeny?: (itemId: string) => void + onDelete?: (itemId: string) => void onGenerate?: (dayIndex: number, mealType: string) => void }) { const droppableId = `slot-${dayIndex}-${mealType}` @@ -164,17 +155,18 @@ function MealCard({ item, dragHandleProps, isDragging, onApprove, onDeny }: { isDragging={dragSnapshot.isDragging} onApprove={onApprove} onDeny={onDeny} + onDelete={onDelete} />
)} ) : ( -
+
Empty {onGenerate && ( @@ -196,12 +188,14 @@ function MealCard({ item, dragHandleProps, isDragging, onApprove, onDeny }: { items, onApprove, onDeny, + onDelete, onGenerate, }: { dayIndex: number items: MealPlanItem[] onApprove?: (itemId: string) => void onDeny?: (itemId: string) => void + onDelete?: (itemId: string) => void onGenerate?: (dayIndex: number, mealType: string) => void }) { const today = new Date().getDay() @@ -222,7 +216,7 @@ function MealCard({ item, dragHandleProps, isDragging, onApprove, onDeny }: { {MEAL_TYPES.map(mealType => { const item = items.find(i => i.meal_type === mealType) return ( -
+
{mealType} @@ -232,6 +226,7 @@ function MealCard({ item, dragHandleProps, isDragging, onApprove, onDeny }: { item={item} onApprove={onApprove} onDeny={onDeny} + onDelete={onDelete} onGenerate={onGenerate} />
@@ -326,7 +321,7 @@ export default function Dashboard() { const { draggableId, destination } = result const itemId = draggableId.replace('item-', '') const [, dayStr, typeStr] = destination.droppableId.split('-') - handleDrop(itemId, parseInt(dayStr, 10), typeStr) + handleDrop(itemId, parseInt(dayStr, 10) + 1, typeStr) } async function handleApprove(itemId: string) { @@ -349,6 +344,17 @@ export default function Dashboard() { } } + async function handleDelete(itemId: string) { + if (!confirm('Delete this meal from the plan?')) return + try { + await mealPlannerApi.meals.deleteItem(itemId) + toast.success('Meal deleted') + queryClient.invalidateQueries({ queryKey: ['mealPlan'] }) + } catch (err: any) { + toast.error(err?.response?.data?.detail || 'Failed to delete meal') + } + } + async function handleGenerate(dayIndex: number, mealType: string) { if (!mealPlan) return try { @@ -436,6 +442,7 @@ export default function Dashboard() { items={items} onApprove={handleApprove} onDeny={handleDeny} + onDelete={handleDelete} onGenerate={handleGenerate} /> ))} diff --git a/frontend/src/pages/MealDetail.tsx b/frontend/src/pages/MealDetail.tsx index fd6dcfd..a6b6336 100644 --- a/frontend/src/pages/MealDetail.tsx +++ b/frontend/src/pages/MealDetail.tsx @@ -188,7 +188,9 @@ export default function MealDetail() {
- ${item.estimated_cost?.toFixed(2) || 'N/A'} + {item.estimated_cost != null + ? `$${item.estimated_cost.toFixed(2)}` + : 'No estimate'}
per serving
@@ -246,10 +248,9 @@ export default function MealDetail() {
- {ing.quantity && `${ing.quantity} `} - {ing.unit && `${ing.unit} `} + {ing.qty != null && `${ing.qty}${ing.unit ? ` ${ing.unit}` : ''} `} - {ing.name} + {ing.name || ing.ingredient?.name || 'Ingredient'} {ing.is_optional && ( (optional) )} diff --git a/frontend/src/pages/NotFound.tsx b/frontend/src/pages/NotFound.tsx new file mode 100644 index 0000000..3be54bf --- /dev/null +++ b/frontend/src/pages/NotFound.tsx @@ -0,0 +1,13 @@ +import { Compass } from 'lucide-react'; +import { EmptyState } from '../components/ui/EmptyState'; + +export default function NotFound() { + return ( + + ); +} diff --git a/frontend/src/pages/RecipeDetail.tsx b/frontend/src/pages/RecipeDetail.tsx index 1bbfdea..60209ef 100644 --- a/frontend/src/pages/RecipeDetail.tsx +++ b/frontend/src/pages/RecipeDetail.tsx @@ -158,7 +158,7 @@ export default function RecipeDetail() {
  • - {ing.qty != null && `${ing.qty} ${ing.unit || ''} `.trim()} + {ing.qty != null && `${ing.qty}${ing.unit ? ` ${ing.unit}` : ''} `} {ing.name || 'Unknown ingredient'}
  • diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 16c9204..b637e9f 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -58,6 +58,8 @@ export interface RecipeIngredient { quantity?: number unit?: string is_optional: boolean + notes?: string | null + ingredient?: { name?: string; aisle?: string } } export interface MealPlan {