mirror of
https://github.com/rjNemo/meal_planner
synced 2026-06-06 02:26:49 +00:00
parent
0d8cc9c9b3
commit
cb101b22ec
43 changed files with 447 additions and 291 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
build/
|
||||
node_modules/
|
||||
/.idea
|
||||
/.idea
|
||||
.env
|
||||
1
TODO.md
1
TODO.md
|
|
@ -14,3 +14,4 @@
|
|||
- [ ] Use Css-in-Js
|
||||
- [ ] Redirect to 404
|
||||
- [x] Typescript
|
||||
- [ ] strict typing
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
"devDependencies": {
|
||||
"@types/node": "^14.14.37",
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"typescript": "^4.2.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,16 @@ 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: [] });
|
||||
const [meal, setMeal] = useState(null);
|
||||
const [searchResults, setSearchResults] = useState({
|
||||
meals: [] as MealSummary[],
|
||||
});
|
||||
const [_, setMeal] = useState(null);
|
||||
|
||||
const getRandomMeal = () => {
|
||||
getData("random", setMeal);
|
||||
|
|
|
|||
|
|
@ -1,120 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
// import { notificationMail, confirmationMail } from "../utils/mail";
|
||||
|
||||
export const ContactForm = ({ setIsSubmitted }) => {
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
// confirmationMail(email);
|
||||
// const body = `Sender: ${firstName} ${lastName}\nPhone: ${phone}\nMessage: ${message}`;
|
||||
// notificationMail(email, `New message from ${firstName} ${lastName}`, body);
|
||||
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}
|
||||
/>
|
||||
<ContactFormSubmit text="Send Message" color="orange darken-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const ContactFormInput = ({ id, type = "text", value, dispatch }) => {
|
||||
const handleChange = (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>
|
||||
);
|
||||
};
|
||||
|
||||
const ContactFormTextArea = ({ id, value, dispatch }) => {
|
||||
const handleChange = (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>
|
||||
);
|
||||
};
|
||||
|
||||
const ContactFormSubmit = ({ text, color }) => {
|
||||
return (
|
||||
<button
|
||||
className={`waves-effect waves-light btn ${color}`}
|
||||
type="submit"
|
||||
name="submit"
|
||||
>
|
||||
<i className="material-icons right">send</i> {text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
import React from "react";
|
||||
import { FC } from "react";
|
||||
|
||||
export const CopyrightText = () => {
|
||||
return (
|
||||
<span className="grey-text text-darken-1">
|
||||
© 2020 - <span className="logo">Chef's</span> - Made with{" "}
|
||||
<span role="img" aria-label="heart">
|
||||
❤️
|
||||
</span>
|
||||
export const CopyrightText: FC = () => (
|
||||
<span className="grey-text text-darken-1">
|
||||
© 2020 - <span className="logo">Chef's</span> - Made with{" "}
|
||||
<span role="img" aria-label="heart">
|
||||
❤️
|
||||
</span>
|
||||
);
|
||||
};
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import React from "react";
|
||||
import { FC } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { upFirstChar } from "../utils/methods";
|
||||
|
||||
export const FooterLink = ({ link, textColor = "" }) => {
|
||||
type Props = {
|
||||
link: string;
|
||||
textColor?: string;
|
||||
};
|
||||
|
||||
export const FooterLink: FC<Props> = ({ link, textColor = "" }) => {
|
||||
const textColorClass = `${textColor}-text`;
|
||||
return (
|
||||
<li>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import React from "react";
|
||||
import { FC } from "react";
|
||||
|
||||
export const GitHubLink = () => {
|
||||
return (
|
||||
<a
|
||||
className="grey-text text-darken-1 right"
|
||||
href="https://github.com/rjNemo/meal_planner"
|
||||
target="blank"
|
||||
rel="noopener"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
);
|
||||
};
|
||||
export const GitHubLink: FC = () => (
|
||||
<a
|
||||
className="grey-text text-darken-1 right"
|
||||
href="https://github.com/rjNemo/meal_planner"
|
||||
target="blank"
|
||||
rel="noopener"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import React from "react";
|
||||
import { FC } from "react";
|
||||
import { useAuth0 } from "../utils/auth0-spa";
|
||||
|
||||
export const LogInButton = ({ color }) => {
|
||||
type Props = {
|
||||
color: string;
|
||||
};
|
||||
|
||||
export const LogInButton: FC<Props> = ({ color }) => {
|
||||
const { loginWithRedirect } = useAuth0();
|
||||
const handleClick = () => {
|
||||
loginWithRedirect({});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import { FC } from "react";
|
||||
import { useAuth0 } from "../utils/auth0-spa";
|
||||
|
||||
export const LogOutButton = () => {
|
||||
export const LogOutButton: FC = () => {
|
||||
const { logout } = useAuth0();
|
||||
const handleClick = () => {
|
||||
logout();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import React from "react";
|
||||
import { FC } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const Logo = () => {
|
||||
export const Logo: FC = () => {
|
||||
return (
|
||||
<Link to="/" className="brand-logo">
|
||||
<img
|
||||
// className="responsive-img"
|
||||
src="/logo192.png"
|
||||
alt="chef's logo"
|
||||
height="30px"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React, { FC } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { buttonURL, links } from "../constants";
|
||||
import { useAuth0 } from "../utils/auth0-spa";
|
||||
|
|
@ -7,7 +8,12 @@ import { Logo } from "./Logo";
|
|||
import { LogOutButton } from "./LogOutButton";
|
||||
import { RandomButton } from "./RandomButton";
|
||||
|
||||
export const Navbar = ({ openNavClick, handleClick }) => {
|
||||
type Props = {
|
||||
openNavClick: React.MouseEventHandler;
|
||||
handleClick: () => void;
|
||||
};
|
||||
|
||||
export const Navbar: FC<Props> = ({ openNavClick, handleClick }) => {
|
||||
const { isAuthenticated } = useAuth0();
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
import React from "react";
|
||||
import { FC } from "react";
|
||||
|
||||
export const PreLoader = () => {
|
||||
return (
|
||||
<div className="preloader-wrapper active">
|
||||
<div className="spinner-layer spinner-red-only">
|
||||
<div className="circle-clipper left">
|
||||
<div className="circle"></div>
|
||||
</div>
|
||||
<div className="gap-patch">
|
||||
<div className="circle"></div>
|
||||
</div>
|
||||
<div className="circle-clipper right">
|
||||
<div className="circle"></div>
|
||||
</div>
|
||||
export const PreLoader: FC = () => (
|
||||
<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,7 +1,19 @@
|
|||
import React from "react";
|
||||
import { FC } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const RandomButton = ({ url, size = "large", handleClick, color }) => {
|
||||
type Props = {
|
||||
url: string;
|
||||
size?: string;
|
||||
handleClick: () => void;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export const RandomButton: FC<Props> = ({
|
||||
url,
|
||||
size = "large",
|
||||
handleClick,
|
||||
color,
|
||||
}) => {
|
||||
const classString = `waves-effect waves-light btn-${size} ${color}`;
|
||||
return (
|
||||
<Link to={url}>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
import { ChangeEvent } from "react";
|
||||
import React, { ChangeEvent, FC } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { getData } from "../services/api";
|
||||
import { MealSummary } from "../types/meal";
|
||||
|
||||
export const SearchBar = ({
|
||||
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,
|
||||
}) => {
|
||||
const getSearchResults = (e) => {
|
||||
const getSearchResults: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
searchString === ""
|
||||
? e.preventDefault()
|
||||
: getData(searchString, setSearchResults, "search");
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React, { FC } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { buttonURL, links } from "../constants";
|
||||
import ChefImage from "../images/chef.svg";
|
||||
|
|
@ -8,7 +9,13 @@ import { LogInButton } from "./LogInButton";
|
|||
import { LogOutButton } from "./LogOutButton";
|
||||
import { RandomButton } from "./RandomButton";
|
||||
|
||||
export const SideNav = ({ showNav, closeNavClick, handleClick }) => {
|
||||
type Props = {
|
||||
showNav: boolean;
|
||||
closeNavClick: React.MouseEventHandler;
|
||||
handleClick: () => void;
|
||||
};
|
||||
|
||||
export const SideNav: FC<Props> = ({ showNav, closeNavClick, handleClick }) => {
|
||||
const { isAuthenticated, user } = useAuth0();
|
||||
let transformStyle = {
|
||||
transform: showNav ? "translateX(0%)" : "translateX(-105%)",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { PreLoader } from "../../components/PreLoader";
|
||||
import { getData } from "../../services/api";
|
||||
import { CategoriesPage } from "./components/CategoriesPage";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { getData } from "../../services/api";
|
|||
import { CategoryPage } from "./components/CategoryPage";
|
||||
|
||||
export const Category: FC = () => {
|
||||
const { strCategory } = useParams();
|
||||
const { strCategory } = useParams<{ strCategory: string }>();
|
||||
const [meals, setMeals] = useState({ meals: [] });
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
71
src/containers/Contact/components/ContactForm.tsx
Normal file
71
src/containers/Contact/components/ContactForm.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import React, { FC, useState } from "react";
|
||||
import { ContactFormInput } from "./ContactFormInput";
|
||||
import { ContactFormSubmitButton } from "./ContactFormSubmitButton";
|
||||
import { ContactFormTextArea } from "./ContactFormTextArea";
|
||||
|
||||
type Props = {
|
||||
setIsSubmitted: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const ContactForm: FC<Props> = ({ setIsSubmitted }) => {
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const handleSubmit = (e: React.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>
|
||||
);
|
||||
};
|
||||
35
src/containers/Contact/components/ContactFormInput.tsx
Normal file
35
src/containers/Contact/components/ContactFormInput.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import React, { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
type?: string;
|
||||
value: string;
|
||||
dispatch: React.Dispatch<React.SetStateAction<string>>;
|
||||
};
|
||||
|
||||
export const ContactFormInput: FC<Props> = ({
|
||||
id,
|
||||
type = "text",
|
||||
value,
|
||||
dispatch,
|
||||
}) => {
|
||||
const handleChange: React.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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
export const ContactFormSubmitButton: FC<Props> = ({ text, color }) => (
|
||||
<button
|
||||
className={`waves-effect waves-light btn ${color}`}
|
||||
type="submit"
|
||||
name="submit"
|
||||
>
|
||||
<i className="material-icons right">send</i> {text}
|
||||
</button>
|
||||
);
|
||||
13
src/containers/Contact/components/ContactFormSubmitted.tsx
Normal file
13
src/containers/Contact/components/ContactFormSubmitted.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export function ContactFormSubmitted() {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
28
src/containers/Contact/components/ContactFormTextArea.tsx
Normal file
28
src/containers/Contact/components/ContactFormTextArea.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import React, { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
value: string;
|
||||
dispatch: React.Dispatch<React.SetStateAction<string>>;
|
||||
};
|
||||
|
||||
export const ContactFormTextArea: FC<Props> = ({ id, value, dispatch }) => {
|
||||
const handleChange: React.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,20 +1,13 @@
|
|||
import { FC, useState } from "react";
|
||||
import PageLayout from "../../layouts/PageLayout";
|
||||
import { ContactForm } from "../../components/ContactForm";
|
||||
import { ContactForm } from "./components/ContactForm";
|
||||
import { ContactFormSubmitted } from "./components/ContactFormSubmitted";
|
||||
|
||||
export const Contact: FC = () => {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
return isSubmitted ? (
|
||||
<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>
|
||||
<ContactFormSubmitted />
|
||||
) : (
|
||||
<PageLayout title="Contact Us">
|
||||
<ContactForm setIsSubmitted={setIsSubmitted} />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React from "react";
|
||||
import { FC } from "react";
|
||||
import { RandomButton } from "../../components/RandomButton";
|
||||
import { buttonURL } from "../../constants";
|
||||
import HeroImage from "../../images/chef.svg";
|
||||
|
||||
export const Home = () => (
|
||||
export const Home: FC = () => (
|
||||
<section className="container ">
|
||||
<div className="row">
|
||||
<div className="col s12 m6">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
ingredients: string[];
|
||||
ingredients: string[][];
|
||||
};
|
||||
|
||||
export const MealIngredientList: FC<Props> = ({ ingredients }) => (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { MealPresentation } from "./MealPresentation";
|
|||
import { MealRecipe } from "./MealRecipe";
|
||||
|
||||
type Props = {
|
||||
ingredients: string[];
|
||||
ingredients: string[][];
|
||||
recipe: string;
|
||||
meal: Meal;
|
||||
handleFavChange: () => void;
|
||||
|
|
|
|||
|
|
@ -1,21 +1,37 @@
|
|||
import React, { FC, useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { NotFound } from "../NotFound";
|
||||
import { getData } from "../../services/api";
|
||||
import { useFirebase } from "../../services/Firebase";
|
||||
import { MealApi as MealType } from "../../types/meal";
|
||||
import { useAuth0 } from "../../utils/auth0-spa";
|
||||
import { NotFound } from "../NotFound";
|
||||
import { MealPage } from "./components/MealPage";
|
||||
import { getMeal, getRandomMeal } from "./service";
|
||||
import { buildIngredientList, buildMealProps } from "./service";
|
||||
|
||||
export const Meal: FC = () => {
|
||||
// hooks
|
||||
const { user, isAuthenticated } = useAuth0();
|
||||
const { id } = useParams();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const fb = useFirebase();
|
||||
// local state
|
||||
const [meal, setMeal] = useState(null);
|
||||
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);
|
||||
};
|
||||
|
||||
// effects
|
||||
/** Fetch meal from db */
|
||||
useEffect(() => {
|
||||
|
|
@ -24,7 +40,7 @@ export const Meal: FC = () => {
|
|||
/** Updates fav status in db */
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
fb.isFav(user.email, mealItem?.idMeal).then((res) => setIsFav(res));
|
||||
fb?.isFav(user.email, mealItem?.idMeal).then((res) => setIsFav(res));
|
||||
}
|
||||
}, [user, fb, mealItem?.idMeal, isAuthenticated]);
|
||||
// other logic
|
||||
|
|
@ -44,23 +60,8 @@ export const Meal: FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const item = {
|
||||
mealName: mealItem?.strMeal,
|
||||
imgAddress: mealItem?.strMealThumb,
|
||||
videoAddress: mealItem?.strYoutube,
|
||||
mealCategory: mealItem?.strCategory,
|
||||
mealArea: mealItem?.strArea,
|
||||
isFav,
|
||||
};
|
||||
|
||||
let ingredients = [];
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
let strIng = `strIngredient${i}`;
|
||||
let strMes = `strMeasure${i}`;
|
||||
if (!!mealItem?.[strIng] && !!mealItem?.[strIng]) {
|
||||
ingredients.push([mealItem?.[strIng], mealItem?.[strMes]]);
|
||||
}
|
||||
}
|
||||
const item = buildMealProps(mealItem, isFav!);
|
||||
const ingredients = buildIngredientList(mealItem);
|
||||
|
||||
return !!meal?.meals ? (
|
||||
<MealPage
|
||||
|
|
|
|||
|
|
@ -1,9 +1,24 @@
|
|||
import { getData } from "../../services/api";
|
||||
import { MealApi } from "../../types/meal";
|
||||
|
||||
export const getMeal = (id, setMeal) => {
|
||||
getData(id, setMeal, "lookup");
|
||||
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 getRandomMeal = (setMeal) => {
|
||||
getData("random", setMeal);
|
||||
};
|
||||
export const buildMealProps = (mealItem: MealApi, isFav: boolean) => ({
|
||||
mealName: mealItem?.strMeal,
|
||||
imgAddress: mealItem?.strMealThumb,
|
||||
videoAddress: mealItem?.strYoutube,
|
||||
mealCategory: mealItem?.strCategory,
|
||||
mealArea: mealItem?.strArea,
|
||||
isFav,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { FC } from "react";
|
||||
import { RandomButton } from "../../components/RandomButton";
|
||||
import { getRandomMeal } from "../Meal/service";
|
||||
|
||||
export const NotFound: FC = () => (
|
||||
<div className="container center-align">
|
||||
|
|
@ -16,11 +15,7 @@ export const NotFound: FC = () => (
|
|||
/>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<RandomButton
|
||||
url="/random"
|
||||
handleClick={getRandomMeal}
|
||||
color={null}
|
||||
/>
|
||||
<RandomButton url="/random" handleClick={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { FC, useEffect, useState } from "react";
|
||||
import { PreLoader } from "../../components/PreLoader";
|
||||
import { useFirebase } from "../../services/Firebase";
|
||||
import { MealSummary } from "../../types/meal";
|
||||
import { useAuth0 } from "../../utils/auth0-spa";
|
||||
import { ProfilePage } from "./components/ProfilePage";
|
||||
|
||||
export const Profile: FC = () => {
|
||||
const { loading, user } = useAuth0();
|
||||
const [favs, setFavs] = useState([]);
|
||||
const [favs, setFavs] = useState([] as MealSummary[]);
|
||||
const db = useFirebase();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,6 @@ nav {
|
|||
font-family: "Marck Script", cursive;
|
||||
}
|
||||
|
||||
i.material-icons {
|
||||
a>i.material-icons {
|
||||
color: #ff9800;
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "./index.css";
|
||||
import {App} from "./App";
|
||||
import { App } from "./App";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import { Auth0Provider } from "./utils/auth0-spa";
|
||||
import history from "./utils/history";
|
||||
import Firebase, { FirebaseContext } from "./services/Firebase";
|
||||
import config from "./utils/auth_config.json";
|
||||
|
||||
const onRedirectCallBack = (appState) => {
|
||||
history.push(
|
||||
|
|
@ -18,14 +17,13 @@ const onRedirectCallBack = (appState) => {
|
|||
|
||||
ReactDOM.render(
|
||||
<Auth0Provider
|
||||
// domain={process.env.DOMAIN}
|
||||
// client_id={process.env.CLIENT_ID}
|
||||
domain={config.DOMAIN}
|
||||
client_id={config.CLIENT_ID}
|
||||
domain={process.env.REACT_APP_DOMAIN}
|
||||
client_id={process.env.REACT_APP_CLIENT_ID}
|
||||
redirect_uri={window.location.origin}
|
||||
onRedirectCallBack={onRedirectCallBack}
|
||||
>
|
||||
<FirebaseContext.Provider value={new Firebase()}>
|
||||
{/*<FirebaseContext.Provider value={new Firebase()}> todo fix Firebase app*/}
|
||||
<FirebaseContext.Provider>
|
||||
<App />
|
||||
</FirebaseContext.Provider>
|
||||
</Auth0Provider>,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { FC, useState } from "react";
|
||||
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";
|
||||
|
||||
// TODO FC...
|
||||
const MainLayout = ({
|
||||
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,
|
||||
|
|
@ -14,19 +22,19 @@ const MainLayout = ({
|
|||
}) => {
|
||||
const [showNav, setShowNav] = useState(false);
|
||||
|
||||
const openNavClick = (e) => {
|
||||
const openNavClick: React.MouseEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
setShowNav(true);
|
||||
document.addEventListener("keydown", handleEscKey);
|
||||
};
|
||||
|
||||
const closeNavClick = (e) => {
|
||||
const closeNavClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setShowNav(false);
|
||||
document.removeEventListener("keydown", handleEscKey);
|
||||
};
|
||||
|
||||
const handleEscKey = (e) => {
|
||||
const handleEscKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
setShowNav(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { FC } from "react";
|
||||
import { Redirect, Route, Switch } from "react-router-dom";
|
||||
import { buttonURL } from "../constants";
|
||||
import { Categories } from "../containers/Categories";
|
||||
|
|
@ -8,11 +9,14 @@ 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";
|
||||
|
||||
//TODO: remove state from router move to containers
|
||||
|
||||
const AppRouter = ({ searchString, searchResults }) => (
|
||||
type Props = {
|
||||
searchString: string;
|
||||
searchResults: { meals: MealSummary[] };
|
||||
};
|
||||
const AppRouter: FC<Props> = ({ searchString, searchResults }) => (
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Home />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
import { useEffect } from "react";
|
||||
import { Route } from "react-router-dom";
|
||||
import { FC, useEffect } from "react";
|
||||
import { Route, RouteProps } from "react-router-dom";
|
||||
import { useAuth0 } from "../utils/auth0-spa";
|
||||
|
||||
// TODO use FC and props
|
||||
export const PrivateRoute = ({ component: Component, path, ...rest }) => {
|
||||
type Props = {
|
||||
component: FC;
|
||||
} & RouteProps;
|
||||
|
||||
export const PrivateRoute: FC<Props> = ({
|
||||
component: Component,
|
||||
path,
|
||||
...rest
|
||||
}) => {
|
||||
const { loading, isAuthenticated, loginWithRedirect } = useAuth0();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -18,7 +25,8 @@ export const PrivateRoute = ({ component: Component, path, ...rest }) => {
|
|||
fn();
|
||||
}, [loading, isAuthenticated, loginWithRedirect, path]);
|
||||
|
||||
const render = (props) => (isAuthenticated ? <Component {...props} /> : null);
|
||||
const render = (props: any) =>
|
||||
isAuthenticated ? <Component {...props} /> : null;
|
||||
|
||||
return <Route path={path} render={render} {...rest} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,17 +11,17 @@
|
|||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === "localhost" ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
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) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
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) {
|
||||
|
|
@ -31,7 +31,7 @@ export function register(config) {
|
|||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
window.addEventListener("load", () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
|
|
@ -42,8 +42,8 @@ export function register(config) {
|
|||
// 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'
|
||||
"This web app is being served cache-first by a service " +
|
||||
"worker. To learn more, visit https://bit.ly/CRA-PWA"
|
||||
);
|
||||
});
|
||||
} else {
|
||||
|
|
@ -54,24 +54,24 @@ export function register(config) {
|
|||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
function registerValidSW(swUrl: any, config: any) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
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.'
|
||||
"New content is available and will be used when all " +
|
||||
"tabs for this page are closed. See https://bit.ly/CRA-PWA."
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
|
|
@ -82,7 +82,7 @@ function registerValidSW(swUrl, config) {
|
|||
// 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.');
|
||||
console.log("Content is cached for offline use.");
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
|
|
@ -93,25 +93,25 @@ function registerValidSW(swUrl, config) {
|
|||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
.catch((error) => {
|
||||
console.error("Error during service worker registration:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
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' }
|
||||
headers: { "Service-Worker": "script" },
|
||||
})
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
(contentType != null && contentType.indexOf("javascript") === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
|
@ -123,14 +123,14 @@ function checkValidServiceWorker(swUrl, config) {
|
|||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
"No internet connection found. App is running in offline mode."
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { createContext, useContext } from "react";
|
||||
import Firebase from "./firebase";
|
||||
|
||||
// create a Firebase context to make state available anywhere in the App.
|
||||
const FirebaseContext = createContext(null);
|
||||
const FirebaseContext = createContext(new Firebase());
|
||||
|
||||
export const useFirebase = () => useContext(FirebaseContext);
|
||||
export default FirebaseContext;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,34 @@
|
|||
export const createURI = (keyword: string, option: string) => {
|
||||
import React from "react";
|
||||
|
||||
export const createURI = (keyword: string, option?: string) => {
|
||||
const ROOT = "https://www.themealdb.com/api/json/v1/1/";
|
||||
if (!option) {
|
||||
return `${ROOT}${keyword}.php`;
|
||||
} else if (option === "filter") {
|
||||
return `${ROOT}${option}.php?c=${keyword}`;
|
||||
} else if (option === "lookup") {
|
||||
return `${ROOT}${option}.php?i=${keyword}`;
|
||||
} else if (option === "search") {
|
||||
return `${ROOT}${option}.php?s=${keyword}`;
|
||||
}
|
||||
|
||||
switch (option) {
|
||||
case "filter": {
|
||||
return `${ROOT}${option}.php?c=${keyword}`;
|
||||
}
|
||||
case "lookup": {
|
||||
return `${ROOT}${option}.php?i=${keyword}`;
|
||||
}
|
||||
case "search": {
|
||||
return `${ROOT}${option}.php?s=${keyword}`;
|
||||
}
|
||||
default: {
|
||||
throw Error("Unexpected URI");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getData = (keyword: string, set, option: string = null) => {
|
||||
export const getData = (
|
||||
keyword: string,
|
||||
set: React.Dispatch<React.SetStateAction<any>>,
|
||||
option?: string
|
||||
) => {
|
||||
const URI = createURI(keyword, option);
|
||||
|
||||
fetch(URI)
|
||||
.then((response) => response.json())
|
||||
.catch((error) => console.warn(error + "url:" + URI))
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export default interface Meal {
|
|||
videoAddress: string;
|
||||
mealCategory: string;
|
||||
mealArea: string;
|
||||
isFav: boolean;
|
||||
isFav?: boolean;
|
||||
}
|
||||
|
||||
export interface MealSummary {
|
||||
|
|
@ -12,3 +12,13 @@ export interface MealSummary {
|
|||
strMeal: string;
|
||||
strMealThumb: string;
|
||||
}
|
||||
|
||||
export interface MealApi {
|
||||
idMeal: string;
|
||||
strMeal: string;
|
||||
strMealThumb: string;
|
||||
strYoutube: string;
|
||||
strCategory: string;
|
||||
strArea: string;
|
||||
strInstructions: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"DOMAIN": "chefs-meal-planner.eu.auth0.com",
|
||||
"CLIENT_ID": "EXe8HCfFd0jSSfqzjAvpdk72ce0y2Hh9"
|
||||
}
|
||||
|
|
@ -10,7 +10,10 @@
|
|||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": false,
|
||||
"strict": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
|
|
|
|||
24
yarn.lock
24
yarn.lock
|
|
@ -2037,6 +2037,11 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/history@*":
|
||||
version "4.7.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934"
|
||||
integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==
|
||||
|
||||
"@types/html-minifier-terser@^5.0.0":
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#3c9ee980f1a10d6021ae6632ca3e79ca2ec4fb50"
|
||||
|
|
@ -2129,7 +2134,24 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
|
||||
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
|
||||
|
||||
"@types/react@^17.0.3":
|
||||
"@types/react-router-dom@^5.1.7":
|
||||
version "5.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.7.tgz#a126d9ea76079ffbbdb0d9225073eb5797ab7271"
|
||||
integrity sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg==
|
||||
dependencies:
|
||||
"@types/history" "*"
|
||||
"@types/react" "*"
|
||||
"@types/react-router" "*"
|
||||
|
||||
"@types/react-router@*":
|
||||
version "5.1.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.13.tgz#051c0d229bd48ad90558a1db500708127cc512f7"
|
||||
integrity sha512-ZIuaO9Yrln54X6elg8q2Ivp6iK6p4syPsefEYAhRDAoqNh48C8VYUmB9RkXjKSQAJSJV0mbIFCX7I4vZDcHrjg==
|
||||
dependencies:
|
||||
"@types/history" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^17.0.3":
|
||||
version "17.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79"
|
||||
integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==
|
||||
|
|
|
|||
Loading…
Reference in a new issue