remove old imlpementation
18
README.md
|
|
@ -95,13 +95,16 @@ address: [link](https://mood2food.netlify.app/).
|
||||||
## Built With
|
## Built With
|
||||||
|
|
||||||
- [Nuxt](https://nuxt.com/) - The Intuitive Vue Framework
|
- [Nuxt](https://nuxt.com/) - The Intuitive Vue Framework
|
||||||
- [Tailwindcss](https://tailwindcss.com) -Rapidly build modern websites without ever leaving your HTML.
|
- [Tailwindcss](https://tailwindcss.com) -Rapidly build modern websites without
|
||||||
- [TheMealDb](https://www.themealdb.com/api.php) - An open, crowd-sourced database of Recipes from around the world
|
ever leaving your HTML.
|
||||||
|
- [TheMealDb](https://www.themealdb.com/api.php) - An open, crowd-sourced database
|
||||||
|
of Recipes from around the world
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Please read [CONTRIBUTING.md](https://github.com/rjNemo/meal_planner/contributors) for details on our code of conduct,
|
Please read [CONTRIBUTING.md](https://github.com/rjNemo/meal_planner/contributors)
|
||||||
and the process for submitting pull requests to us.
|
for details on our code of conduct, and the process for submitting pull requests
|
||||||
|
to us.
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
|
|
@ -112,9 +115,10 @@ the [tags on this repository](https://github.com/rjNemo/meal_planner/tags).
|
||||||
|
|
||||||
- **Ruidy Nemausat** - _Initial work_ - [GitHub](https://github.com/rjNemo)
|
- **Ruidy Nemausat** - _Initial work_ - [GitHub](https://github.com/rjNemo)
|
||||||
|
|
||||||
See also the list of [contributors](https://github.com/rjNemo/meal_planner/contributors) who participated in this
|
See also the list of [contributors](https://github.com/rjNemo/meal_planner/contributors)
|
||||||
project.
|
who participated in this project.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
|
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md)
|
||||||
|
file for details
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 259 KiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.1 MiB |
|
|
@ -24,6 +24,7 @@ if (error.value) {
|
||||||
message: error.value.message,
|
message: error.value.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = useRequestURL();
|
const url = useRequestURL();
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: `${recipe.value!.title} | Mood2Food`,
|
title: `${recipe.value!.title} | Mood2Food`,
|
||||||
|
|
|
||||||
11
src/App.tsx
|
|
@ -1,11 +0,0 @@
|
||||||
import "./index.css";
|
|
||||||
import MainLayout from "./layouts/MainLayout";
|
|
||||||
import { AppRouter, Router } from "./router";
|
|
||||||
|
|
||||||
export const App = () => (
|
|
||||||
<Router>
|
|
||||||
<MainLayout>
|
|
||||||
<AppRouter />
|
|
||||||
</MainLayout>
|
|
||||||
</Router>
|
|
||||||
);
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { MealSummary } from "../types/meal";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
meal: MealSummary;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CardEntry = ({
|
|
||||||
meal: { idMeal, strMeal, strMealThumb },
|
|
||||||
className = "col s12 m6",
|
|
||||||
}: Props) => (
|
|
||||||
<Link to={`/${idMeal}`}>
|
|
||||||
<li>
|
|
||||||
<div className={className}>
|
|
||||||
<div className="card hoverable">
|
|
||||||
<div className="card-image">
|
|
||||||
<img src={strMealThumb} alt={strMeal} />
|
|
||||||
</div>
|
|
||||||
<div className="card-content">
|
|
||||||
<h4>{strMeal}</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
export const CopyrightText = () => (
|
|
||||||
<span className="grey-text text-darken-1">
|
|
||||||
© {new Date().getFullYear()} - <span className="logo">Chef's</span> - Made with{" "}
|
|
||||||
<span role="img" aria-label="heart">
|
|
||||||
❤️
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { links } from "../constants";
|
|
||||||
import { CopyrightText } from "./CopyrightText";
|
|
||||||
import { FooterLink } from "./FooterLink";
|
|
||||||
import { GitHubLink } from "./GitHubLink";
|
|
||||||
|
|
||||||
export const Footer = () => {
|
|
||||||
const footerLinks = [...links, "random"];
|
|
||||||
return (
|
|
||||||
<footer className="page-footer">
|
|
||||||
<div className="row">
|
|
||||||
<div className="container">
|
|
||||||
<div className=" s12">
|
|
||||||
<h5 className="black-text">Navigation</h5>
|
|
||||||
<ul>
|
|
||||||
{footerLinks.map((link, i) => (
|
|
||||||
<FooterLink key={i} link={link} textColor="black" />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="footer-copyright">
|
|
||||||
<div className="container">
|
|
||||||
<CopyrightText />
|
|
||||||
<GitHubLink />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { upFirstChar } from "../utils/string";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
link: string;
|
|
||||||
textColor?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FooterLink = ({ link, textColor = "" }: Props) => {
|
|
||||||
const textColorClass = `${textColor}-text`;
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<Link className={`${textColorClass} waves-effect text-lighten-3`} to={`/${link}`}>
|
|
||||||
{upFirstChar(link)}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
export const GitHubLink = () => (
|
|
||||||
<a
|
|
||||||
className="grey-text text-darken-1 right"
|
|
||||||
href="https://github.com/rjNemo/meal_planner"
|
|
||||||
target="blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export const Logo = () => (
|
|
||||||
<Link to="/" className="brand-logo">
|
|
||||||
<img
|
|
||||||
src="/logo192.png"
|
|
||||||
alt="chef's logo"
|
|
||||||
height="30px"
|
|
||||||
style={{ position: "relative", top: "5px" }}
|
|
||||||
/>
|
|
||||||
<span className="logo orange-text text-accent-4">Chef's</span>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import { MouseEventHandler } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { buttonURL, links } from "../constants";
|
|
||||||
import { FooterLink } from "./FooterLink";
|
|
||||||
import { Logo } from "./Logo";
|
|
||||||
import { RandomButton } from "./RandomButton";
|
|
||||||
|
|
||||||
type Props = { openNavClick: MouseEventHandler };
|
|
||||||
|
|
||||||
export const Navbar = ({ openNavClick }: Props) => (
|
|
||||||
<div className="navbar-fixed">
|
|
||||||
<nav>
|
|
||||||
<div className="nav-wrapper">
|
|
||||||
<div className="container ">
|
|
||||||
<Logo />
|
|
||||||
<ul id="nav-mobile" className="right hide-on-med-and-down">
|
|
||||||
{links.map((link, i) => (
|
|
||||||
<FooterLink key={i} link={link} textColor="black" />
|
|
||||||
))}
|
|
||||||
<li>
|
|
||||||
<RandomButton
|
|
||||||
url={buttonURL}
|
|
||||||
size="small"
|
|
||||||
color="orange darken-2"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<Link
|
|
||||||
to="#"
|
|
||||||
data-target="slide-out"
|
|
||||||
className="sidenav-trigger "
|
|
||||||
onClick={openNavClick}
|
|
||||||
>
|
|
||||||
<i className="material-icons">menu</i>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
export const PreLoader = () => (
|
|
||||||
<div className="preloader-wrapper active">
|
|
||||||
<div className="spinner-layer spinner-red-only">
|
|
||||||
<div className="circle-clipper left">
|
|
||||||
<div className="circle" />
|
|
||||||
</div>
|
|
||||||
<div className="gap-patch">
|
|
||||||
<div className="circle" />
|
|
||||||
</div>
|
|
||||||
<div className="circle-clipper right">
|
|
||||||
<div className="circle" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { useMeal } from "../store/meal";
|
|
||||||
import { fetchRandomMeal } from "../store/meal/async";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
url: string;
|
|
||||||
size?: string;
|
|
||||||
color?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RandomButton = ({ url, size = "large", color }: Props) => {
|
|
||||||
const classString = `waves-effect waves-light btn-${size} ${color}`;
|
|
||||||
const { dispatch } = useMeal();
|
|
||||||
return (
|
|
||||||
<Link to={url}>
|
|
||||||
<button className={classString} onClick={() => fetchRandomMeal(dispatch)}>
|
|
||||||
Random Recipe
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import { ChangeEvent, MouseEventHandler, useState } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { useMeal } from "../store/meal";
|
|
||||||
import { fetchSearchResults } from "../store/meal/async";
|
|
||||||
|
|
||||||
export const SearchBar = () => {
|
|
||||||
const { dispatch } = useMeal();
|
|
||||||
const [searchString, setSearchString] = useState("");
|
|
||||||
const getSearchResults: MouseEventHandler<HTMLButtonElement> = (e) => {
|
|
||||||
searchString === "" ? e.preventDefault() : fetchSearchResults(dispatch, searchString);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearSearchBar = () => {
|
|
||||||
setSearchString("");
|
|
||||||
dispatch({ type: "clearSearchResults" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = ({ target: { value } }: ChangeEvent<HTMLInputElement>) =>
|
|
||||||
setSearchString(value);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="section">
|
|
||||||
<div className="container">
|
|
||||||
<div className=" nav-wrapper">
|
|
||||||
<div className="row center-align">
|
|
||||||
<form>
|
|
||||||
<div className="input-field col s10">
|
|
||||||
<input
|
|
||||||
className="validate"
|
|
||||||
id="search"
|
|
||||||
type="search"
|
|
||||||
required
|
|
||||||
name="search"
|
|
||||||
value={searchString}
|
|
||||||
placeholder="Search for a recipe"
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<label className="label-icon" htmlFor="search">
|
|
||||||
<i className="material-icons">search</i>
|
|
||||||
</label>
|
|
||||||
<i className="material-icons" onClick={clearSearchBar}>
|
|
||||||
close
|
|
||||||
</i>
|
|
||||||
</div>
|
|
||||||
<div className="col s2 valign-wrapper">
|
|
||||||
<Link to="/search">
|
|
||||||
<button
|
|
||||||
className="btn-floating waves-effect waves-light orange lighten-2"
|
|
||||||
type="submit"
|
|
||||||
name="searchButton"
|
|
||||||
value="Search"
|
|
||||||
onClick={getSearchResults}
|
|
||||||
>
|
|
||||||
<i className="material-icons right">send</i>
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import { MouseEventHandler } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { buttonURL, links } from "../constants";
|
|
||||||
import SpecialEventImage from "../images/special_event.svg";
|
|
||||||
import { FooterLink } from "./FooterLink";
|
|
||||||
import { RandomButton } from "./RandomButton";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
showNav: boolean;
|
|
||||||
closeNavClick: MouseEventHandler;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SideNav = ({ showNav, closeNavClick }: Props) => {
|
|
||||||
let transformStyle = {
|
|
||||||
transform: showNav ? "translateX(0%)" : "translateX(-105%)",
|
|
||||||
transition: "0.5s",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul id="slide-out" className="sidenav" style={transformStyle}>
|
|
||||||
<li>
|
|
||||||
<div className="user-view" style={{ height: "30vh" }}>
|
|
||||||
<div className="background">
|
|
||||||
<img
|
|
||||||
style={{
|
|
||||||
position: "fixed" /* Sit on top of the page content */,
|
|
||||||
width: "100%" /* Full width (cover the whole page) */,
|
|
||||||
height: "30vh" /* Full width (cover the whole page) */,
|
|
||||||
top: "0",
|
|
||||||
left: "0",
|
|
||||||
right: "0",
|
|
||||||
bottom: "0",
|
|
||||||
backgroundColor: "rgba(0,0,0,0.5)",
|
|
||||||
zIndex: 2,
|
|
||||||
}}
|
|
||||||
src={SpecialEventImage}
|
|
||||||
alt="sidenav_background"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<i className="material-icons right" onClick={closeNavClick}>
|
|
||||||
close
|
|
||||||
</i>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<RandomButton url={buttonURL} size="small" color="orange darken-2" />
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div className="divider" />
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link to="#" className="subheader">
|
|
||||||
Navigation
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{links.map((link, i) => (
|
|
||||||
<FooterLink key={i} link={link} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export const apiRoot = "https://www.themealdb.com/api/json/v1/1/";
|
|
||||||
export const buttonURL = "/random";
|
|
||||||
export const links = ["categories", "contact"];
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { Link, useRouteMatch } from "react-router-dom";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
strCategory: string;
|
|
||||||
strCategoryThumb: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CategoriesEntry = ({ strCategory, strCategoryThumb }: Props) => {
|
|
||||||
const { url } = useRouteMatch();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link to={`${url}/${strCategory}`}>
|
|
||||||
<li>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col s12">
|
|
||||||
<div className="card horizontal hoverable">
|
|
||||||
<div className="card-image valign-wrapper">
|
|
||||||
<img src={strCategoryThumb} alt={strCategory} />
|
|
||||||
</div>
|
|
||||||
<div className="card-stacked">
|
|
||||||
<div className="card-content black-text">
|
|
||||||
<h2 className="logo">{strCategory}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CategoriesEntry;
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import PageLayout from "../../../layouts/PageLayout";
|
|
||||||
import CategoriesEntry from "./CategoriesEntry";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
categories: { strCategory: string; strCategoryThumb: string }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CategoriesPage = ({ categories }: Props) => (
|
|
||||||
<PageLayout title="Chef's Categories">
|
|
||||||
<ul>
|
|
||||||
{categories.map(({ strCategory, strCategoryThumb }, i) => (
|
|
||||||
<CategoriesEntry key={i} strCategory={strCategory} strCategoryThumb={strCategoryThumb} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</PageLayout>
|
|
||||||
);
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { PreLoader } from "../../components/PreLoader";
|
|
||||||
import { getData } from "../../services/api";
|
|
||||||
import { CategoriesPage } from "./components/CategoriesPage";
|
|
||||||
|
|
||||||
export const Categories = () => {
|
|
||||||
const [categories, setCategories] = useState({ categories: [] });
|
|
||||||
|
|
||||||
const getCategories = () => getData("categories").then((data) => setCategories(data));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getCategories();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return categories.categories.length === 0 ? (
|
|
||||||
<PreLoader />
|
|
||||||
) : (
|
|
||||||
<CategoriesPage categories={categories.categories} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { CardEntry } from "../../../components/CardEntry";
|
|
||||||
import PageLayout from "../../../layouts/PageLayout";
|
|
||||||
import { MealSummary } from "../../../types/meal";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
meals: { meals: MealSummary[] };
|
|
||||||
strCategory: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CategoryPage = ({ meals, strCategory }: Props) => (
|
|
||||||
<PageLayout title={`Chef's ${strCategory} Recipes`}>
|
|
||||||
<ul>
|
|
||||||
<div className="row">
|
|
||||||
{meals.meals.map((meal) => (
|
|
||||||
<CardEntry meal={meal} key={meal.idMeal} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ul>
|
|
||||||
</PageLayout>
|
|
||||||
);
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Redirect, useParams } from "react-router-dom";
|
|
||||||
import { getData } from "../../services/api";
|
|
||||||
import { CategoryPage } from "./components/CategoryPage";
|
|
||||||
|
|
||||||
export const Category = () => {
|
|
||||||
const { strCategory } = useParams<{ strCategory: string }>();
|
|
||||||
const [meals, setMeals] = useState({ meals: [] });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getMeals = () => getData(strCategory, "filter");
|
|
||||||
getMeals().then((data) => setMeals(data));
|
|
||||||
}, [strCategory]);
|
|
||||||
|
|
||||||
return !meals.meals ? (
|
|
||||||
<Redirect to="/404" />
|
|
||||||
) : (
|
|
||||||
<CategoryPage meals={meals} strCategory={strCategory} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import { useState, FormEvent } from "react";
|
|
||||||
import { ContactFormInput } from "./ContactFormInput";
|
|
||||||
import { ContactFormSubmitButton } from "./ContactFormSubmitButton";
|
|
||||||
import { ContactFormTextArea } from "./ContactFormTextArea";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
setIsSubmitted: (value: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ContactForm = ({ setIsSubmitted }: Props) => {
|
|
||||||
const [firstName, setFirstName] = useState("");
|
|
||||||
const [lastName, setLastName] = useState("");
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [phone, setPhone] = useState("");
|
|
||||||
const [message, setMessage] = useState("");
|
|
||||||
|
|
||||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsSubmitted(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col s12">
|
|
||||||
<div className="col s12 m6">
|
|
||||||
<ContactFormInput id="First Name" value={firstName} dispatch={setFirstName} />
|
|
||||||
</div>
|
|
||||||
<div className="col s12 m6">
|
|
||||||
<ContactFormInput id="Last Name" value={lastName} dispatch={setLastName} />
|
|
||||||
</div>
|
|
||||||
<div className="col s12 m6">
|
|
||||||
<ContactFormInput id="Email" type="email" value={email} dispatch={setEmail} />
|
|
||||||
</div>
|
|
||||||
<div className="col s12 m6">
|
|
||||||
<ContactFormInput id="Phone" value={phone} type="tel" dispatch={setPhone} />
|
|
||||||
</div>
|
|
||||||
<div className="col s12">
|
|
||||||
<ContactFormTextArea id="Message" value={message} dispatch={setMessage} />
|
|
||||||
<ContactFormSubmitButton text="Send Message" color="orange darken-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { ChangeEventHandler, Dispatch, SetStateAction } from "react";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
id: string;
|
|
||||||
type?: string;
|
|
||||||
value: string;
|
|
||||||
dispatch: Dispatch<SetStateAction<string>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ContactFormInput = ({ id, type = "text", value, dispatch }: Props) => {
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dispatch(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="input-field">
|
|
||||||
{/* <i className="material-icons prefix">account_circle</i> */}
|
|
||||||
<input
|
|
||||||
className="validate"
|
|
||||||
type={type}
|
|
||||||
id={id}
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label htmlFor={id}>{id}</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
type Props = {
|
|
||||||
text: string;
|
|
||||||
color: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ContactFormSubmitButton = ({ text, color }: Props) => (
|
|
||||||
<button className={`waves-effect waves-light btn ${color}`} type="submit" name="submit">
|
|
||||||
<i className="material-icons right">send</i> {text}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
export const ContactFormSubmitted = () => (
|
|
||||||
<div className="container center-align">
|
|
||||||
<img
|
|
||||||
className="responsive-img"
|
|
||||||
src={require("../../../images/mail_sent.svg")}
|
|
||||||
alt="mail_sent"
|
|
||||||
width="30%"
|
|
||||||
/>
|
|
||||||
<h4>Thank you for your message</h4>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { ChangeEventHandler, Dispatch, FC, SetStateAction } from "react";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
id: string;
|
|
||||||
value: string;
|
|
||||||
dispatch: Dispatch<SetStateAction<string>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ContactFormTextArea: FC<Props> = ({ id, value, dispatch }) => {
|
|
||||||
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dispatch(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="input-field">
|
|
||||||
<label htmlFor={id}>{id}</label>
|
|
||||||
<textarea
|
|
||||||
className="materialize-textarea validate"
|
|
||||||
rows={12}
|
|
||||||
name={id}
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { useState } from "react";
|
|
||||||
import PageLayout from "../../layouts/PageLayout";
|
|
||||||
import { ContactForm } from "./components/ContactForm";
|
|
||||||
import { ContactFormSubmitted } from "./components/ContactFormSubmitted";
|
|
||||||
|
|
||||||
export const Contact = () => {
|
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
||||||
|
|
||||||
return isSubmitted ? (
|
|
||||||
<ContactFormSubmitted />
|
|
||||||
) : (
|
|
||||||
<PageLayout title="Contact Us">
|
|
||||||
<ContactForm setIsSubmitted={setIsSubmitted} />
|
|
||||||
</PageLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { RandomButton } from "../../components/RandomButton";
|
|
||||||
import { buttonURL } from "../../constants";
|
|
||||||
import HeroImage from "../../images/chef.svg";
|
|
||||||
|
|
||||||
export const Home = () => (
|
|
||||||
<section className="container ">
|
|
||||||
<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" />
|
|
||||||
</div>
|
|
||||||
<picture className="col s12 m6">
|
|
||||||
<img src={HeroImage} alt="hero_image" width="100%" />
|
|
||||||
</picture>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
type Props = { ingredients: string[][] };
|
|
||||||
|
|
||||||
export const MealIngredientList = ({ ingredients }: Props) => (
|
|
||||||
<div className="ingredientList">
|
|
||||||
<table className="striped highlight responsive-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Ingredient</th>
|
|
||||||
<th>Quantity</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{ingredients.map((ing, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>{ing[0]}</td>
|
|
||||||
<td>{ing[1]}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { Meal } from "../../../types/meal";
|
|
||||||
import { MealIngredientList } from "./MealIngredientList";
|
|
||||||
import { MealPresentation } from "./MealPresentation";
|
|
||||||
import { MealRecipe } from "./MealRecipe";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
ingredients: string[][];
|
|
||||||
recipe: string;
|
|
||||||
meal: Meal;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MealPage = ({ meal, ingredients, recipe }: Props) => (
|
|
||||||
<section className="container">
|
|
||||||
<div className="row">
|
|
||||||
<div className="col s12 l6">
|
|
||||||
<MealPresentation meal={meal} />
|
|
||||||
</div>
|
|
||||||
<div className="col s12 l6">
|
|
||||||
<MealIngredientList ingredients={ingredients} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<MealRecipe recipe={recipe} />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { Meal } from "../../../types/meal";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
meal: Meal;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MealPresentation = ({
|
|
||||||
meal: { mealName, imgAddress, videoAddress, mealCategory, mealArea },
|
|
||||||
}: Props) => {
|
|
||||||
return (
|
|
||||||
<div className="row">
|
|
||||||
<div className="col s12">
|
|
||||||
<div className="card orange lighten-1">
|
|
||||||
<div className="card-content black-text">
|
|
||||||
<span className="card-title">{mealName}</span>
|
|
||||||
<img className="responsive-img" src={imgAddress} alt={mealName} />
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<div className="chip">
|
|
||||||
<b>Video:</b>
|
|
||||||
<a href={videoAddress} target="blank" rel="noopener">
|
|
||||||
<i className="close material-icons">video_library</i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chip">
|
|
||||||
<b>Category: </b> {mealCategory}
|
|
||||||
<Link to={`/categories/${mealCategory}`}>
|
|
||||||
<i className="close material-icons">call_made</i>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chip">
|
|
||||||
<b>Origin:</b> {mealArea}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
type Props = { recipe: string };
|
|
||||||
|
|
||||||
export const MealRecipe = ({ recipe }: Props) => (
|
|
||||||
<div className="recipe">
|
|
||||||
<div className="divider" />
|
|
||||||
<h3>Instructions</h3>
|
|
||||||
<p className="flow-text">{recipe}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { useMeal } from "../../store/meal";
|
|
||||||
import { fetchMeal, fetchRandomMeal } from "../../store/meal/async";
|
|
||||||
import { NotFound } from "../NotFound";
|
|
||||||
import { MealPage } from "./components/MealPage";
|
|
||||||
import { buildIngredientList, buildMealProps } from "./service";
|
|
||||||
|
|
||||||
export const Meal = () => {
|
|
||||||
// hooks
|
|
||||||
const { id } = useParams<{ id: string }>();
|
|
||||||
const { state, dispatch } = useMeal();
|
|
||||||
// variables
|
|
||||||
const mealItem = state.meals?.[0];
|
|
||||||
// effects
|
|
||||||
/** Fetch meal from db */
|
|
||||||
useEffect(() => {
|
|
||||||
!id ? fetchRandomMeal(dispatch) : fetchMeal(dispatch, id);
|
|
||||||
}, [id, dispatch]);
|
|
||||||
|
|
||||||
const item = buildMealProps(mealItem);
|
|
||||||
const ingredients = buildIngredientList(mealItem);
|
|
||||||
|
|
||||||
return !!state.meals ? (
|
|
||||||
<MealPage
|
|
||||||
meal={item}
|
|
||||||
ingredients={ingredients}
|
|
||||||
recipe={mealItem?.strInstructions}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<NotFound />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import { MealApi } from "../../types/meal";
|
|
||||||
|
|
||||||
export const buildIngredientList = (mealItem: MealApi): string[][] => {
|
|
||||||
let ingredients = [];
|
|
||||||
for (let i = 1; i <= 20; i++) {
|
|
||||||
let strIng = `strIngredient${i}`;
|
|
||||||
let strMes = `strMeasure${i}`;
|
|
||||||
// @ts-ignore
|
|
||||||
if (!!mealItem?.[strIng] && !!mealItem?.[strIng]) {
|
|
||||||
// @ts-ignore
|
|
||||||
ingredients.push([mealItem?.[strIng], mealItem?.[strMes]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ingredients;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const buildMealProps = (mealItem: MealApi) => ({
|
|
||||||
mealName: mealItem?.strMeal,
|
|
||||||
imgAddress: mealItem?.strMealThumb,
|
|
||||||
videoAddress: mealItem?.strYoutube,
|
|
||||||
mealCategory: mealItem?.strCategory,
|
|
||||||
mealArea: mealItem?.strArea,
|
|
||||||
});
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import { RandomButton } from "../../components/RandomButton";
|
|
||||||
|
|
||||||
export const NotFound = () => (
|
|
||||||
<div className="container center-align">
|
|
||||||
<div className="row">
|
|
||||||
<h1>Wrong Way!</h1>
|
|
||||||
<div className="col s12 offset-m3 m6">
|
|
||||||
<div className="card hoverable">
|
|
||||||
<div className="card-image">
|
|
||||||
<img
|
|
||||||
className="responsive-img"
|
|
||||||
src="https://images.otstatic.com/prod/26153735/2/large.jpg"
|
|
||||||
alt="404 not found"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="card-content">
|
|
||||||
<RandomButton url="/random" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import BreakfastImage from "../../../images/breakfast.svg";
|
|
||||||
import PageLayout from "../../../layouts/PageLayout";
|
|
||||||
import { MealSummary } from "../../../types/meal";
|
|
||||||
import { SearchResult } from "./SearchResult";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
searchString: string;
|
|
||||||
searchResults: MealSummary[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SearchPage = ({ searchString, searchResults }: Props) => (
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { CardEntry } from "../../../components/CardEntry";
|
|
||||||
import { MealSummary } from "../../../types/meal";
|
|
||||||
|
|
||||||
type Props = { meal: MealSummary };
|
|
||||||
|
|
||||||
export const SearchResult = ({ meal }: Props) => <CardEntry meal={meal} />;
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import { useMeal } from "../../store/meal";
|
|
||||||
import { SearchPage } from "./components/SearchPage";
|
|
||||||
|
|
||||||
export const Search = () => {
|
|
||||||
const {
|
|
||||||
state: { searchString, search },
|
|
||||||
} = useMeal();
|
|
||||||
return <SearchPage searchString={searchString} searchResults={search} />;
|
|
||||||
};
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
|
@ -1 +0,0 @@
|
||||||
<svg id="fd577a60-d552-4fe8-bffb-d9cccd3352c0" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="788.38181" height="719" viewBox="0 0 788.38181 719"><defs><linearGradient id="aeee3e51-5d06-43ae-b234-e7a12d326f50" x1="365" y1="605" x2="365" y2="286" gradientUnits="userSpaceOnUse"><stop offset="0" stop-opacity="0.12"/><stop offset="0.55135" stop-opacity="0.09"/><stop offset="1" stop-opacity="0.02"/></linearGradient><linearGradient id="acb63a3b-00bb-44a8-8877-bb0607d49a63" x1="1117.61899" y1="-43.05793" x2="1117.61899" y2="-102.40539" gradientTransform="matrix(-0.64881, 0.76095, 0.76095, 0.64881, 817.40491, -656.85567)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="gray" stop-opacity="0.25"/><stop offset="0.53514" stop-color="gray" stop-opacity="0.12"/><stop offset="1" stop-color="gray" stop-opacity="0.1"/></linearGradient></defs><title>Mail sent</title><polygon points="125 410 0 268 374 0 748 268 621 410 125 410" fill="#ee6e73"/><polygon points="125 410 0 268 374 0 748 268 621 410 125 410" fill="#514abf"/><rect y="286" width="730" height="319" fill="#ee6e73"/><rect y="286" width="730" height="319" fill="url(#aeee3e51-5d06-43ae-b234-e7a12d326f50)"/><polygon points="748 719 0 719 0 268 374 494 748 268 748 719" fill="#ee6e73"/><polygon points="652.582 116.818 573.252 55.99 363.113 330.042 246.402 240.55 185.573 319.879 341.938 439.598 341.938 439.598 382.036 469.649 652.582 116.818" fill="#3ad29f"/><polygon points="3.153 166.882 0.179 168.334 0.521 167.726 0.26 167.81 0.626 167.539 40.653 96.307 61.114 113.753 78.667 130.004 73.627 132.465 79.293 142.459 3.153 166.882" fill="url(#acb63a3b-00bb-44a8-8877-bb0607d49a63)"/><polygon points="59.087 115.617 75.131 130.493 2.411 166.646 41.11 117.046 59.087 115.617" fill="#ee6e73"/><polygon points="59.087 115.617 75.131 130.493 2.411 166.646 41.11 117.046 59.087 115.617" opacity="0.2"/><polygon points="40.373 99.66 2.411 166.646 59.087 115.617 40.373 99.66" fill="#ee6e73"/><polygon points="75.599 142.01 2.489 166.16 63.604 120.539 75.599 142.01" fill="#ee6e73"/><polygon points="686.959 38.73 670.942 66.499 788.382 85.622 712.742 32.949 686.959 38.73" fill="#ee6e73"/><polygon points="686.959 38.73 670.942 66.499 788.382 85.622 712.742 32.949 686.959 38.73" opacity="0.2"/><polygon points="706.247 8.293 788.382 85.622 686.959 38.73 706.247 8.293" fill="#ee6e73"/><polygon points="675.274 82.821 788.062 84.976 682.769 47.576 675.274 82.821" fill="#ee6e73"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
|
@ -1,34 +0,0 @@
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
|
||||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-footer {
|
|
||||||
padding-top: 20px;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #ffe0b2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
font-family: "Marck Script", cursive;
|
|
||||||
}
|
|
||||||
|
|
||||||
a>i.material-icons {
|
|
||||||
color: #ff9800;
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
import "./index.css";
|
|
||||||
import { App } from "./App";
|
|
||||||
import * as serviceWorker from "./serviceWorker";
|
|
||||||
import { AppProvider } from "./store/meal";
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<AppProvider>
|
|
||||||
<App />
|
|
||||||
</AppProvider>,
|
|
||||||
document.getElementById("root")
|
|
||||||
);
|
|
||||||
|
|
||||||
serviceWorker.register();
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import { FC, MouseEvent, MouseEventHandler, useState } from "react";
|
|
||||||
import { Footer } from "../components/Footer";
|
|
||||||
import { Navbar } from "../components/Navbar";
|
|
||||||
import { SearchBar } from "../components/SearchBar";
|
|
||||||
import { SideNav } from "../components/SideNav";
|
|
||||||
|
|
||||||
const MainLayout: FC = ({ children }) => {
|
|
||||||
const [showNav, setShowNav] = useState(false);
|
|
||||||
|
|
||||||
const openNavClick: MouseEventHandler = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setShowNav(true);
|
|
||||||
document.addEventListener("keydown", handleEscKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeNavClick = (e: MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setShowNav(false);
|
|
||||||
document.removeEventListener("keydown", handleEscKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEscKey = ({ key }: KeyboardEvent) => {
|
|
||||||
if (key === "Escape") {
|
|
||||||
setShowNav(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<header>
|
|
||||||
<Navbar openNavClick={openNavClick} />
|
|
||||||
<SearchBar />
|
|
||||||
<SideNav showNav={showNav} closeNavClick={closeNavClick} />
|
|
||||||
</header>
|
|
||||||
<main>{children}</main>
|
|
||||||
<Footer />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MainLayout;
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
type Props = { title: string };
|
|
||||||
|
|
||||||
const PageLayout: FC<Props> = ({ title, children }) => (
|
|
||||||
<div className="container">
|
|
||||||
<h1 className="logo">{title}</h1>
|
|
||||||
<main>{children}</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default PageLayout;
|
|
||||||
1
src/react-app-env.d.ts
vendored
|
|
@ -1 +0,0 @@
|
||||||
/// <reference types="react-scripts" />
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
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 { Search } from "../containers/Search";
|
|
||||||
|
|
||||||
export const AppRouter = () => (
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/">
|
|
||||||
<Home />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route exact path={buttonURL}>
|
|
||||||
<Meal />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route exact path="/categories">
|
|
||||||
<Categories />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path="/categories/:strCategory/">
|
|
||||||
<Category />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route exact path="/search">
|
|
||||||
<Search />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path="/contact">
|
|
||||||
<Contact />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path="/404">
|
|
||||||
<NotFound />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path="/:id">
|
|
||||||
<Meal />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path="*">
|
|
||||||
<Redirect to="/404" />
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { FC, useEffect } from "react";
|
|
||||||
import { Router as RouterOriginal, useLocation } from "react-router-dom";
|
|
||||||
import history from "../utils/history";
|
|
||||||
|
|
||||||
export const Router: FC = ({ children }) => (
|
|
||||||
<RouterOriginal history={history}>
|
|
||||||
<ScrollToTop />
|
|
||||||
{children}
|
|
||||||
</RouterOriginal>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ScrollToTop: FC = () => {
|
|
||||||
const location = useLocation();
|
|
||||||
useEffect(() => {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}, [location.pathname]);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export { AppRouter } from "./AppRouter";
|
|
||||||
export { Router } from "./Router";
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
// This optional code is used to register a service worker.
|
|
||||||
// register() is not called by default.
|
|
||||||
|
|
||||||
// This lets the app load faster on subsequent visits in production, and gives
|
|
||||||
// it offline capabilities. However, it also means that developers (and users)
|
|
||||||
// will only see deployed updates on subsequent visits to a page, after all the
|
|
||||||
// existing tabs open on the page have been closed, since previously cached
|
|
||||||
// resources are updated in the background.
|
|
||||||
|
|
||||||
// To learn more about the benefits of this model and instructions on how to
|
|
||||||
// opt-in, read https://bit.ly/CRA-PWA
|
|
||||||
|
|
||||||
const isLocalhost = Boolean(
|
|
||||||
window.location.hostname === "localhost" ||
|
|
||||||
// [::1] is the IPv6 localhost address.
|
|
||||||
window.location.hostname === "[::1]" ||
|
|
||||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
|
||||||
window.location.hostname.match(
|
|
||||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export function register(config: any) {
|
|
||||||
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
|
|
||||||
// The URL constructor is available in all browsers that support SW.
|
|
||||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
|
||||||
// from what our page is served on. This might happen if a CDN is used to
|
|
||||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
|
||||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
|
||||||
|
|
||||||
if (isLocalhost) {
|
|
||||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
|
||||||
checkValidServiceWorker(swUrl, config);
|
|
||||||
|
|
||||||
// Add some additional logging to localhost, pointing developers to the
|
|
||||||
// service worker/PWA documentation.
|
|
||||||
navigator.serviceWorker.ready.then(() => {
|
|
||||||
console.log(
|
|
||||||
"This web app is being served cache-first by a service " +
|
|
||||||
"worker. To learn more, visit https://bit.ly/CRA-PWA"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Is not localhost. Just register service worker
|
|
||||||
registerValidSW(swUrl, config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerValidSW(swUrl: any, config: any) {
|
|
||||||
navigator.serviceWorker
|
|
||||||
.register(swUrl)
|
|
||||||
.then((registration) => {
|
|
||||||
registration.onupdatefound = () => {
|
|
||||||
const installingWorker = registration.installing;
|
|
||||||
if (installingWorker == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
installingWorker.onstatechange = () => {
|
|
||||||
if (installingWorker.state === "installed") {
|
|
||||||
if (navigator.serviceWorker.controller) {
|
|
||||||
// At this point, the updated precached content has been fetched,
|
|
||||||
// but the previous service worker will still serve the older
|
|
||||||
// content until all client tabs are closed.
|
|
||||||
console.log(
|
|
||||||
"New content is available and will be used when all " +
|
|
||||||
"tabs for this page are closed. See https://bit.ly/CRA-PWA."
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute callback
|
|
||||||
if (config && config.onUpdate) {
|
|
||||||
config.onUpdate(registration);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// At this point, everything has been precached.
|
|
||||||
// It's the perfect time to display a
|
|
||||||
// "Content is cached for offline use." message.
|
|
||||||
console.log("Content is cached for offline use.");
|
|
||||||
|
|
||||||
// Execute callback
|
|
||||||
if (config && config.onSuccess) {
|
|
||||||
config.onSuccess(registration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error during service worker registration:", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkValidServiceWorker(swUrl: any, config: any) {
|
|
||||||
// Check if the service worker can be found. If it can't reload the page.
|
|
||||||
fetch(swUrl, {
|
|
||||||
headers: { "Service-Worker": "script" },
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
// Ensure service worker exists, and that we really are getting a JS file.
|
|
||||||
const contentType = response.headers.get("content-type");
|
|
||||||
if (
|
|
||||||
response.status === 404 ||
|
|
||||||
(contentType != null && contentType.indexOf("javascript") === -1)
|
|
||||||
) {
|
|
||||||
// No service worker found. Probably a different app. Reload the page.
|
|
||||||
navigator.serviceWorker.ready.then((registration) => {
|
|
||||||
registration.unregister().then(() => {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Service worker found. Proceed as normal.
|
|
||||||
registerValidSW(swUrl, config);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.log(
|
|
||||||
"No internet connection found. App is running in offline mode."
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unregister() {
|
|
||||||
if ("serviceWorker" in navigator) {
|
|
||||||
navigator.serviceWorker.ready.then((registration) => {
|
|
||||||
registration.unregister();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { apiRoot } from "../constants";
|
|
||||||
|
|
||||||
type Option = "filter" | "lookup" | "search";
|
|
||||||
|
|
||||||
const createURI = (keyword: string, option?: Option) => {
|
|
||||||
if (!option) {
|
|
||||||
return `${apiRoot}${keyword}.php`;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (option) {
|
|
||||||
case "filter": {
|
|
||||||
return `${apiRoot}${option}.php?c=${keyword}`;
|
|
||||||
}
|
|
||||||
case "lookup": {
|
|
||||||
return `${apiRoot}${option}.php?i=${keyword}`;
|
|
||||||
}
|
|
||||||
case "search": {
|
|
||||||
return `${apiRoot}${option}.php?s=${keyword}`;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw Error("Unexpected URI");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getData = async (keyword: string, option?: Option) => {
|
|
||||||
const URI = createURI(keyword, option);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(URI);
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
return console.warn(error + "url:" + URI);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
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 },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
//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>;
|
|
||||||
};
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
export interface Meal {
|
|
||||||
mealName: string;
|
|
||||||
imgAddress: string;
|
|
||||||
videoAddress: string;
|
|
||||||
mealCategory: string;
|
|
||||||
mealArea: string;
|
|
||||||
isFav?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MealSummary {
|
|
||||||
idMeal: string;
|
|
||||||
strMeal: string;
|
|
||||||
strMealThumb: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MealApi {
|
|
||||||
idMeal: string;
|
|
||||||
strMeal: string;
|
|
||||||
strMealThumb: string;
|
|
||||||
strYoutube: string;
|
|
||||||
strCategory: string;
|
|
||||||
strArea: string;
|
|
||||||
strInstructions: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
import { createBrowserHistory } from "history";
|
|
||||||
export default createBrowserHistory();
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
// This must be set on the server using or firebase functions
|
|
||||||
import { createTransport } from "nodemailer";
|
|
||||||
import { mailAdress, mailPassword } from "./secret";
|
|
||||||
|
|
||||||
const myMail = mailAdress;
|
|
||||||
const myPass = mailPassword;
|
|
||||||
|
|
||||||
const handleMail = (mailTo, subject, text) => {
|
|
||||||
let transporter = createTransport({
|
|
||||||
service: "gmail",
|
|
||||||
auth: {
|
|
||||||
user: myMail,
|
|
||||||
pass: myPass,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let mailOptions = {
|
|
||||||
from: myMail,
|
|
||||||
to: mailTo,
|
|
||||||
subject: subject,
|
|
||||||
text: text,
|
|
||||||
};
|
|
||||||
|
|
||||||
transporter.sendMail(mailOptions, function (error, info) {
|
|
||||||
if (error) {
|
|
||||||
console.log(error);
|
|
||||||
} else {
|
|
||||||
console.log("Email sent: " + info.response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const confirmationMail = (
|
|
||||||
mailTo,
|
|
||||||
subject = "Your message has been sent",
|
|
||||||
text = "Thanks for your message. We'll reply you soon."
|
|
||||||
) => {
|
|
||||||
handleMail(mailTo, subject, text);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const notificationMail = (mailTo = myMail, subject, text) => {
|
|
||||||
handleMail(mailTo, subject, text);
|
|
||||||
};
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export const upFirstChar = (lower: string): string =>
|
|
||||||
lower.replace(/^\w/, (c) => c.toUpperCase());
|
|
||||||