add feature to edit recipes
This commit is contained in:
@@ -160,19 +160,27 @@ router.post('/', authenticateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Update recipe
|
||||
router.put('/:id', async (req, res) => {
|
||||
// Update recipe (protected route)
|
||||
router.put('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const recipe = await Recipe.findByIdAndUpdate(
|
||||
req.params.id,
|
||||
req.body,
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
if (!recipe) {
|
||||
const recipeId = req.params.id;
|
||||
|
||||
// Find the recipe first to preserve original creator
|
||||
const existingRecipe = await Recipe.findById(recipeId);
|
||||
if (!existingRecipe) {
|
||||
return res.status(404).json({ error: 'Recipe not found' });
|
||||
}
|
||||
res.json(recipe);
|
||||
|
||||
// Update the recipe while preserving the createdBy field
|
||||
const updatedRecipe = await Recipe.findByIdAndUpdate(
|
||||
recipeId,
|
||||
{ ...req.body, createdBy: existingRecipe.createdBy }, // Preserve original creator
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
res.json(updatedRecipe);
|
||||
} catch (error) {
|
||||
console.error('Error updating recipe:', error);
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
@@ -182,17 +190,12 @@ router.delete('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const recipeId = req.params.id;
|
||||
|
||||
// Find the recipe first to check ownership
|
||||
// Find the recipe first to verify it exists
|
||||
const recipe = await Recipe.findById(recipeId);
|
||||
if (!recipe) {
|
||||
return res.status(404).json({ error: 'Recipe not found' });
|
||||
}
|
||||
|
||||
// Check if user owns the recipe (only recipe creator can delete)
|
||||
if (recipe.createdBy.toString() !== req.userId) {
|
||||
return res.status(403).json({ error: 'You can only delete recipes you created' });
|
||||
}
|
||||
|
||||
// Delete the recipe
|
||||
await Recipe.findByIdAndDelete(recipeId);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import Dashboard from './pages/Dashboard';
|
||||
import Login from './pages/Login';
|
||||
import Register from './pages/Register';
|
||||
import CreateRecipe from './pages/CreateRecipe';
|
||||
import EditRecipe from './pages/EditRecipe';
|
||||
import './App.css';
|
||||
|
||||
// Protected Route component
|
||||
@@ -110,6 +111,14 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/edit-recipe/:id"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<EditRecipe />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ const IngredientAutocomplete: React.FC<IngredientAutocompleteProps> = ({
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
const [hasUserInteracted, setHasUserInteracted] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const suggestionsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -49,7 +50,8 @@ const IngredientAutocomplete: React.FC<IngredientAutocompleteProps> = ({
|
||||
const response = await api.get(`/recipes/ingredients/suggestions?q=${encodeURIComponent(query)}`);
|
||||
const suggestionData = response.data as IngredientSuggestion[];
|
||||
setSuggestions(suggestionData);
|
||||
setShowSuggestions(suggestionData.length > 0);
|
||||
// Only show suggestions if user has interacted with the component
|
||||
setShowSuggestions(hasUserInteracted && suggestionData.length > 0);
|
||||
setSelectedIndex(-1);
|
||||
} catch (error) {
|
||||
console.error('Error fetching ingredient suggestions:', error);
|
||||
@@ -74,6 +76,7 @@ const IngredientAutocomplete: React.FC<IngredientAutocompleteProps> = ({
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setHasUserInteracted(true);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
@@ -122,7 +125,9 @@ const IngredientAutocomplete: React.FC<IngredientAutocompleteProps> = ({
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
if (value && suggestions.length > 0) {
|
||||
setHasUserInteracted(true);
|
||||
// Show suggestions if we have them and user has interacted
|
||||
if (suggestions.length > 0) {
|
||||
setShowSuggestions(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -231,6 +231,35 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-edit:hover:not(:disabled) {
|
||||
background: #5a6268;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-edit:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Recipe, recipesAPI } from '../services/api';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import './RecipeModal.css';
|
||||
@@ -21,6 +22,7 @@ const RecipeModal: React.FC<RecipeModalProps> = ({
|
||||
selectedQuantity,
|
||||
}) => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||
@@ -52,8 +54,15 @@ const RecipeModal: React.FC<RecipeModalProps> = ({
|
||||
setShowDeleteConfirm(false);
|
||||
};
|
||||
|
||||
// Check if current user is the creator of this recipe
|
||||
const canDelete = user && recipe.createdBy === user.id;
|
||||
const handleEditClick = () => {
|
||||
// Navigate to edit page with recipe data
|
||||
navigate(`/edit-recipe/${recipe._id}`, { state: { recipe } });
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Any logged-in user can edit or delete recipes
|
||||
const canEdit = !!user;
|
||||
const canDelete = !!user;
|
||||
|
||||
const getDifficultyColor = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
@@ -164,15 +173,25 @@ const RecipeModal: React.FC<RecipeModalProps> = ({
|
||||
|
||||
<div className="modal-footer">
|
||||
<div className="modal-footer-buttons">
|
||||
{canDelete && (
|
||||
<button
|
||||
className="btn btn-danger btn-delete"
|
||||
onClick={handleDeleteClick}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete Recipe'}
|
||||
</button>
|
||||
)}
|
||||
<div className="modal-action-buttons">
|
||||
{canEdit && (
|
||||
<button
|
||||
className="btn btn-secondary btn-edit"
|
||||
onClick={handleEditClick}
|
||||
>
|
||||
Edit Recipe
|
||||
</button>
|
||||
)}
|
||||
{canDelete && (
|
||||
<button
|
||||
className="btn btn-danger btn-delete"
|
||||
onClick={handleDeleteClick}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete Recipe'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className={`btn ${isSelected ? 'btn-success' : 'btn-primary'} btn-large`}
|
||||
onClick={() => onAddToSelection(recipe._id)}
|
||||
|
||||
@@ -27,6 +27,7 @@ const UnitAutocomplete: React.FC<UnitAutocompleteProps> = ({
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
const [hasUserInteracted, setHasUserInteracted] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const suggestionsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -52,7 +53,8 @@ const UnitAutocomplete: React.FC<UnitAutocompleteProps> = ({
|
||||
const response = await api.get(`/recipes/ingredients/units?ingredient=${encodeURIComponent(ingredientName)}`);
|
||||
const unitData = response.data as UnitSuggestion[];
|
||||
setSuggestions(unitData);
|
||||
setShowSuggestions(unitData.length > 0);
|
||||
// Only show suggestions if user has interacted with the component
|
||||
setShowSuggestions(hasUserInteracted && unitData.length > 0);
|
||||
setSelectedIndex(-1);
|
||||
} catch (error) {
|
||||
console.error('Error fetching unit suggestions:', error);
|
||||
@@ -77,6 +79,7 @@ const UnitAutocomplete: React.FC<UnitAutocompleteProps> = ({
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setHasUserInteracted(true);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
@@ -125,7 +128,9 @@ const UnitAutocomplete: React.FC<UnitAutocompleteProps> = ({
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
if (ingredient && suggestions.length > 0) {
|
||||
setHasUserInteracted(true);
|
||||
// Show suggestions if we have them and user has interacted
|
||||
if (suggestions.length > 0) {
|
||||
setShowSuggestions(true);
|
||||
}
|
||||
};
|
||||
|
||||
492
frontend/src/pages/EditRecipe.tsx
Normal file
492
frontend/src/pages/EditRecipe.tsx
Normal file
@@ -0,0 +1,492 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { Recipe, recipesAPI } from '../services/api';
|
||||
import IngredientAutocomplete from '../components/IngredientAutocomplete';
|
||||
import UnitAutocomplete from '../components/UnitAutocomplete';
|
||||
import './CreateRecipe.css'; // Reuse the same styles
|
||||
|
||||
interface Ingredient {
|
||||
name: string;
|
||||
amount: number;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface Instruction {
|
||||
step: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface RecipeFormData {
|
||||
title: string;
|
||||
description: string;
|
||||
ingredients: Ingredient[];
|
||||
instructions: Instruction[];
|
||||
servings: number;
|
||||
prepTime: number;
|
||||
cookTime: number;
|
||||
category: 'breakfast' | 'lunch' | 'dinner' | 'dessert' | 'snack' | 'appetizer';
|
||||
difficulty: 'easy' | 'medium' | 'hard';
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
const EditRecipe: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const location = useLocation();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [fetchingRecipe, setFetchingRecipe] = useState(true);
|
||||
|
||||
const [formData, setFormData] = useState<RecipeFormData>({
|
||||
title: '',
|
||||
description: '',
|
||||
ingredients: [{ name: '', amount: 0, unit: '' }],
|
||||
instructions: [{ step: 1, description: '' }],
|
||||
servings: 4,
|
||||
prepTime: 15,
|
||||
cookTime: 30,
|
||||
category: 'dinner',
|
||||
difficulty: 'medium',
|
||||
imageUrl: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loadRecipe = async () => {
|
||||
try {
|
||||
setFetchingRecipe(true);
|
||||
|
||||
// Try to get recipe from location state first (passed from modal)
|
||||
if (location.state?.recipe) {
|
||||
const recipe = location.state.recipe as Recipe;
|
||||
populateForm(recipe);
|
||||
} else if (id) {
|
||||
// Fallback: fetch recipe by ID
|
||||
const response = await recipesAPI.getById(id);
|
||||
const recipe = response.data as Recipe;
|
||||
populateForm(recipe);
|
||||
} else {
|
||||
setError('No recipe data available');
|
||||
return;
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.response?.data?.error || 'Failed to load recipe');
|
||||
} finally {
|
||||
setFetchingRecipe(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadRecipe();
|
||||
}, [id, location.state]);
|
||||
|
||||
const populateForm = (recipe: Recipe) => {
|
||||
setFormData({
|
||||
title: recipe.title,
|
||||
description: recipe.description,
|
||||
ingredients: recipe.ingredients.length > 0 ? recipe.ingredients : [{ name: '', amount: 0, unit: '' }],
|
||||
instructions: recipe.instructions.length > 0 ? recipe.instructions : [{ step: 1, description: '' }],
|
||||
servings: recipe.servings,
|
||||
prepTime: recipe.prepTime,
|
||||
cookTime: recipe.cookTime,
|
||||
category: recipe.category,
|
||||
difficulty: recipe.difficulty,
|
||||
imageUrl: recipe.imageUrl || ''
|
||||
});
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof RecipeFormData, value: any) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
// Clear messages when user starts editing
|
||||
if (error) setError(null);
|
||||
if (success) setSuccess(null);
|
||||
};
|
||||
|
||||
const updateIngredient = (index: number, field: keyof Ingredient, value: string | number) => {
|
||||
const updatedIngredients = [...formData.ingredients];
|
||||
updatedIngredients[index] = {
|
||||
...updatedIngredients[index],
|
||||
[field]: value
|
||||
};
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
ingredients: updatedIngredients
|
||||
}));
|
||||
};
|
||||
|
||||
const addIngredient = () => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
ingredients: [...prev.ingredients, { name: '', amount: 0, unit: '' }]
|
||||
}));
|
||||
};
|
||||
|
||||
const removeIngredient = (index: number) => {
|
||||
if (formData.ingredients.length > 1) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
ingredients: prev.ingredients.filter((_, i) => i !== index)
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const updateInstruction = (index: number, value: string) => {
|
||||
const updatedInstructions = [...formData.instructions];
|
||||
updatedInstructions[index] = {
|
||||
...updatedInstructions[index],
|
||||
description: value
|
||||
};
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
instructions: updatedInstructions
|
||||
}));
|
||||
};
|
||||
|
||||
const addInstruction = () => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
instructions: [...prev.instructions, { step: prev.instructions.length + 1, description: '' }]
|
||||
}));
|
||||
};
|
||||
|
||||
const removeInstruction = (index: number) => {
|
||||
if (formData.instructions.length > 1) {
|
||||
const updatedInstructions = formData.instructions
|
||||
.filter((_, i) => i !== index)
|
||||
.map((instruction, i) => ({ ...instruction, step: i + 1 }));
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
instructions: updatedInstructions
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = (): string | null => {
|
||||
if (!formData.title.trim()) return 'Recipe title is required';
|
||||
if (!formData.description.trim()) return 'Recipe description is required';
|
||||
if (formData.ingredients.some(ing => !ing.name.trim() || ing.amount <= 0 || !ing.unit.trim())) {
|
||||
return 'All ingredients must have a name, amount, and unit';
|
||||
}
|
||||
if (formData.instructions.some(inst => !inst.description.trim())) {
|
||||
return 'All instruction steps must have a description';
|
||||
}
|
||||
if (formData.servings < 1) return 'Servings must be at least 1';
|
||||
if (formData.prepTime < 0) return 'Prep time cannot be negative';
|
||||
if (formData.cookTime < 0) return 'Cook time cannot be negative';
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const validationError = validateForm();
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
if (!id) {
|
||||
setError('Recipe ID is missing');
|
||||
return;
|
||||
}
|
||||
|
||||
await recipesAPI.update(id, formData);
|
||||
setSuccess('Recipe updated successfully!');
|
||||
|
||||
// Redirect back to dashboard after a short delay
|
||||
setTimeout(() => {
|
||||
navigate('/dashboard');
|
||||
}, 2000);
|
||||
} catch (error: any) {
|
||||
setError(error.response?.data?.error || 'Failed to update recipe');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
navigate('/dashboard');
|
||||
};
|
||||
|
||||
if (fetchingRecipe) {
|
||||
return (
|
||||
<div className="create-recipe-container">
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>Loading recipe...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && !formData.title) {
|
||||
return (
|
||||
<div className="create-recipe-container">
|
||||
<div className="error-container">
|
||||
<p className="error-message">{error}</p>
|
||||
<button className="btn btn-secondary" onClick={handleCancel}>
|
||||
Back to Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="create-recipe-container">
|
||||
<div className="create-recipe-header">
|
||||
<h1>Edit Recipe</h1>
|
||||
<p>Update your recipe details</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="create-recipe-form">
|
||||
{/* Basic Information */}
|
||||
<div className="form-section">
|
||||
<h2>Basic Information</h2>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="title">Recipe Title *</label>
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => handleInputChange('title', e.target.value)}
|
||||
placeholder="Enter recipe title"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="description">Description *</label>
|
||||
<textarea
|
||||
id="description"
|
||||
value={formData.description}
|
||||
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||
placeholder="Describe your recipe"
|
||||
rows={3}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="imageUrl">Image URL (optional)</label>
|
||||
<input
|
||||
id="imageUrl"
|
||||
type="url"
|
||||
value={formData.imageUrl}
|
||||
onChange={(e) => handleInputChange('imageUrl', e.target.value)}
|
||||
placeholder="https://example.com/image.jpg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recipe Details */}
|
||||
<div className="form-section">
|
||||
<h2>Recipe Details</h2>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="servings">Servings *</label>
|
||||
<input
|
||||
id="servings"
|
||||
type="number"
|
||||
min="1"
|
||||
value={formData.servings}
|
||||
onChange={(e) => handleInputChange('servings', parseInt(e.target.value) || 1)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="prepTime">Prep Time (minutes) *</label>
|
||||
<input
|
||||
id="prepTime"
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.prepTime}
|
||||
onChange={(e) => handleInputChange('prepTime', parseInt(e.target.value) || 0)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="cookTime">Cook Time (minutes) *</label>
|
||||
<input
|
||||
id="cookTime"
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.cookTime}
|
||||
onChange={(e) => handleInputChange('cookTime', parseInt(e.target.value) || 0)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="category">Category *</label>
|
||||
<select
|
||||
id="category"
|
||||
value={formData.category}
|
||||
onChange={(e) => handleInputChange('category', e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="breakfast">Breakfast</option>
|
||||
<option value="lunch">Lunch</option>
|
||||
<option value="dinner">Dinner</option>
|
||||
<option value="dessert">Dessert</option>
|
||||
<option value="snack">Snack</option>
|
||||
<option value="appetizer">Appetizer</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="difficulty">Difficulty *</label>
|
||||
<select
|
||||
id="difficulty"
|
||||
value={formData.difficulty}
|
||||
onChange={(e) => handleInputChange('difficulty', e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="easy">Easy</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="hard">Hard</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ingredients */}
|
||||
<div className="form-section">
|
||||
<h2>Ingredients</h2>
|
||||
|
||||
{formData.ingredients.map((ingredient, index) => (
|
||||
<div key={index} className="ingredient-row">
|
||||
<div className="form-group ingredient-name-group">
|
||||
<IngredientAutocomplete
|
||||
value={ingredient.name}
|
||||
onChange={(value) => updateIngredient(index, 'name', value)}
|
||||
placeholder="Ingredient name"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
placeholder="Amount"
|
||||
value={ingredient.amount || ''}
|
||||
onChange={(e) => updateIngredient(index, 'amount', parseFloat(e.target.value) || 0)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<UnitAutocomplete
|
||||
value={ingredient.unit}
|
||||
onChange={(value) => updateIngredient(index, 'unit', value)}
|
||||
ingredient={ingredient.name}
|
||||
placeholder="Unit (cups, tsp, etc.)"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger btn-small"
|
||||
onClick={() => removeIngredient(index)}
|
||||
disabled={formData.ingredients.length === 1}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={addIngredient}
|
||||
>
|
||||
Add Ingredient
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="form-section">
|
||||
<h2>Instructions</h2>
|
||||
|
||||
{formData.instructions.map((instruction, index) => (
|
||||
<div key={index} className="instruction-row">
|
||||
<div className="instruction-number">{instruction.step}</div>
|
||||
<div className="form-group instruction-input-group">
|
||||
<textarea
|
||||
placeholder="Describe this step"
|
||||
value={instruction.description}
|
||||
onChange={(e) => updateInstruction(index, e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger btn-small"
|
||||
onClick={() => removeInstruction(index)}
|
||||
disabled={formData.instructions.length === 1}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={addInstruction}
|
||||
>
|
||||
Add Step
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="success-message">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Buttons */}
|
||||
<div className="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={handleCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Updating Recipe...' : 'Update Recipe'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditRecipe;
|
||||
Reference in New Issue
Block a user