Skip to content

Commit

Permalink
add update and form components
Browse files Browse the repository at this point in the history
  • Loading branch information
Kyle Zarazan committed Nov 21, 2024
1 parent c8ed5cb commit 5e2d735
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 57 deletions.
8 changes: 6 additions & 2 deletions app/javascript/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -17,8 +18,11 @@ const App: React.FC = () => {
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="search" element={<Search />} />

<Route path="recipes" element={<Recipes />} />
<Route path="recipes/new" element={<NewRecipeForm />} />
<Route path="recipes/:id/edit" element={<RecipeEdit />} />

<Route path="foods" element={<Foods />} />
</Route>
</Routes>
Expand Down
68 changes: 68 additions & 0 deletions app/javascript/components/recipes/RecipeEdit.tsx
Original file line number Diff line number Diff line change
@@ -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<RecipeFormData>({
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 <div>Recipe not found</div>;
}

return (
<RecipeForm
formData={formData}
setFormData={setFormData}
onSubmit={handleSubmit}
submitButtonText="Update Recipe"
/>
);
};

export default EditRecipeForm;
Original file line number Diff line number Diff line change
@@ -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<RecipeFormData>({
name: '',
description: '',
ingredients_attributes: []
});
const RecipeForm: React.FC<RecipeFormProps> = ({ formData, setFormData, onSubmit, submitButtonText }) => {
const { items: foods } = useAppSelector((state) => state.foods);

const addIngredient = () => {
setFormData({
Expand All @@ -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,
Expand All @@ -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 (
<form onSubmit={handleSubmit} className="max-w-2xl mx-auto p-4">
<form onSubmit={onSubmit} className="max-w-2xl mx-auto p-4">
<div className="mb-4">
<label className="block mb-2">Recipe Name:</label>
<input
Expand Down Expand Up @@ -142,10 +108,10 @@ const NewRecipeForm: React.FC = () => {
type="submit"
className="bg-green-500 text-white px-6 py-2 rounded"
>
Create Recipe
{submitButtonText}
</button>
</form>
);
};

export default NewRecipeForm;
export default RecipeForm;
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ const RecipeTable: React.FC = () => {
<span key={ingredient.id}>{ingredient.food_name}</span>
))}
</td>
<td>
<Link
to={`/recipes/${recipe.id}/edit`}
className="bg-green-500 text-white px-3 py-1 rounded hover:bg-green-600"
>
Edit
</Link>
</td>
</tr>
))}
</tbody>
Expand Down
65 changes: 65 additions & 0 deletions app/javascript/components/recipes/RecipeNew.tsx
Original file line number Diff line number Diff line change
@@ -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<RecipeFormData>({
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 (
<RecipeForm
formData={formData}
setFormData={setFormData}
onSubmit={handleSubmit}
submitButtonText="Create Recipe"
/>
);
};

export default NewRecipeForm;
32 changes: 26 additions & 6 deletions app/javascript/store/recipesSlice.ts
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand All @@ -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)
})
}
);

Expand Down Expand Up @@ -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'
}
})
},
})

Expand Down
1 change: 1 addition & 0 deletions app/javascript/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export interface Ingredient {
export interface Recipe {
id: number;
name: string;
description: string;
ingredients: Ingredient[];
}

0 comments on commit 5e2d735

Please sign in to comment.