diff --git a/TODO.md b/TODO.md index df8b4e3..7bd9d2c 100644 --- a/TODO.md +++ b/TODO.md @@ -20,7 +20,7 @@ - [x] Configure Tailwind CSS - [x] Set up React Router - [x] Create recipe list page -- [ ] Create recipe detail/edit page +- [x] Create recipe detail/edit page - [ ] Implement cook mode UI ### Features @@ -46,6 +46,17 @@ ## ✅ Completed Tasks ### 2026-03-24 +- **Recipe detail/edit page implementation** + - Created useRecipe hook for fetching single recipe with loading/error states + - Created RecipeForm component with full validation (title, ingredients, instructions required) + - Implemented RecipeDetailPage with view/edit modes + - Added create new recipe functionality (/recipe/new route) + - Implemented edit existing recipe with form pre-population + - Added delete recipe with confirmation dialog + - Included metadata display (servings, prep time, cook time) + - Added navigation to cook mode and back to list + - Verified TypeScript compilation and Vite build succeed + - **Recipe list page implementation** - Created API client service (src/services/api.ts) with all CRUD operations - Created useRecipes hook for data fetching with search and pagination diff --git a/frontend/src/components/RecipeForm.tsx b/frontend/src/components/RecipeForm.tsx new file mode 100644 index 0000000..55bf4ea --- /dev/null +++ b/frontend/src/components/RecipeForm.tsx @@ -0,0 +1,278 @@ +import { useState, useEffect } from 'react'; +import type { Recipe } from '../types/recipe'; + +interface RecipeFormProps { + recipe?: Recipe | null; + onSubmit: (data: RecipeFormData) => Promise; + onCancel: () => void; + submitLabel?: string; +} + +export interface RecipeFormData { + title: string; + description?: string; + ingredients: string[]; + instructions: string[]; + source_url?: string; + notes?: string; + servings?: number; + prep_time_minutes?: number; + cook_time_minutes?: number; +} + +/** + * RecipeForm - Form component for creating/editing recipes + */ +export function RecipeForm({ recipe, onSubmit, onCancel, submitLabel = 'Save Recipe' }: RecipeFormProps) { + // Form state + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [ingredientsText, setIngredientsText] = useState(''); + const [instructionsText, setInstructionsText] = useState(''); + const [sourceUrl, setSourceUrl] = useState(''); + const [notes, setNotes] = useState(''); + const [servings, setServings] = useState(''); + const [prepTime, setPrepTime] = useState(''); + const [cookTime, setCookTime] = useState(''); + + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + // Populate form from recipe prop + useEffect(() => { + if (recipe) { + setTitle(recipe.title || ''); + setDescription(recipe.description || ''); + setIngredientsText(recipe.ingredients.join('\n')); + setInstructionsText(recipe.instructions.join('\n')); + setSourceUrl(recipe.source_url || ''); + setNotes(recipe.notes || ''); + setServings(recipe.servings?.toString() || ''); + setPrepTime(recipe.prep_time_minutes?.toString() || ''); + setCookTime(recipe.cook_time_minutes?.toString() || ''); + } + }, [recipe]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + // Validation + if (!title.trim()) { + setError('Title is required'); + return; + } + + const ingredientsList = ingredientsText + .split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0); + + if (ingredientsList.length === 0) { + setError('At least one ingredient is required'); + return; + } + + const instructionsList = instructionsText + .split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0); + + if (instructionsList.length === 0) { + setError('At least one instruction step is required'); + return; + } + + const data: RecipeFormData = { + title: title.trim(), + description: description.trim() || undefined, + ingredients: ingredientsList, + instructions: instructionsList, + source_url: sourceUrl.trim() || undefined, + notes: notes.trim() || undefined, + servings: servings ? parseInt(servings, 10) : undefined, + prep_time_minutes: prepTime ? parseInt(prepTime, 10) : undefined, + cook_time_minutes: cookTime ? parseInt(cookTime, 10) : undefined, + }; + + try { + setIsSubmitting(true); + await onSubmit(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to save recipe'); + setIsSubmitting(false); + } + }; + + return ( +
+ {error && ( +
+ {error} +
+ )} + + {/* Title */} +
+ + setTitle(e.target.value)} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" + placeholder="e.g., Chocolate Chip Cookies" + required + /> +
+ + {/* Description */} +
+ +