* configure context

* refactor

* get meal with context

* random button with context

* async actions

* refactor meal client
This commit is contained in:
Ruidy 2021-04-05 11:58:43 +02:00 committed by GitHub
parent 7cde13f071
commit e8ac939fc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 175 additions and 183 deletions

View file

@ -1,24 +1,13 @@
import { FC, useState } from "react";
import { FC } from "react";
import { PreLoader } from "./components/PreLoader";
import "./index.css";
import MainLayout from "./layouts/MainLayout";
import { AppRouter } from "./router";
import { Router } from "./router/Router";
import { getData } from "./services/api";
import { MealSummary } from "./types/meal";
import { useAuth0 } from "./utils/auth0-spa";
export const App: FC = () => {
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 ? (
<div className="container center-align valign-wrapper">
@ -26,13 +15,8 @@ export const App: FC = () => {
</div>
) : (
<Router>
<MainLayout
getRandomMeal={getRandomMeal}
searchString={searchString}
setSearchResults={setSearchResults}
setSearchString={setSearchString}
>
<AppRouter searchString={searchString} searchResults={searchResults} />
<MainLayout>
<AppRouter />
</MainLayout>
</Router>
);

View file

@ -8,12 +8,9 @@ import { Logo } from "./Logo";
import { LogOutButton } from "./LogOutButton";
import { RandomButton } from "./RandomButton";
type Props = {
openNavClick: React.MouseEventHandler;
handleClick: () => void;
};
type Props = { openNavClick: React.MouseEventHandler };
export const Navbar: FC<Props> = ({ openNavClick, handleClick }) => {
export const Navbar: FC<Props> = ({ openNavClick }) => {
const { isAuthenticated } = useAuth0();
return (
@ -31,7 +28,6 @@ export const Navbar: FC<Props> = ({ openNavClick, handleClick }) => {
)}
<li>
<RandomButton
handleClick={handleClick}
url={buttonURL}
size="small"
color="orange darken-2"

View file

@ -1,23 +1,20 @@
import { FC } from "react";
import { Link } from "react-router-dom";
import { useMeal } from "../store/meal";
import { fetchRandomMeal } from "../store/meal/async";
type Props = {
url: string;
size?: string;
handleClick: () => void;
color?: string;
};
export const RandomButton: FC<Props> = ({
url,
size = "large",
handleClick,
color,
}) => {
export const RandomButton: FC<Props> = ({ url, size = "large", color }) => {
const classString = `waves-effect waves-light btn-${size} ${color}`;
const { dispatch } = useMeal();
return (
<Link to={url}>
<button className={classString} onClick={handleClick}>
<button className={classString} onClick={() => fetchRandomMeal(dispatch)}>
Random Recipe
</button>
</Link>

View file

@ -1,30 +1,20 @@
import React, { ChangeEvent, FC } from "react";
import React, { ChangeEvent, FC, useState } from "react";
import { Link } from "react-router-dom";
import { getData } from "../services/api";
import { MealSummary } from "../types/meal";
import { useMeal } from "../store/meal";
import { fetchSearchResults } from "../store/meal/async";
type Props = {
searchString: string;
setSearchString: React.Dispatch<React.SetStateAction<string>>;
setSearchResults: React.Dispatch<
React.SetStateAction<{ meals: MealSummary[] }>
>;
};
export const SearchBar: FC<Props> = ({
searchString,
setSearchString,
setSearchResults,
}) => {
export const SearchBar: FC = () => {
const { dispatch } = useMeal();
const [searchString, setSearchString] = useState("");
const getSearchResults: React.MouseEventHandler<HTMLButtonElement> = (e) => {
searchString === ""
? e.preventDefault()
: getData(searchString, setSearchResults, "search");
: fetchSearchResults(dispatch, searchString);
};
const clearSearchBar = () => {
setSearchString("");
setSearchResults({ meals: [] });
dispatch({ type: "clearSearchResults" });
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {

View file

@ -9,13 +9,9 @@ import { LogInButton } from "./LogInButton";
import { LogOutButton } from "./LogOutButton";
import { RandomButton } from "./RandomButton";
type Props = {
showNav: boolean;
closeNavClick: React.MouseEventHandler;
handleClick: () => void;
};
type Props = { showNav: boolean; closeNavClick: React.MouseEventHandler };
export const SideNav: FC<Props> = ({ showNav, closeNavClick, handleClick }) => {
export const SideNav: FC<Props> = ({ showNav, closeNavClick }) => {
const { isAuthenticated, user } = useAuth0();
let transformStyle = {
transform: showNav ? "translateX(0%)" : "translateX(-105%)",
@ -65,12 +61,7 @@ export const SideNav: FC<Props> = ({ showNav, closeNavClick, handleClick }) => {
</li>
<li>
<RandomButton
handleClick={handleClick}
url={buttonURL}
size="small"
color="orange darken-2"
/>
<RandomButton url={buttonURL} size="small" color="orange darken-2" />
</li>
<li>
<Link to="#">

View file

@ -1,2 +1,3 @@
export const apiRoot = "https://www.themealdb.com/api/json/v1/1/";
export const buttonURL = "/random";
export const links = ["categories", "contact"];

View file

@ -7,7 +7,7 @@ export const Categories = () => {
const [categories, setCategories] = useState({ categories: [] });
const getCategories = () => {
getData("categories", setCategories);
getData("categories").then((data) => setCategories(data));
};
useEffect(() => {

View file

@ -8,8 +8,8 @@ export const Category: FC = () => {
const [meals, setMeals] = useState({ meals: [] });
useEffect(() => {
const getMeals = () => getData(strCategory, setMeals, "filter");
getMeals();
const getMeals = () => getData(strCategory, "filter");
getMeals().then((data) => setMeals(data));
}, [strCategory]);
return !meals.meals ? (

View file

@ -8,12 +8,7 @@ export const Home: FC = () => (
<div className="row">
<div className="col s12 m6">
<h1 className="logo">Chef's Online Cookbook</h1>
<RandomButton
url={buttonURL}
size="large"
color="orange darken-2"
handleClick={() => {}}
/>
<RandomButton url={buttonURL} size="large" color="orange darken-2" />
</div>
<picture className="col s12 m6">
<img src={HeroImage} alt="hero_image" width="100%" />

View file

@ -1,8 +1,8 @@
import React, { FC, useEffect, useState } from "react";
import { FC, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { getData } from "../../services/api";
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 { NotFound } from "../NotFound";
import { MealPage } from "./components/MealPage";
@ -12,31 +12,18 @@ export const Meal: FC = () => {
// hooks
const { user, isAuthenticated } = useAuth0();
const { id } = useParams<{ id: string }>();
const { state, dispatch } = useMeal();
const fb = useFirebase();
// local state
const [meal, setMeal] = useState({ meals: [] as MealType[] });
const [isFav, setIsFav] = useState<boolean>();
// variables
const mealItem = meal?.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);
};
const mealItem = state.meals?.[0];
// effects
/** Fetch meal from db */
useEffect(() => {
!id ? getRandomMeal(setMeal) : getMeal(id, setMeal);
}, [id]);
!id ? fetchRandomMeal(dispatch) : fetchMeal(dispatch, id);
}, [id, dispatch]);
/** Updates fav status in db */
useEffect(() => {
if (isAuthenticated) {
@ -63,7 +50,7 @@ export const Meal: FC = () => {
const item = buildMealProps(mealItem, isFav!);
const ingredients = buildIngredientList(mealItem);
return !!meal?.meals ? (
return !!state.meals ? (
<MealPage
meal={item}
ingredients={ingredients}

View file

@ -15,7 +15,7 @@ export const NotFound: FC = () => (
/>
</div>
<div className="card-content">
<RandomButton url="/random" handleClick={() => {}} />
<RandomButton url="/random" />
</div>
</div>
</div>

View file

@ -6,29 +6,26 @@ import { SearchResult } from "./SearchResult";
type Props = {
searchString: string;
searchResults: { meals: MealSummary[] };
searchResults: MealSummary[];
};
export const SearchPage: FC<Props> = ({ searchString, searchResults }) => {
const { meals } = searchResults;
return (
<PageLayout title={`Results for: ${searchString}`}>
{!meals ? (
<div className="center-align">
<p>
No results to display, instead there is a picture of my breakfast.
</p>
<img src={BreakfastImage} alt="Nothing here!" width="70%" />
</div>
) : (
<div className="row">
<ul>
{meals.map((meal, i) => (
<SearchResult key={i} meal={meal} />
))}
</ul>
</div>
)}
</PageLayout>
);
};
export const SearchPage: FC<Props> = ({ searchString, searchResults }) => (
<PageLayout title={`Results for: ${searchString}`}>
{!searchResults ? (
<div className="center-align">
<p>
No results to display, instead there is a picture of my breakfast.
</p>
<img src={BreakfastImage} alt="Nothing here!" width="70%" />
</div>
) : (
<div className="row">
<ul>
{searchResults.map((meal, i) => (
<SearchResult key={i} meal={meal} />
))}
</ul>
</div>
)}
</PageLayout>
);

View file

@ -1,12 +1,13 @@
import { FC } from "react";
import { MealSummary } from "../../types/meal";
import { useMeal } from "../../store/meal";
import { SearchPage } from "./components/SearchPage";
type Props = {
searchString: string;
searchResults: { meals: MealSummary[] };
export const Search: FC = () => {
const { state } = useMeal();
return (
<SearchPage
searchString={state.searchString}
searchResults={state.search}
/>
);
};
export const Search: FC<Props> = ({ searchString, searchResults }) => (
<SearchPage searchString={searchString} searchResults={searchResults} />
);

View file

@ -6,6 +6,7 @@ import * as serviceWorker from "./serviceWorker";
import { Auth0Provider } from "./utils/auth0-spa";
import history from "./utils/history";
import { FirebaseContext } from "./services/Firebase";
import { AppProvider } from "./store/meal";
const onRedirectCallBack = (appState) => {
history.push(
@ -24,7 +25,9 @@ ReactDOM.render(
>
{/*<FirebaseContext.Provider value={new Firebase()}> todo fix Firebase app*/}
<FirebaseContext.Provider>
<App />
<AppProvider>
<App />
</AppProvider>
</FirebaseContext.Provider>
</Auth0Provider>,
document.getElementById("root")

View file

@ -3,23 +3,8 @@ import { Footer } from "../components/Footer";
import { Navbar } from "../components/Navbar";
import { SearchBar } from "../components/SearchBar";
import { SideNav } from "../components/SideNav";
import { MealSummary } from "../types/meal";
type Props = {
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 MainLayout: FC = ({ children }) => {
const [showNav, setShowNav] = useState(false);
const openNavClick: React.MouseEventHandler = (e) => {
@ -43,18 +28,9 @@ const MainLayout: FC<Props> = ({
return (
<>
<header>
<Navbar handleClick={getRandomMeal} openNavClick={openNavClick} />
<SearchBar
searchString={searchString}
setSearchString={setSearchString}
setSearchResults={setSearchResults}
/>
<SideNav
showNav={showNav}
closeNavClick={closeNavClick}
handleClick={() => {}}
/>
<Navbar openNavClick={openNavClick} />
<SearchBar />
<SideNav showNav={showNav} closeNavClick={closeNavClick} />
</header>
<main>{children}</main>
<Footer />

View file

@ -1,8 +1,6 @@
import { FC } from "react";
type Props = {
title: string;
};
type Props = { title: string };
const PageLayout: FC<Props> = ({ title, children }) => (
<div className="container">

View file

@ -3,20 +3,15 @@ import { Redirect, Route, Switch } from "react-router-dom";
import { buttonURL } from "../constants";
import { Categories } from "../containers/Categories";
import { Category } from "../containers/Category";
import { Contact } from "../containers/Contact";
import { Home } from "../containers/Home";
import { Meal } from "../containers/Meal";
import { NotFound } from "../containers/NotFound";
import { Profile } from "../containers/Profile";
import { Search } from "../containers/Search";
import { Contact } from "../containers/Contact";
import { NotFound } from "../containers/NotFound";
import { MealSummary } from "../types/meal";
import { PrivateRoute } from "./PrivateRoute";
type Props = {
searchString: string;
searchResults: { meals: MealSummary[] };
};
const AppRouter: FC<Props> = ({ searchString, searchResults }) => (
const AppRouter: FC = () => (
<Switch>
<Route exact path="/">
<Home />
@ -37,7 +32,7 @@ const AppRouter: FC<Props> = ({ searchString, searchResults }) => (
</Route>
<Route exact path="/search">
<Search searchString={searchString} searchResults={searchResults} />
<Search />
</Route>
<Route path="/contact">

View file

@ -1,20 +1,21 @@
import React from "react";
import { apiRoot } from "../constants";
export const createURI = (keyword: string, option?: string) => {
const ROOT = "https://www.themealdb.com/api/json/v1/1/";
type Option = "filter" | "lookup" | "search";
const createURI = (keyword: string, option?: Option) => {
if (!option) {
return `${ROOT}${keyword}.php`;
return `${apiRoot}${keyword}.php`;
}
switch (option) {
case "filter": {
return `${ROOT}${option}.php?c=${keyword}`;
return `${apiRoot}${option}.php?c=${keyword}`;
}
case "lookup": {
return `${ROOT}${option}.php?i=${keyword}`;
return `${apiRoot}${option}.php?i=${keyword}`;
}
case "search": {
return `${ROOT}${option}.php?s=${keyword}`;
return `${apiRoot}${option}.php?s=${keyword}`;
}
default: {
throw Error("Unexpected URI");
@ -22,15 +23,10 @@ export const createURI = (keyword: string, option?: string) => {
}
};
export const getData = (
keyword: string,
set: React.Dispatch<React.SetStateAction<any>>,
option?: string
) => {
export const getData = (keyword: string, option?: Option) => {
const URI = createURI(keyword, option);
fetch(URI)
return fetch(URI)
.then((response) => response.json())
.catch((error) => console.warn(error + "url:" + URI))
.then((data) => set(data));
.catch((error) => console.warn(error + "url:" + URI));
};

23
src/store/meal/async.ts Normal file
View 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
View 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
View 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;