├── .firebase └── hosting.YnVpbGQ.cache ├── .firebaserc ├── .gitignore ├── README.md ├── firebase.json ├── package-lock.json ├── package.json ├── public ├── index.html ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── Footer.js ├── Recipe.js ├── Title.js ├── index.css ├── index.js ├── logo.svg ├── recipe.module.css ├── serviceWorker.js └── setupTests.js /.firebase/hosting.YnVpbGQ.cache: -------------------------------------------------------------------------------- 1 | asset-manifest.json,1598478619947,d3d36b331cb52aebbc981452cd1ba6aae56aee29e8f3fcd924afea72e6c4e54c 2 | precache-manifest.5b74c0a374008bcd9d3e826c1bdb7c42.js,1598478619946,b9c4be3f46819f818913f5704cab2c924baa20c9e57f428ff83654e6d9515df9 3 | index.html,1598478619947,a208d77211ceed2b1bb3ffc68f6fff2ab1c650322e730e6ec36086a34a29c0a8 4 | robots.txt,1598478614168,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2 5 | manifest.json,1598478614167,341d52628782f8ac9290bbfc43298afccb47b7cbfcee146ae30cf0f46bc30900 6 | service-worker.js,1598478619947,7604c67439c81b655cff7f072a5d3794ac0356e1c22225f1c49e86eb9f2527e3 7 | static/css/main.2822d827.chunk.css,1598478619947,9242a627e48b9c0abb479a254811b06be07c8acb8ed9fbc4b528602e0439ab25 8 | static/css/main.2822d827.chunk.css.map,1598478619949,61bacc3c202ed37c1dbd4c08a1a28f671c125bf356f037d03277e50f856f5ea4 9 | static/js/2.6d7f5f33.chunk.js.LICENSE.txt,1598478619949,c81fc59355bbb15e39bbf67990f01acc497d5f9c99cd2e8d192d6c868ca3d79f 10 | static/js/runtime-main.786b8bbc.js,1598478619948,e3069d8bba37cff1cc1db4740ca0bf3f897c89f323430b9b87f947c76e118f85 11 | static/js/main.50ab7838.chunk.js.map,1598478619949,33b532ead99c9ddf25339dfb0e4cbb53b3a6d3886cc8c2530870b4e1b262a990 12 | static/js/main.50ab7838.chunk.js,1598478619949,fff078b7e6d235f39124bbe88ed9f7aec19425b698fff057a08d6d2542864284 13 | static/js/runtime-main.786b8bbc.js.map,1598478619949,d99ce36555b5aae3f06fbc48367146e0a366bdd5b6a26e93d158f422598bd3d5 14 | static/css/2.06c478a6.chunk.css,1598478619949,7162a9f695e5fc57345be224648deee64c0476ab6ae41d0694619472a3046d72 15 | static/js/2.6d7f5f33.chunk.js,1598478619949,7cc768b5f090e0a7588a3bdc57043c376825512908f1051e7c193f6db6faa5b9 16 | static/css/2.06c478a6.chunk.css.map,1598478619949,a422292552b609bee971442ffec31f06c1227269ce1f64b9a82cf511bbb62510 17 | static/js/2.6d7f5f33.chunk.js.map,1598478619949,e511714e2789f31246cf8213bd8141577300ae3f8e752d5ac915dfc5a2f683f8 18 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "recipesearchapi" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Recipe Search API https://recipesearchapi.web.app 2 | #### The purpose of this project is to practice React.js and API fetching. 3 | 4 | #### This website uses the Recipe Search API from Edemam.com. This API has the data of tens of thousands of foods, simply search any type of dish you like and it will find you its ingredients. 5 | 6 | ### Familiarized myself with: 7 | > - React.js Hooks (State/Effect) 8 | > - Asynchronous API (async/await) 9 | > - JSX, components, props, css modules, etc. 10 | 11 | (React.js, JavaScript, HTML, CSS/Bootstrap, deployed with Google Firebase) 12 | 13 | [![Image from Gyazo](https://i.gyazo.com/82d7e9ad694fc5cd9ef3351dfa1ca196.gif)](https://gyazo.com/82d7e9ad694fc5cd9ef3351dfa1ca196) 14 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ], 15 | "headers": [ 16 | { 17 | "source": "/service-worker.js", "headers": [{"key": "Cache-Control", "value": "no-cache"}] 18 | } 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recipe-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "react": "^16.13.1", 10 | "react-dom": "^16.13.1", 11 | "react-scripts": "3.4.3" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject" 18 | }, 19 | "eslintConfig": { 20 | "extends": "react-app" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | min-height:100vh; 3 | background-image: linear-gradient(to right, #243949 0%, #517fa4 100%);} 4 | 5 | .search-form{ 6 | min-height: 10vh; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | 12 | .search-bar{ 13 | width: 30%; 14 | padding: 10px; 15 | border: none; 16 | } 17 | 18 | .search-button{ 19 | background: rgb(255, 187, 0); 20 | border:none; 21 | padding: 10px 20px; 22 | color: black; 23 | font-weight: bold; 24 | margin:0; 25 | } 26 | 27 | button:hover { 28 | /* background-color: #EF9B0F; */ 29 | background-color: #8cc152; 30 | color: white; 31 | } 32 | 33 | .recipe{ 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | flex-wrap: wrap; 38 | } 39 | 40 | .title { 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | flex-direction: column; 45 | } 46 | /* .recipe-card{ 47 | border: 2px solid black; 48 | border-radius: 5px; 49 | padding: 5px; 50 | 51 | } */ -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import Recipe from './Recipe'; 3 | import Title from './Title'; 4 | import Footer from './Footer'; 5 | import './App.css'; 6 | 7 | 8 | const App = () => { 9 | 10 | //should use env variables... 11 | const APP_ID = 'd40c2f29'; 12 | const APP_KEY = '4330d7b01907e83778c80814e2bd894c'; 13 | 14 | //If have a counter button, this will log every time you click the button (render), even if page doesn't refresh 15 | //useEffect(something, dependency) --> will run 'something' everytime dependency changes, so put an empty array for no change: only activates on first page load. 16 | //can put counter inside, so every time you click counter button the value changes, and this will log 17 | // useEffect(() => { 18 | // console.log('Effect has been run'); 19 | // }, []) 20 | 21 | //STATES 22 | //setRecipes(data.hits) will store the API data to 'recipes' state here 23 | const [recipes, setRecipes] = useState([]); //array because data.hits is an array (check with inspect element) 24 | const [search, setSearch] = useState(""); //state to store user input for search bar, emtpy to begin with 25 | const [query, setQuery] = useState("croissant"); //state to store the final query when press search 26 | 27 | //EFFECTS 28 | //activates on page load, and everytime [] is updated (never lel) 29 | useEffect( () => { 30 | getRecipes(); 31 | }, [query]); 32 | 33 | //making an asynchronous call, 'await' keyword to await call to complete, although lets caller of function continue processing (asynchronous) 34 | //also setting the state: taking the API data and storing in state 35 | const getRecipes = async () => { 36 | //after the 'search?' the 'q' signifies query: whatever is after it = what we're seraching for. 37 | //JavaScript Template Literals: use back ticks (the button besides number 1) instead of strings: allow interpolation with what's inside ${} 38 | const response = await fetch(`https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`); //change the elements in the query to match ur ID and KEY + query 39 | const data = await response.json(); 40 | console.log(data.hits); //pulling the 'hits' object (array) from the whole json object (it's where it contains the recipes) 41 | setRecipes(data.hits); //setting the hits array in our state above (recipe variable) 42 | } 43 | 44 | //updating 'search' state with the onChange event target value 45 | const updateSearch = (e) => { 46 | setSearch(e.target.value); 47 | console.log(search); 48 | } 49 | 50 | //updating query when click on search 51 | const getSearch = (e) => { 52 | e.preventDefault(); //prevent page refresh when pressing submit 53 | setQuery(search); 54 | setSearch(""); 55 | } 56 | 57 | //changing search button on mouse over and out 58 | const buttonOver = (e) => { 59 | e.target.innerHTML = "Go!"; 60 | } 61 | const buttonOut = (e) => { 62 | e.target.innerHTML = "Search"; 63 | } 64 | 65 | return( 66 |
67 | 68 | <form onSubmit={getSearch} className="search-form"> 69 | <input className="search-bar" type="text" placeholder="Enter your favourite dish, I'm sure we have it! :)" value={search} onChange={updateSearch}/> {/*passes the onChange event to updateSearch, it needs to be here so can retrieve input value (as opposed to button)*/} 70 | <button className="search-button" type="submit" onMouseOver={buttonOver} onMouseOut={buttonOut}>Search</button> 71 | </form> 72 | <div className="recipe"> 73 | {recipes.map(recipe => ( //iterate over the array, use parenthesis to return the JSX. 74 | <Recipe 75 | //passing the state data as prop to render actual item values for this component 76 | key = {recipe.recipe.label} //key attribute to prevent compile error on browser (need unique one) 77 | title={recipe.recipe.label} 78 | calories={recipe.recipe.calories} 79 | image={recipe.recipe.image} 80 | ingredients={recipe.recipe.ingredients} //ingredient is an array 81 | /> 82 | ))}; 83 | </div> 84 | <Footer /> 85 | </div> 86 | ); 87 | } 88 | 89 | export default App; 90 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(<App />); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = () => { 4 | return ( 5 | <div className="text-center py-3 text-warning"> 6 | © {new Date().getFullYear()} Matthew Pan: Thanks for visiting my page :) 7 | </div> 8 | ); 9 | } 10 | 11 | export default Footer; -------------------------------------------------------------------------------- /src/Recipe.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import style from './recipe.module.css'; //to add css module only for this component, will not matter if use same class name 3 | 4 | //essentially, passing data from App component's state to Recipe component through props 5 | const Recipe = ({title, calories, image, ingredients}) => { //destructure props with {} 6 | return( 7 | <div className={style.recipe}> 8 | <h1 className="display-4 p-3">{title}</h1> 9 | <ul> {/*Ingredients is an array of objects, so need to iterate through it with map to display*/} 10 | {ingredients.map(ingredient => ( 11 | <li className="lead">{ingredient.text}</li> 12 | ))} 13 | </ul> 14 | <p>Calories: {Math.floor(calories)}g</p> 15 | <img className={style.image} src={image} alt="Cannot display :("/> 16 | </div> 17 | ); 18 | } 19 | 20 | export default Recipe; -------------------------------------------------------------------------------- /src/Title.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Title = () => { 4 | return( 5 | <div className="title container"> 6 | <div className="border rounded m-3 p-5 shadow bg-warning"> 7 | <h1 className="display-1 p-3">Recipe Search API</h1> 8 | <h3 className="lead">This Edamam recipe API has the data of tens of thousands of foods, including international dishes.<br></br> Enter <strong>ANY</strong> sort of food (e.g.: pasta, chicken enchilada, dumpling, etc.) to see its magic. <span className="spinner-grow spinner-grow-sm"> </span></h3> 9 | </div> 10 | </div> 11 | ); 12 | } 13 | 14 | export default Title; 15 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import 'bootstrap/dist/css/bootstrap.min.css'; 5 | import App from './App'; 6 | import * as serviceWorker from './serviceWorker'; 7 | 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: https://bit.ly/CRA-PWA 18 | serviceWorker.unregister(); 19 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"> 2 | <g fill="#61DAFB"> 3 | <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/> 4 | <circle cx="420.9" cy="296.5" r="45.7"/> 5 | <path d="M520.5 78.1z"/> 6 | </g> 7 | </svg> 8 | -------------------------------------------------------------------------------- /src/recipe.module.css: -------------------------------------------------------------------------------- 1 | .recipe{ 2 | border-radius: 10px; 3 | box-shadow: 0px 5px 20px rgb(71,71,71); 4 | margin: 20px; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: space-evenly; 8 | background: white; 9 | align-items: center; 10 | min-width: 50%; /* makes them in 1 column! all have to be this size minimally */ 11 | } 12 | 13 | .image { 14 | border-radius: 50%; 15 | width: 150px; 16 | height: 150px; 17 | margin-bottom: 7px; 18 | } 19 | 20 | /* to add css module for components, will not matter if use same class name */ -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | --------------------------------------------------------------------------------