mirror of
https://github.com/rjNemo/meal_planner
synced 2026-06-12 13:26:45 +00:00
state (#9)
* configure context * refactor * get meal with context * random button with context * async actions * refactor meal client
This commit is contained in:
parent
7cde13f071
commit
e8ac939fc9
21 changed files with 175 additions and 183 deletions
22
src/App.tsx
22
src/App.tsx
|
|
@ -1,24 +1,13 @@
|
||||||
import { FC, useState } from "react";
|
import { FC } from "react";
|
||||||
import { PreLoader } from "./components/PreLoader";
|
import { PreLoader } from "./components/PreLoader";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import MainLayout from "./layouts/MainLayout";
|
import MainLayout from "./layouts/MainLayout";
|
||||||
import { AppRouter } from "./router";
|
import { AppRouter } from "./router";
|
||||||
import { Router } from "./router/Router";
|
import { Router } from "./router/Router";
|
||||||
import { getData } from "./services/api";
|
|
||||||
import { MealSummary } from "./types/meal";
|
|
||||||
import { useAuth0 } from "./utils/auth0-spa";
|
import { useAuth0 } from "./utils/auth0-spa";
|
||||||
|
|
||||||
export const App: FC = () => {
|
export const App: FC = () => {
|
||||||
const { loading } = useAuth0();
|
const { loading } = useAuth0();
|
||||||
const [searchString, setSearchString] = useState("");
|
|
||||||
const [searchResults, setSearchResults] = useState({
|
|
||||||
meals: [] as MealSummary[],
|
|
||||||
});
|
|
||||||
const [_, setMeal] = useState(null);
|
|
||||||
|
|
||||||
const getRandomMeal = () => {
|
|
||||||
getData("random", setMeal);
|
|
||||||
};
|
|
||||||
|
|
||||||
return loading ? (
|
return loading ? (
|
||||||
<div className="container center-align valign-wrapper">
|
<div className="container center-align valign-wrapper">
|
||||||
|
|
@ -26,13 +15,8 @@ export const App: FC = () => {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Router>
|
<Router>
|
||||||
<MainLayout
|
<MainLayout>
|
||||||
getRandomMeal={getRandomMeal}
|
<AppRouter />
|
||||||
searchString={searchString}
|
|
||||||
setSearchResults={setSearchResults}
|
|
||||||
setSearchString={setSearchString}
|
|
||||||
>
|
|
||||||
<AppRouter searchString={searchString} searchResults={searchResults} />
|
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,9 @@ import { Logo } from "./Logo";
|
||||||
import { LogOutButton } from "./LogOutButton";
|
import { LogOutButton } from "./LogOutButton";
|
||||||
import { RandomButton } from "./RandomButton";
|
import { RandomButton } from "./RandomButton";
|
||||||
|
|
||||||
type Props = {
|
type Props = { openNavClick: React.MouseEventHandler };
|
||||||
openNavClick: React.MouseEventHandler;
|
|
||||||
handleClick: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Navbar: FC<Props> = ({ openNavClick, handleClick }) => {
|
export const Navbar: FC<Props> = ({ openNavClick }) => {
|
||||||
const { isAuthenticated } = useAuth0();
|
const { isAuthenticated } = useAuth0();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -31,7 +28,6 @@ export const Navbar: FC<Props> = ({ openNavClick, handleClick }) => {
|
||||||
)}
|
)}
|
||||||
<li>
|
<li>
|
||||||
<RandomButton
|
<RandomButton
|
||||||
handleClick={handleClick}
|
|
||||||
url={buttonURL}
|
url={buttonURL}
|
||||||
size="small"
|
size="small"
|
||||||
color="orange darken-2"
|
color="orange darken-2"
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,20 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { useMeal } from "../store/meal";
|
||||||
|
import { fetchRandomMeal } from "../store/meal/async";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string;
|
url: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
handleClick: () => void;
|
|
||||||
color?: string;
|
color?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RandomButton: FC<Props> = ({
|
export const RandomButton: FC<Props> = ({ url, size = "large", color }) => {
|
||||||
url,
|
|
||||||
size = "large",
|
|
||||||
handleClick,
|
|
||||||
color,
|
|
||||||
}) => {
|
|
||||||
const classString = `waves-effect waves-light btn-${size} ${color}`;
|
const classString = `waves-effect waves-light btn-${size} ${color}`;
|
||||||
|
const { dispatch } = useMeal();
|
||||||
return (
|
return (
|
||||||
<Link to={url}>
|
<Link to={url}>
|
||||||
<button className={classString} onClick={handleClick}>
|
<button className={classString} onClick={() => fetchRandomMeal(dispatch)}>
|
||||||
Random Recipe
|
Random Recipe
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,20 @@
|
||||||
import React, { ChangeEvent, FC } from "react";
|
import React, { ChangeEvent, FC, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { getData } from "../services/api";
|
import { useMeal } from "../store/meal";
|
||||||
import { MealSummary } from "../types/meal";
|
import { fetchSearchResults } from "../store/meal/async";
|
||||||
|
|
||||||
type Props = {
|
export const SearchBar: FC = () => {
|
||||||
searchString: string;
|
const { dispatch } = useMeal();
|
||||||
setSearchString: React.Dispatch<React.SetStateAction<string>>;
|
const [searchString, setSearchString] = useState("");
|
||||||
setSearchResults: React.Dispatch<
|
|
||||||
React.SetStateAction<{ meals: MealSummary[] }>
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SearchBar: FC<Props> = ({
|
|
||||||
searchString,
|
|
||||||
setSearchString,
|
|
||||||
setSearchResults,
|
|
||||||
}) => {
|
|
||||||
const getSearchResults: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
const getSearchResults: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||||
searchString === ""
|
searchString === ""
|
||||||
? e.preventDefault()
|
? e.preventDefault()
|
||||||
: getData(searchString, setSearchResults, "search");
|
: fetchSearchResults(dispatch, searchString);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearSearchBar = () => {
|
const clearSearchBar = () => {
|
||||||
setSearchString("");
|
setSearchString("");
|
||||||
setSearchResults({ meals: [] });
|
dispatch({ type: "clearSearchResults" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,9 @@ import { LogInButton } from "./LogInButton";
|
||||||
import { LogOutButton } from "./LogOutButton";
|
import { LogOutButton } from "./LogOutButton";
|
||||||
import { RandomButton } from "./RandomButton";
|
import { RandomButton } from "./RandomButton";
|
||||||
|
|
||||||
type Props = {
|
type Props = { showNav: boolean; closeNavClick: React.MouseEventHandler };
|
||||||
showNav: boolean;
|
|
||||||
closeNavClick: React.MouseEventHandler;
|
|
||||||
handleClick: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SideNav: FC<Props> = ({ showNav, closeNavClick, handleClick }) => {
|
export const SideNav: FC<Props> = ({ showNav, closeNavClick }) => {
|
||||||
const { isAuthenticated, user } = useAuth0();
|
const { isAuthenticated, user } = useAuth0();
|
||||||
let transformStyle = {
|
let transformStyle = {
|
||||||
transform: showNav ? "translateX(0%)" : "translateX(-105%)",
|
transform: showNav ? "translateX(0%)" : "translateX(-105%)",
|
||||||
|
|
@ -65,12 +61,7 @@ export const SideNav: FC<Props> = ({ showNav, closeNavClick, handleClick }) => {
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<RandomButton
|
<RandomButton url={buttonURL} size="small" color="orange darken-2" />
|
||||||
handleClick={handleClick}
|
|
||||||
url={buttonURL}
|
|
||||||
size="small"
|
|
||||||
color="orange darken-2"
|
|
||||||
/>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="#">
|
<Link to="#">
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
|
export const apiRoot = "https://www.themealdb.com/api/json/v1/1/";
|
||||||
export const buttonURL = "/random";
|
export const buttonURL = "/random";
|
||||||
export const links = ["categories", "contact"];
|
export const links = ["categories", "contact"];
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export const Categories = () => {
|
||||||
const [categories, setCategories] = useState({ categories: [] });
|
const [categories, setCategories] = useState({ categories: [] });
|
||||||
|
|
||||||
const getCategories = () => {
|
const getCategories = () => {
|
||||||
getData("categories", setCategories);
|
getData("categories").then((data) => setCategories(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ export const Category: FC = () => {
|
||||||
const [meals, setMeals] = useState({ meals: [] });
|
const [meals, setMeals] = useState({ meals: [] });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getMeals = () => getData(strCategory, setMeals, "filter");
|
const getMeals = () => getData(strCategory, "filter");
|
||||||
getMeals();
|
getMeals().then((data) => setMeals(data));
|
||||||
}, [strCategory]);
|
}, [strCategory]);
|
||||||
|
|
||||||
return !meals.meals ? (
|
return !meals.meals ? (
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,7 @@ export const Home: FC = () => (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col s12 m6">
|
<div className="col s12 m6">
|
||||||
<h1 className="logo">Chef's Online Cookbook</h1>
|
<h1 className="logo">Chef's Online Cookbook</h1>
|
||||||
<RandomButton
|
<RandomButton url={buttonURL} size="large" color="orange darken-2" />
|
||||||
url={buttonURL}
|
|
||||||
size="large"
|
|
||||||
color="orange darken-2"
|
|
||||||
handleClick={() => {}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<picture className="col s12 m6">
|
<picture className="col s12 m6">
|
||||||
<img src={HeroImage} alt="hero_image" width="100%" />
|
<img src={HeroImage} alt="hero_image" width="100%" />
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { getData } from "../../services/api";
|
|
||||||
import { useFirebase } from "../../services/Firebase";
|
import { useFirebase } from "../../services/Firebase";
|
||||||
import { MealApi as MealType } from "../../types/meal";
|
import { useMeal } from "../../store/meal";
|
||||||
|
import { fetchMeal, fetchRandomMeal } from "../../store/meal/async";
|
||||||
import { useAuth0 } from "../../utils/auth0-spa";
|
import { useAuth0 } from "../../utils/auth0-spa";
|
||||||
import { NotFound } from "../NotFound";
|
import { NotFound } from "../NotFound";
|
||||||
import { MealPage } from "./components/MealPage";
|
import { MealPage } from "./components/MealPage";
|
||||||
|
|
@ -12,31 +12,18 @@ export const Meal: FC = () => {
|
||||||
// hooks
|
// hooks
|
||||||
const { user, isAuthenticated } = useAuth0();
|
const { user, isAuthenticated } = useAuth0();
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const { state, dispatch } = useMeal();
|
||||||
const fb = useFirebase();
|
const fb = useFirebase();
|
||||||
// local state
|
// local state
|
||||||
const [meal, setMeal] = useState({ meals: [] as MealType[] });
|
|
||||||
const [isFav, setIsFav] = useState<boolean>();
|
const [isFav, setIsFav] = useState<boolean>();
|
||||||
// variables
|
// variables
|
||||||
const mealItem = meal?.meals?.[0];
|
const mealItem = state.meals?.[0];
|
||||||
|
|
||||||
const getMeal = (
|
|
||||||
id: string,
|
|
||||||
setMeal: React.Dispatch<React.SetStateAction<{ meals: MealType[] }>>
|
|
||||||
) => {
|
|
||||||
getData(id, setMeal, "lookup");
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRandomMeal = (
|
|
||||||
setMeal: React.Dispatch<React.SetStateAction<{ meals: MealType[] }>>
|
|
||||||
) => {
|
|
||||||
getData("random", setMeal);
|
|
||||||
};
|
|
||||||
|
|
||||||
// effects
|
// effects
|
||||||
/** Fetch meal from db */
|
/** Fetch meal from db */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!id ? getRandomMeal(setMeal) : getMeal(id, setMeal);
|
!id ? fetchRandomMeal(dispatch) : fetchMeal(dispatch, id);
|
||||||
}, [id]);
|
}, [id, dispatch]);
|
||||||
|
|
||||||
/** Updates fav status in db */
|
/** Updates fav status in db */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
|
|
@ -63,7 +50,7 @@ export const Meal: FC = () => {
|
||||||
const item = buildMealProps(mealItem, isFav!);
|
const item = buildMealProps(mealItem, isFav!);
|
||||||
const ingredients = buildIngredientList(mealItem);
|
const ingredients = buildIngredientList(mealItem);
|
||||||
|
|
||||||
return !!meal?.meals ? (
|
return !!state.meals ? (
|
||||||
<MealPage
|
<MealPage
|
||||||
meal={item}
|
meal={item}
|
||||||
ingredients={ingredients}
|
ingredients={ingredients}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export const NotFound: FC = () => (
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-content">
|
<div className="card-content">
|
||||||
<RandomButton url="/random" handleClick={() => {}} />
|
<RandomButton url="/random" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,29 +6,26 @@ import { SearchResult } from "./SearchResult";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
searchString: string;
|
searchString: string;
|
||||||
searchResults: { meals: MealSummary[] };
|
searchResults: MealSummary[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SearchPage: FC<Props> = ({ searchString, searchResults }) => {
|
export const SearchPage: FC<Props> = ({ searchString, searchResults }) => (
|
||||||
const { meals } = searchResults;
|
<PageLayout title={`Results for: ${searchString}`}>
|
||||||
return (
|
{!searchResults ? (
|
||||||
<PageLayout title={`Results for: ${searchString}`}>
|
<div className="center-align">
|
||||||
{!meals ? (
|
<p>
|
||||||
<div className="center-align">
|
No results to display, instead there is a picture of my breakfast.
|
||||||
<p>
|
</p>
|
||||||
No results to display, instead there is a picture of my breakfast.
|
<img src={BreakfastImage} alt="Nothing here!" width="70%" />
|
||||||
</p>
|
</div>
|
||||||
<img src={BreakfastImage} alt="Nothing here!" width="70%" />
|
) : (
|
||||||
</div>
|
<div className="row">
|
||||||
) : (
|
<ul>
|
||||||
<div className="row">
|
{searchResults.map((meal, i) => (
|
||||||
<ul>
|
<SearchResult key={i} meal={meal} />
|
||||||
{meals.map((meal, i) => (
|
))}
|
||||||
<SearchResult key={i} meal={meal} />
|
</ul>
|
||||||
))}
|
</div>
|
||||||
</ul>
|
)}
|
||||||
</div>
|
</PageLayout>
|
||||||
)}
|
);
|
||||||
</PageLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { MealSummary } from "../../types/meal";
|
import { useMeal } from "../../store/meal";
|
||||||
import { SearchPage } from "./components/SearchPage";
|
import { SearchPage } from "./components/SearchPage";
|
||||||
|
|
||||||
type Props = {
|
export const Search: FC = () => {
|
||||||
searchString: string;
|
const { state } = useMeal();
|
||||||
searchResults: { meals: MealSummary[] };
|
return (
|
||||||
|
<SearchPage
|
||||||
|
searchString={state.searchString}
|
||||||
|
searchResults={state.search}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Search: FC<Props> = ({ searchString, searchResults }) => (
|
|
||||||
<SearchPage searchString={searchString} searchResults={searchResults} />
|
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import * as serviceWorker from "./serviceWorker";
|
||||||
import { Auth0Provider } from "./utils/auth0-spa";
|
import { Auth0Provider } from "./utils/auth0-spa";
|
||||||
import history from "./utils/history";
|
import history from "./utils/history";
|
||||||
import { FirebaseContext } from "./services/Firebase";
|
import { FirebaseContext } from "./services/Firebase";
|
||||||
|
import { AppProvider } from "./store/meal";
|
||||||
|
|
||||||
const onRedirectCallBack = (appState) => {
|
const onRedirectCallBack = (appState) => {
|
||||||
history.push(
|
history.push(
|
||||||
|
|
@ -24,7 +25,9 @@ ReactDOM.render(
|
||||||
>
|
>
|
||||||
{/*<FirebaseContext.Provider value={new Firebase()}> todo fix Firebase app*/}
|
{/*<FirebaseContext.Provider value={new Firebase()}> todo fix Firebase app*/}
|
||||||
<FirebaseContext.Provider>
|
<FirebaseContext.Provider>
|
||||||
<App />
|
<AppProvider>
|
||||||
|
<App />
|
||||||
|
</AppProvider>
|
||||||
</FirebaseContext.Provider>
|
</FirebaseContext.Provider>
|
||||||
</Auth0Provider>,
|
</Auth0Provider>,
|
||||||
document.getElementById("root")
|
document.getElementById("root")
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,8 @@ import { Footer } from "../components/Footer";
|
||||||
import { Navbar } from "../components/Navbar";
|
import { Navbar } from "../components/Navbar";
|
||||||
import { SearchBar } from "../components/SearchBar";
|
import { SearchBar } from "../components/SearchBar";
|
||||||
import { SideNav } from "../components/SideNav";
|
import { SideNav } from "../components/SideNav";
|
||||||
import { MealSummary } from "../types/meal";
|
|
||||||
|
|
||||||
type Props = {
|
const MainLayout: FC = ({ children }) => {
|
||||||
getRandomMeal: () => void;
|
|
||||||
searchString: string;
|
|
||||||
setSearchString: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
setSearchResults: React.Dispatch<
|
|
||||||
React.SetStateAction<{ meals: MealSummary[] }>
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
const MainLayout: FC<Props> = ({
|
|
||||||
getRandomMeal,
|
|
||||||
searchString,
|
|
||||||
setSearchString,
|
|
||||||
setSearchResults,
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const [showNav, setShowNav] = useState(false);
|
const [showNav, setShowNav] = useState(false);
|
||||||
|
|
||||||
const openNavClick: React.MouseEventHandler = (e) => {
|
const openNavClick: React.MouseEventHandler = (e) => {
|
||||||
|
|
@ -43,18 +28,9 @@ const MainLayout: FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header>
|
<header>
|
||||||
<Navbar handleClick={getRandomMeal} openNavClick={openNavClick} />
|
<Navbar openNavClick={openNavClick} />
|
||||||
|
<SearchBar />
|
||||||
<SearchBar
|
<SideNav showNav={showNav} closeNavClick={closeNavClick} />
|
||||||
searchString={searchString}
|
|
||||||
setSearchString={setSearchString}
|
|
||||||
setSearchResults={setSearchResults}
|
|
||||||
/>
|
|
||||||
<SideNav
|
|
||||||
showNav={showNav}
|
|
||||||
closeNavClick={closeNavClick}
|
|
||||||
handleClick={() => {}}
|
|
||||||
/>
|
|
||||||
</header>
|
</header>
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = { title: string };
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PageLayout: FC<Props> = ({ title, children }) => (
|
const PageLayout: FC<Props> = ({ title, children }) => (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,15 @@ import { Redirect, Route, Switch } from "react-router-dom";
|
||||||
import { buttonURL } from "../constants";
|
import { buttonURL } from "../constants";
|
||||||
import { Categories } from "../containers/Categories";
|
import { Categories } from "../containers/Categories";
|
||||||
import { Category } from "../containers/Category";
|
import { Category } from "../containers/Category";
|
||||||
|
import { Contact } from "../containers/Contact";
|
||||||
import { Home } from "../containers/Home";
|
import { Home } from "../containers/Home";
|
||||||
import { Meal } from "../containers/Meal";
|
import { Meal } from "../containers/Meal";
|
||||||
|
import { NotFound } from "../containers/NotFound";
|
||||||
import { Profile } from "../containers/Profile";
|
import { Profile } from "../containers/Profile";
|
||||||
import { Search } from "../containers/Search";
|
import { Search } from "../containers/Search";
|
||||||
import { Contact } from "../containers/Contact";
|
|
||||||
import { NotFound } from "../containers/NotFound";
|
|
||||||
import { MealSummary } from "../types/meal";
|
|
||||||
import { PrivateRoute } from "./PrivateRoute";
|
import { PrivateRoute } from "./PrivateRoute";
|
||||||
|
|
||||||
type Props = {
|
const AppRouter: FC = () => (
|
||||||
searchString: string;
|
|
||||||
searchResults: { meals: MealSummary[] };
|
|
||||||
};
|
|
||||||
const AppRouter: FC<Props> = ({ searchString, searchResults }) => (
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/">
|
<Route exact path="/">
|
||||||
<Home />
|
<Home />
|
||||||
|
|
@ -37,7 +32,7 @@ const AppRouter: FC<Props> = ({ searchString, searchResults }) => (
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route exact path="/search">
|
<Route exact path="/search">
|
||||||
<Search searchString={searchString} searchResults={searchResults} />
|
<Search />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/contact">
|
<Route path="/contact">
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
import React from "react";
|
import { apiRoot } from "../constants";
|
||||||
|
|
||||||
export const createURI = (keyword: string, option?: string) => {
|
type Option = "filter" | "lookup" | "search";
|
||||||
const ROOT = "https://www.themealdb.com/api/json/v1/1/";
|
|
||||||
|
const createURI = (keyword: string, option?: Option) => {
|
||||||
if (!option) {
|
if (!option) {
|
||||||
return `${ROOT}${keyword}.php`;
|
return `${apiRoot}${keyword}.php`;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (option) {
|
switch (option) {
|
||||||
case "filter": {
|
case "filter": {
|
||||||
return `${ROOT}${option}.php?c=${keyword}`;
|
return `${apiRoot}${option}.php?c=${keyword}`;
|
||||||
}
|
}
|
||||||
case "lookup": {
|
case "lookup": {
|
||||||
return `${ROOT}${option}.php?i=${keyword}`;
|
return `${apiRoot}${option}.php?i=${keyword}`;
|
||||||
}
|
}
|
||||||
case "search": {
|
case "search": {
|
||||||
return `${ROOT}${option}.php?s=${keyword}`;
|
return `${apiRoot}${option}.php?s=${keyword}`;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
throw Error("Unexpected URI");
|
throw Error("Unexpected URI");
|
||||||
|
|
@ -22,15 +23,10 @@ export const createURI = (keyword: string, option?: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getData = (
|
export const getData = (keyword: string, option?: Option) => {
|
||||||
keyword: string,
|
|
||||||
set: React.Dispatch<React.SetStateAction<any>>,
|
|
||||||
option?: string
|
|
||||||
) => {
|
|
||||||
const URI = createURI(keyword, option);
|
const URI = createURI(keyword, option);
|
||||||
|
|
||||||
fetch(URI)
|
return fetch(URI)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.catch((error) => console.warn(error + "url:" + URI))
|
.catch((error) => console.warn(error + "url:" + URI));
|
||||||
.then((data) => set(data));
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
23
src/store/meal/async.ts
Normal file
23
src/store/meal/async.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { getData } from "../../services/api";
|
||||||
|
import { Dispatch } from "./reducer";
|
||||||
|
|
||||||
|
export const fetchRandomMeal = async (dispatch: Dispatch) => {
|
||||||
|
const meal = await getData("random");
|
||||||
|
dispatch({ type: "setMeal", payload: meal?.meals });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchMeal = async (dispatch: Dispatch, id: string) => {
|
||||||
|
const meal = await getData(id, "lookup");
|
||||||
|
dispatch({ type: "setMeal", payload: meal?.meals });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchSearchResults = async (
|
||||||
|
dispatch: Dispatch,
|
||||||
|
searchString: string
|
||||||
|
) => {
|
||||||
|
const meals = await getData(searchString, "search");
|
||||||
|
dispatch({
|
||||||
|
type: "setSearchResults",
|
||||||
|
payload: { search: meals?.meals, searchString },
|
||||||
|
});
|
||||||
|
};
|
||||||
35
src/store/meal/index.tsx
Normal file
35
src/store/meal/index.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
//https://kentcdodds.com/blog/how-to-use-react-context-effectively
|
||||||
|
|
||||||
|
import { createContext, FC, useContext, useReducer } from "react";
|
||||||
|
import { MealApi, MealSummary } from "../../types/meal";
|
||||||
|
import { appReducer, Dispatch } from "./reducer";
|
||||||
|
|
||||||
|
export type AppState = {
|
||||||
|
meals: MealApi[];
|
||||||
|
search: MealSummary[];
|
||||||
|
searchString: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initState = {
|
||||||
|
meals: [] as MealApi[],
|
||||||
|
search: [] as MealSummary[],
|
||||||
|
searchString: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContextType = { state: AppState; dispatch: Dispatch } | undefined;
|
||||||
|
|
||||||
|
const AppContext = createContext<ContextType>(undefined);
|
||||||
|
|
||||||
|
export const useMeal = () => {
|
||||||
|
const context = useContext(AppContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useMeal must be used within a AppProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppProvider: FC = ({ children }) => {
|
||||||
|
const [state, dispatch] = useReducer(appReducer, initState);
|
||||||
|
const value = { state, dispatch };
|
||||||
|
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||||
|
};
|
||||||
27
src/store/meal/reducer.ts
Normal file
27
src/store/meal/reducer.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { MealSummary } from "../../types/meal";
|
||||||
|
import { AppState } from "./index";
|
||||||
|
|
||||||
|
export const appReducer = (state: AppState, action: Action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "setMeal":
|
||||||
|
return { ...state, meals: action.payload };
|
||||||
|
case "setSearchResults":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
search: action.payload.search,
|
||||||
|
searchString: action.payload.searchString,
|
||||||
|
};
|
||||||
|
case "clearSearchResults":
|
||||||
|
return { ...state, search: [] as MealSummary[] };
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unhandled action type: ${action.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Action = {
|
||||||
|
payload?: any;
|
||||||
|
type: "setMeal" | "setSearchResults" | "clearSearchResults";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Dispatch = (action: Action) => void;
|
||||||
Loading…
Reference in a new issue