diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0d95db2..ee1ccf0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,5 @@ class ApplicationController < ActionController::Base # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. allow_browser versions: :modern + end diff --git a/app/controllers/recipes_controller.rb b/app/controllers/recipes_controller.rb index 3d54c7a..5488352 100644 --- a/app/controllers/recipes_controller.rb +++ b/app/controllers/recipes_controller.rb @@ -4,4 +4,28 @@ def index @recipes = Recipe.all.preload(ingredients: :food) end + def create + @recipe = Recipe.new(recipe_params) + if @recipe.save + render :create, status: :created + else + render :errors, status: :unprocessable_entity + end + end + + def update + @recipe = Recipe.find(params[:id]) + if @recipe.update(recipe_params) + render :create, status: :ok + else + render :errors, status: :unprocessable_entity + end + end + + private + + def recipe_params + params.require(:recipe).permit(:name, :description, ingredients_attributes: [:id, :food_id, :amount, :unit, :_destroy]) + end + end diff --git a/app/javascript/components/App.tsx b/app/javascript/components/App.tsx index b8cdc3d..6fa279a 100644 --- a/app/javascript/components/App.tsx +++ b/app/javascript/components/App.tsx @@ -8,6 +8,7 @@ import Home from "./Home"; import Search from "./Search"; import RecipeTable from "./RecipeTable"; import FoodsTable from "./FoodsTable"; +import NewRecipeForm from './NewRecipeForm'; const App: React.FC = () => { return ( @@ -17,7 +18,8 @@ const App: React.FC = () => { } /> } /> } /> - } /> + } /> + } /> diff --git a/app/javascript/components/FoodsTable.tsx b/app/javascript/components/FoodsTable.tsx index bbf1ee3..ad0af83 100644 --- a/app/javascript/components/FoodsTable.tsx +++ b/app/javascript/components/FoodsTable.tsx @@ -1,42 +1,22 @@ -import React, { useState, useEffect } from 'react'; - -interface Food { - id: number; - name: string; - created_at: string; - modified_at: string; -} +import React, { useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from '../store/hooks' +import { fetchFoods } from '../store/foodsSlice' const FetchDataComponent: React.FC = () => { - const [foods, setFoods] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const dispatch = useAppDispatch() + const { items: foods, status, error } = useAppSelector((state) => state.foods) - // Fetch data when the component mounts useEffect(() => { - const fetchData = async () => { - try { - const response = await fetch('http://localhost:3000/api/foods'); - if (!response.ok) { - throw new Error('Failed to fetch data'); - } - const result: Food[] = await response.json(); - setFoods(result); // Set the fetched data to the state - setLoading(false); // Data loaded, set loading to false - } catch (err: any) { - setError(err.message); // If error occurs, set the error message - setLoading(false); // Stop loading in case of error - } - }; - - fetchData(); - }, []); // Empty dependency array to run only once on mount + if (status === 'idle') { + dispatch(fetchFoods()) + } + }, [dispatch]) - if (loading) { + if (status === 'loading') { return
Loading...
; } - if (error) { + if (status === 'failed') { return
Error: {error}
; } @@ -47,7 +27,6 @@ const FetchDataComponent: React.FC = () => { {foods.map((item) => (
  • {item.id}: {item.name} - {item.created_at}
  • ))} diff --git a/app/javascript/components/Layout.tsx b/app/javascript/components/Layout.tsx index fc63002..cc0569c 100644 --- a/app/javascript/components/Layout.tsx +++ b/app/javascript/components/Layout.tsx @@ -24,7 +24,7 @@ const Nav = (): JSX.Element => { - + ) diff --git a/app/javascript/components/NewRecipeForm.tsx b/app/javascript/components/NewRecipeForm.tsx new file mode 100644 index 0000000..63e0c26 --- /dev/null +++ b/app/javascript/components/NewRecipeForm.tsx @@ -0,0 +1,133 @@ +import React, { useState } from 'react'; +import { useAppDispatch, useAppSelector } from '../store/hooks'; +import { createRecipe } from '../store/recipesSlice'; +import { Ingredient } from '../types/types'; + +const getCsrfToken = (): string => { + const element = document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement; + return element ? element.content : ''; +}; + +export interface RecipeFormData { + name: string; + description: string; + ingredients_attributes: Ingredient[]; +} + +const NewRecipeForm: React.FC = () => { + const dispatch = useAppDispatch(); + const { items: foods } = useAppSelector((state) => state.foods); + const csrfToken = getCsrfToken(); + + const [formData, setFormData] = useState({ + name: '', + description: '', + ingredients_attributes: [] + }); + + const addIngredient = () => { + setFormData({ + ...formData, + ingredients_attributes: [ + ...formData.ingredients_attributes, + { food_id: 0, measurement: '' } + ] + }); + }; + + const handleIngredientChange = (index: number, field: keyof Ingredient, value: string | number) => { + const newIngredients = [...formData.ingredients_attributes]; + newIngredients[index] = { + food_id: newIngredients[index]?.food_id || 0, + measurement: newIngredients[index]?.measurement || '', + [field]: value + }; + setFormData({ + ...formData, + ingredients_attributes: newIngredients + }); + }; + + 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 ( +
    +
    + + setFormData({...formData, name: e.target.value})} + className="w-full p-2 border rounded" + /> +
    + +
    + +