diff --git a/app/javascript/components/App.tsx b/app/javascript/components/App.tsx index 583048a..eb4fe4b 100644 --- a/app/javascript/components/App.tsx +++ b/app/javascript/components/App.tsx @@ -6,9 +6,10 @@ import { store } from '../store/store' import Layout from "./Layout"; import Home from "./Home"; import Search from "./Search"; -import Recipes from "./recipes/Index"; +import Recipes from "./recipes/RecipeIndex"; import Foods from "./foods/Index"; -import NewRecipeForm from './recipes/New'; +import NewRecipeForm from './recipes/RecipeNew'; +import RecipeEdit from './recipes/RecipeEdit'; const App: React.FC = () => { return ( @@ -17,8 +18,11 @@ const App: React.FC = () => { }> } /> } /> + } /> } /> + } /> + } /> diff --git a/app/javascript/components/recipes/RecipeEdit.tsx b/app/javascript/components/recipes/RecipeEdit.tsx new file mode 100644 index 0000000..dd33dc0 --- /dev/null +++ b/app/javascript/components/recipes/RecipeEdit.tsx @@ -0,0 +1,68 @@ +import React, { useEffect, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '../../store/hooks'; +import { updateRecipe } from '../../store/recipesSlice'; +import { RecipeFormData } from './RecipeNew'; +import useCsrfToken from '../../hooks/useCsrfToken'; +import { useNavigate, useParams } from 'react-router-dom'; +import RecipeForm from './RecipeForm'; +import { fetchFoods } from '../../store/foodsSlice'; + +const EditRecipeForm: React.FC = () => { + const dispatch = useAppDispatch(); + const csrfToken = useCsrfToken(); + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + + const recipe = useAppSelector(state => + state.recipes.items.find(recipe => recipe.id === Number(id)) + ); + + const [formData, setFormData] = useState({ + name: '', + description: '', + ingredients_attributes: [] + }); + + useEffect(() => { + dispatch(fetchFoods()); + + if (recipe) { + setFormData({ + name: recipe.name, + description: recipe.description, + ingredients_attributes: recipe.ingredients + }); + } + }, [dispatch, recipe]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + dispatch(updateRecipe({ + id: Number(id), + data: formData, + csrfToken + })) + .unwrap() + .then(() => { + navigate('/recipes'); + }) + .catch((error: Error) => { + alert('Failed to update recipe: ' + error.message); + }); + }; + + if (!recipe) { + return Recipe not found; + } + + return ( + + ); +}; + +export default EditRecipeForm; diff --git a/app/javascript/components/recipes/New.tsx b/app/javascript/components/recipes/RecipeForm.tsx similarity index 65% rename from app/javascript/components/recipes/New.tsx rename to app/javascript/components/recipes/RecipeForm.tsx index 4a5a461..d5d6148 100644 --- a/app/javascript/components/recipes/New.tsx +++ b/app/javascript/components/recipes/RecipeForm.tsx @@ -1,32 +1,16 @@ -import React, { useEffect, useState } from 'react'; -import { useAppDispatch, useAppSelector } from '../../store/hooks'; -import { createRecipe } from '../../store/recipesSlice'; -import { Ingredient } from '../../types/types'; -import useCsrfToken from '../../hooks/useCsrfToken'; -import { fetchFoods } from '../../store/foodsSlice'; +import React from 'react'; +import { RecipeFormData } from './RecipeNew'; +import { useAppSelector } from '../../store/hooks'; -export interface RecipeFormData { - name: string; - description: string; - ingredients_attributes: Ingredient[]; +interface RecipeFormProps { + formData: RecipeFormData; + setFormData: (data: RecipeFormData) => void; + onSubmit: (e: React.FormEvent) => void; + submitButtonText: string; } -const NewRecipeForm: React.FC = () => { - const dispatch = useAppDispatch(); - const csrfToken = useCsrfToken(); - const { items: foods, status: foods_status } = useAppSelector((state) => state.foods); - - useEffect(() => { - if (foods_status === 'idle') { - dispatch(fetchFoods()) - } - }, [dispatch]); - - const [formData, setFormData] = useState({ - name: '', - description: '', - ingredients_attributes: [] - }); +const RecipeForm: React.FC = ({ formData, setFormData, onSubmit, submitButtonText }) => { + const { items: foods } = useAppSelector((state) => state.foods); const addIngredient = () => { setFormData({ @@ -44,8 +28,8 @@ const NewRecipeForm: React.FC = () => { ingredients_attributes: formData.ingredients_attributes.filter((_, index) => index !== indexToDelete) }); }; - - const handleIngredientChange = (index: number, field: keyof Ingredient, value: string | number) => { + + const handleIngredientChange = (index: number, field: 'food_id' | 'measurement', value: string | number) => { const newIngredients = [...formData.ingredients_attributes]; newIngredients[index] = { food_id: newIngredients[index]?.food_id || 0, @@ -58,26 +42,8 @@ const NewRecipeForm: React.FC = () => { }); }; - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - dispatch(createRecipe({ - data: formData, - csrfToken - })) - .unwrap() - .then(() => { - setFormData({ - name: '', - description: '', - ingredients_attributes: [] - }); - }) - .catch((error: Error) => { - alert('Failed to create recipe: ' + error.message); - }); - }; return ( - + Recipe Name: { type="submit" className="bg-green-500 text-white px-6 py-2 rounded" > - Create Recipe + {submitButtonText} ); }; -export default NewRecipeForm; +export default RecipeForm; diff --git a/app/javascript/components/recipes/Index.tsx b/app/javascript/components/recipes/RecipeIndex.tsx similarity index 83% rename from app/javascript/components/recipes/Index.tsx rename to app/javascript/components/recipes/RecipeIndex.tsx index 31f133e..e74fc7c 100644 --- a/app/javascript/components/recipes/Index.tsx +++ b/app/javascript/components/recipes/RecipeIndex.tsx @@ -40,6 +40,14 @@ const RecipeTable: React.FC = () => { {ingredient.food_name} ))} + + + Edit + + ))} diff --git a/app/javascript/components/recipes/RecipeNew.tsx b/app/javascript/components/recipes/RecipeNew.tsx new file mode 100644 index 0000000..17821fb --- /dev/null +++ b/app/javascript/components/recipes/RecipeNew.tsx @@ -0,0 +1,65 @@ +import React, { useEffect, useState } from 'react'; +import { useAppDispatch } from '../../store/hooks'; +import { createRecipe } from '../../store/recipesSlice'; +import { fetchFoods } from '../../store/foodsSlice'; +import useCsrfToken from '../../hooks/useCsrfToken'; +import { useNavigate } from 'react-router-dom'; +import RecipeForm from './RecipeForm'; +import { Ingredient } from '../../types/types'; + +export interface RecipeFormData { + name: string; + description: string; + ingredients_attributes: Ingredient[]; +} + +const NewRecipeForm: React.FC = () => { + const dispatch = useAppDispatch(); + const csrfToken = useCsrfToken(); + const navigate = useNavigate(); + + useEffect(() => { + clearForm(); + dispatch(fetchFoods()); + }, [dispatch]); + + const [formData, setFormData] = useState({ + name: '', + description: '', + ingredients_attributes: [] + }); + + const clearForm = () => { + setFormData({ + name: '', + description: '', + ingredients_attributes: [] + }); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + dispatch(createRecipe({ + data: formData, + csrfToken + })) + .unwrap() + .then(() => { + navigate('/recipes'); + }) + .catch((error: Error) => { + alert('Failed to create recipe: ' + error.message); + }); + }; + + return ( + + ); +}; + +export default NewRecipeForm; diff --git a/app/javascript/store/recipesSlice.ts b/app/javascript/store/recipesSlice.ts index e89e2fd..1ada9fd 100644 --- a/app/javascript/store/recipesSlice.ts +++ b/app/javascript/store/recipesSlice.ts @@ -1,7 +1,15 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' -import { RecipeFormData } from '../components/recipes/New'; +import { RecipeFormData } from '../components/recipes/RecipeNew'; import { Recipe } from '../types/types'; +export const fetchRecipes = createAsyncThunk( + 'recipes/fetchRecipes', + async () => { + const response = await fetch('/api/recipes'); + return response.json(); + } +); + export const createRecipe = createAsyncThunk( 'recipes/create', async ({ data, csrfToken }: { data: RecipeFormData; csrfToken: string }) => { @@ -23,11 +31,17 @@ export const createRecipe = createAsyncThunk( } ); -export const fetchRecipes = createAsyncThunk( - 'recipes/fetchRecipes', - async () => { - const response = await fetch('/api/recipes'); - return response.json(); +export const updateRecipe = createAsyncThunk( + 'recipes/update', + async ({ id, data, csrfToken }: { id: number; data: RecipeFormData; csrfToken: string }) => { + await fetch(`/api/recipes/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify(data) + }) } ); @@ -60,6 +74,12 @@ const recipesSlice = createSlice({ state.status = 'failed' state.error = action.error.message || null }) + .addCase(createRecipe.fulfilled, (state, action) => { + if (state.status !== 'idle') { + state.items.push(action.payload.recipe) + state.status = 'succeeded' + } + }) }, }) diff --git a/app/javascript/types/types.ts b/app/javascript/types/types.ts index 0d249e2..e379b56 100644 --- a/app/javascript/types/types.ts +++ b/app/javascript/types/types.ts @@ -8,5 +8,6 @@ export interface Ingredient { export interface Recipe { id: number; name: string; + description: string; ingredients: Ingredient[]; }