From 42727c125599cdf35ad6287c8457813c5591b810 Mon Sep 17 00:00:00 2001 From: Kyle Zarazan Date: Tue, 19 Nov 2024 14:02:44 -0700 Subject: [PATCH] Add redux --- app/javascript/components/App.tsx | 21 +++--- app/javascript/components/FoodsTable.tsx | 58 ++++++++++++++++ app/javascript/components/Layout.tsx | 6 +- app/javascript/components/RecipeTable.tsx | 82 ++++++++++------------- app/javascript/store/hooks.ts | 5 ++ app/javascript/store/recipesSlice.ts | 55 +++++++++++++++ app/javascript/store/store.ts | 11 +++ app/models/recipe.rb | 2 + app/views/layouts/application.html.erb | 2 +- 9 files changed, 184 insertions(+), 58 deletions(-) create mode 100644 app/javascript/components/FoodsTable.tsx create mode 100644 app/javascript/store/hooks.ts create mode 100644 app/javascript/store/recipesSlice.ts create mode 100644 app/javascript/store/store.ts diff --git a/app/javascript/components/App.tsx b/app/javascript/components/App.tsx index a7ceebf..b8cdc3d 100644 --- a/app/javascript/components/App.tsx +++ b/app/javascript/components/App.tsx @@ -1,21 +1,26 @@ import React from 'react'; import { Routes, Route } from "react-router-dom"; +import { Provider } from 'react-redux' +import { store } from '../store/store' import Layout from "./Layout"; import Home from "./Home"; import Search from "./Search"; import RecipeTable from "./RecipeTable"; +import FoodsTable from "./FoodsTable"; const App: React.FC = () => { return ( - - }> - } /> - } /> - } /> - } /> - - + + + }> + } /> + } /> + } /> + } /> + + + ); }; diff --git a/app/javascript/components/FoodsTable.tsx b/app/javascript/components/FoodsTable.tsx new file mode 100644 index 0000000..bbf1ee3 --- /dev/null +++ b/app/javascript/components/FoodsTable.tsx @@ -0,0 +1,58 @@ +import React, { useState, useEffect } from 'react'; + +interface Food { + id: number; + name: string; + created_at: string; + modified_at: string; +} + +const FetchDataComponent: React.FC = () => { + const [foods, setFoods] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // 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 (loading) { + return
Loading...
; + } + + if (error) { + return
Error: {error}
; + } + + return ( +
+

Fetched Data:

+
    + {foods.map((item) => ( +
  • + {item.id}: {item.name} + {item.created_at} +
  • + ))} +
+
+ ); +}; + +export default FetchDataComponent; diff --git a/app/javascript/components/Layout.tsx b/app/javascript/components/Layout.tsx index 66065b1..fc63002 100644 --- a/app/javascript/components/Layout.tsx +++ b/app/javascript/components/Layout.tsx @@ -5,12 +5,12 @@ const Layout = (): JSX.Element => { return (
-

ICON

+

ICON

-
+
diff --git a/app/javascript/components/RecipeTable.tsx b/app/javascript/components/RecipeTable.tsx index 0b63f77..dcc9acd 100644 --- a/app/javascript/components/RecipeTable.tsx +++ b/app/javascript/components/RecipeTable.tsx @@ -1,58 +1,48 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; +import { useEffect } from 'react' +import { useAppDispatch, useAppSelector } from '../store/hooks' +import { fetchRecipes } from '../store/recipesSlice' -interface Recipe { - id: number; - name: string; - created_at: string; - modified_at: string; -} - -const FetchDataComponent: React.FC = () => { - const [recipes, setRecipes] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); +const RecipeTable: React.FC = () => { + const dispatch = useAppDispatch() + const { items: recipes, status, error } = useAppSelector((state) => state.recipes) - // Fetch data when the component mounts useEffect(() => { - const fetchData = async () => { - try { - const response = await fetch('http://localhost:3000/api/recipes'); - if (!response.ok) { - throw new Error('Failed to fetch data'); - } - const result: Recipe[] = await response.json(); - setRecipes(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 - } - }; + dispatch(fetchRecipes()) + }, [dispatch]) - fetchData(); - }, []); // Empty dependency array to run only once on mount - - if (loading) { - return
Loading...
; + if (status === 'loading') { + return
Loading...
} - if (error) { - return
Error: {error}
; + if (status === 'failed') { + return
Error: {error}
} return (
-

Fetched Data:

-
    - {recipes.map((item) => ( -
  • - {item.id}: {item.name} - {item.created_at} -
  • - ))} -
+ + + + + + + + + {recipes.map((recipe) => ( + + + + + ))} + +
Recipe NameIngredients
{recipe.name} + {recipe.ingredients?.map((ingredient) => ( + {ingredient.food_name} + ))} +
- ); -}; + ) +} -export default FetchDataComponent; +export default RecipeTable diff --git a/app/javascript/store/hooks.ts b/app/javascript/store/hooks.ts new file mode 100644 index 0000000..4eabbe8 --- /dev/null +++ b/app/javascript/store/hooks.ts @@ -0,0 +1,5 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' +import type { RootState, AppDispatch } from './store' + +export const useAppDispatch: () => AppDispatch = useDispatch +export const useAppSelector: TypedUseSelectorHook = useSelector diff --git a/app/javascript/store/recipesSlice.ts b/app/javascript/store/recipesSlice.ts new file mode 100644 index 0000000..6e91301 --- /dev/null +++ b/app/javascript/store/recipesSlice.ts @@ -0,0 +1,55 @@ +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' + +export const fetchRecipes = createAsyncThunk( + 'recipes/fetchRecipes', + async () => { + const response = await fetch('/api/recipes') + return response.json() + } +) + +interface Ingredient { + id: number; + food_name: string; + measurement: string; +} + +interface Recipe { + id: number; + name: string; + ingredients: Ingredient[]; +} + +interface RecipesState { + items: Recipe[] + status: 'idle' | 'loading' | 'succeeded' | 'failed' + error: string | null +} + +const initialState: RecipesState = { + items: [], + status: 'idle', + error: null +} + +const recipesSlice = createSlice({ + name: 'recipes', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(fetchRecipes.pending, (state) => { + state.status = 'loading' + }) + .addCase(fetchRecipes.fulfilled, (state, action) => { + state.status = 'succeeded' + state.items = action.payload + }) + .addCase(fetchRecipes.rejected, (state, action) => { + state.status = 'failed' + state.error = action.error.message || null + }) + }, +}) + +export default recipesSlice.reducer diff --git a/app/javascript/store/store.ts b/app/javascript/store/store.ts new file mode 100644 index 0000000..e54d7de --- /dev/null +++ b/app/javascript/store/store.ts @@ -0,0 +1,11 @@ +import { configureStore } from '@reduxjs/toolkit' +import recipesReducer from './recipesSlice' + +export const store = configureStore({ + reducer: { + recipes: recipesReducer, + }, +}) + +export type RootState = ReturnType +export type AppDispatch = typeof store.dispatch diff --git a/app/models/recipe.rb b/app/models/recipe.rb index d854fbe..ad85006 100644 --- a/app/models/recipe.rb +++ b/app/models/recipe.rb @@ -1,3 +1,5 @@ class Recipe < ApplicationRecord has_many :ingredients + + accepts_nested_attributes_for :ingredients end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 43da5ef..6b85037 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -21,7 +21,7 @@ <%= javascript_include_tag "application", type: "module" %> - + <%= yield %>