├── .gitignore ├── replit.nix ├── tsconfig.node.json ├── vite.config.js ├── .upm └── store.json ├── src ├── index.jsx ├── App.jsx ├── components │ ├── Modal.jsx │ ├── Favorites.jsx │ ├── Search.jsx │ └── Meals.jsx ├── context.jsx └── App.css ├── tsconfig.json ├── index.html ├── package.json ├── .replit ├── vite.config.js.timestamp-1660753608147.mjs ├── public └── favicon.svg ├── replit_zip_error_log.txt ├── .cache └── replit │ └── nix │ └── env.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /replit.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: { 2 | deps = [ 3 | pkgs.nodejs-16_x 4 | pkgs.nodePackages.typescript-language-server 5 | pkgs.yarn 6 | pkgs.replitPackages.jest 7 | ]; 8 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | host: '0.0.0.0', 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /.upm/store.json: -------------------------------------------------------------------------------- 1 | {"version":2,"languages":{"nodejs-npm":{"specfileHash":"d0c7a23bc5a9d0214d2d094c3bc3c7c6","lockfileHash":"bcfa2a4d173057b9d95129036e0e5b3a","guessedImports":["vite","@vitejs/plugin-react","react-icons","react","axios","react-dom"],"guessedImportsHash":"5bff661744821afa84350891b38248c5"}}} 2 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import { AppProvider } from './context' 5 | ReactDOM.createRoot(document.getElementById('root')).render( 6 | 7 | 8 | 9 | 10 | 11 | ) -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useGlobalContext } from './context' 2 | import './App.css' 3 | 4 | 5 | 6 | import Search from './components/Search' 7 | import Meals from './components/Meals' 8 | import Modal from './components/Modal' 9 | import Favorites from './components/Favorites' 10 | export default function App() { 11 | const { showModal, favorites } = useGlobalContext() 12 | 13 | return ( 14 |
15 | 16 | 17 | {favorites.length > 0 && } 18 | 19 | 20 | {showModal && } 21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Meals DB 8 | 9 | 10 |
11 | 12 | 13 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-typescript", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "React TypeScript on Replit, using Vite bundler", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "axios": "^0.27.2", 16 | "react-icons": "^4.4.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.0.15", 20 | "@types/react-dom": "^18.0.6", 21 | "@vitejs/plugin-react": "^2.0.0", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0", 24 | "typescript": "^4.7.4", 25 | "typescript-language-server": "^0.11.2", 26 | "vite": "^3.0.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Modal.jsx: -------------------------------------------------------------------------------- 1 | import { useGlobalContext } from '../context' 2 | 3 | const Modal = () => { 4 | const { selectedMeal, closeModal } = useGlobalContext() 5 | 6 | const { strMealThumb: image, strMeal: title, strInstructions: text, strSource: source } = selectedMeal 7 | return 19 | } 20 | 21 | export default Modal -------------------------------------------------------------------------------- /src/components/Favorites.jsx: -------------------------------------------------------------------------------- 1 | import { useGlobalContext } from '../context' 2 | 3 | 4 | const Favorites = () => { 5 | const { favorites, selectMeal, removeFromFavorites } = useGlobalContext() 6 | 7 | return
8 |
9 |
Favorites
10 |
11 | {favorites.map((item) => { 12 | const { idMeal, strMealThumb: image } = item; 13 | 14 | return
15 | selectMeal(idMeal, true)} /> 16 | 17 |
18 | })} 19 |
20 |
21 |
22 | } 23 | 24 | 25 | export default Favorites -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | run = "npm run dev" 2 | entrypoint = "src/App.jsx" 3 | 4 | hidden = [".config", "tsconfig.json", "tsconfig.node.json", "vite.config.js", ".gitignore"] 5 | 6 | [nix] 7 | channel = "stable-21_11" 8 | 9 | [env] 10 | PATH = "/home/runner/$REPL_SLUG/.config/npm/node_global/bin:/home/runner/$REPL_SLUG/node_modules/.bin" 11 | XDG_CONFIG_HOME = "/home/runner/.config" 12 | npm_config_prefix = "/home/runner/$REPL_SLUG/.config/npm/node_global" 13 | 14 | [gitHubImport] 15 | requiredFiles = [".replit", "replit.nix", ".config"] 16 | 17 | [packager] 18 | language = "nodejs" 19 | 20 | [packager.features] 21 | packageSearch = true 22 | guessImports = true 23 | enabledForHosting = false 24 | 25 | [languages.javascript] 26 | pattern = "**/{*.js,*.jsx,*.ts,*.tsx}" 27 | 28 | [languages.javascript.languageServer] 29 | start = "./node_modules/.bin/typescript-language-server --stdio" 30 | -------------------------------------------------------------------------------- /src/components/Search.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { useGlobalContext } from '../context' 3 | 4 | 5 | 6 | const Search = () => { 7 | const { setSearchTerm, fetchRandomMeal } = useGlobalContext() 8 | const [text, setText] = useState('') 9 | 10 | const handleChange = (e) => { 11 | setText(e.target.value) 12 | } 13 | const handleSubmit = (e) => { 14 | e.preventDefault() 15 | if (text) { 16 | setSearchTerm(text) 17 | } 18 | } 19 | 20 | const handleRandomMeal = () => { 21 | setSearchTerm('') 22 | setText('') 23 | fetchRandomMeal() 24 | } 25 | 26 | return
27 |
28 | 29 | 30 | 31 |
32 |
33 | } 34 | 35 | 36 | export default Search -------------------------------------------------------------------------------- /src/components/Meals.jsx: -------------------------------------------------------------------------------- 1 | import { useGlobalContext } from '../context' 2 | import { BsHandThumbsUp } from 'react-icons/bs' 3 | const Meals = () => { 4 | const { loading, meals, selectMeal, addToFavorites } = useGlobalContext(); 5 | 6 | if (loading) { 7 | return
8 |

Loading...

9 |
10 | } 11 | 12 | if (meals.length < 1) { 13 | return
14 |

No meals matched your search term. Please try again.

15 |
16 | } 17 | 18 | return
19 | {meals.map((singleMeal) => { 20 | const { idMeal, strMeal: title, strMealThumb: image } = singleMeal 21 | return
22 | selectMeal(idMeal)} /> 23 | 27 |
28 | })} 29 |
30 | 31 | } 32 | 33 | export default Meals 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /vite.config.js.timestamp-1660753608147.mjs: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import { defineConfig } from "vite"; 3 | import react from "@vitejs/plugin-react"; 4 | var vite_config_default = defineConfig({ 5 | plugins: [react()], 6 | server: { 7 | host: "0.0.0.0" 8 | } 9 | }); 10 | export { 11 | vite_config_default as default 12 | }; 13 | //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS9ydW5uZXIvbWVhbHMtYXBwbGljYXRpb25cIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9ob21lL3J1bm5lci9tZWFscy1hcHBsaWNhdGlvbi92aXRlLmNvbmZpZy5qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vaG9tZS9ydW5uZXIvbWVhbHMtYXBwbGljYXRpb24vdml0ZS5jb25maWcuanNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJztcbmltcG9ydCByZWFjdCBmcm9tICdAdml0ZWpzL3BsdWdpbi1yZWFjdCc7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbcmVhY3QoKV0sXG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJyxcbiAgfVxufSlcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBNFEsU0FBUyxvQkFBb0I7QUFDelMsT0FBTyxXQUFXO0FBR2xCLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQzFCLFNBQVMsQ0FBQyxNQUFNLENBQUM7QUFBQSxFQUNqQixRQUFRO0FBQUEsSUFDTixNQUFNO0FBQUEsRUFDUjtBQUNGLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg== 14 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/context.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from 'react' 2 | 3 | const AppContext = React.createContext() 4 | 5 | import axios from 'axios' 6 | const allMealsUrl = 'https://www.themealdb.com/api/json/v1/1/search.php?s=' 7 | const randomMealUrl = 'https://www.themealdb.com/api/json/v1/1/random.php' 8 | 9 | 10 | const getFavoritesFromLocalStorage = () => { 11 | let favorites = localStorage.getItem('favorites'); 12 | if (favorites) { 13 | favorites = JSON.parse(localStorage.getItem('favorites')) 14 | } 15 | else { 16 | favorites = [] 17 | } 18 | return favorites 19 | } 20 | 21 | const AppProvider = ({ children }) => { 22 | const [meals, setMeals] = useState([]) 23 | const [loading, setLoading] = useState(false) 24 | const [searchTerm, setSearchTerm] = useState('') 25 | 26 | const [showModal, setShowModal] = useState(false) 27 | const [selectedMeal, setSelectedMeal] = useState(null) 28 | const [favorites, setFavorites] = useState(getFavoritesFromLocalStorage()); 29 | 30 | const fetchMeals = async (url) => { 31 | setLoading(true) 32 | try { 33 | const { data } = await axios.get(url) 34 | if (data.meals) { 35 | setMeals(data.meals) 36 | } 37 | else { 38 | setMeals([]) 39 | } 40 | } 41 | catch (e) { 42 | 43 | console.log(e.response) 44 | } 45 | setLoading(false) 46 | } 47 | 48 | useEffect(() => { 49 | fetchMeals(allMealsUrl) 50 | }, []) 51 | 52 | useEffect(() => { 53 | if (!searchTerm) return 54 | fetchMeals(`${allMealsUrl}${searchTerm}`) 55 | }, [searchTerm]) 56 | 57 | 58 | const fetchRandomMeal = () => { 59 | fetchMeals(randomMealUrl) 60 | } 61 | 62 | const selectMeal = (idMeal, favoriteMeal) => { 63 | let meal; 64 | if (favoriteMeal) { 65 | meal = favorites.find((meal) => meal.idMeal === idMeal); 66 | } else { 67 | meal = meals.find((meal) => meal.idMeal === idMeal); 68 | } 69 | setSelectedMeal(meal); 70 | setShowModal(true) 71 | } 72 | 73 | const closeModal = () => { 74 | setShowModal(false) 75 | } 76 | const addToFavorites = (idMeal) => { 77 | const meal = meals.find((meal) => meal.idMeal === idMeal); 78 | const alreadyFavorite = favorites.find((meal) => meal.idMeal === idMeal); 79 | if (alreadyFavorite) return 80 | const updatedFavorites = [...favorites, meal] 81 | setFavorites(updatedFavorites) 82 | localStorage.setItem("favorites", JSON.stringify(updatedFavorites)) 83 | } 84 | const removeFromFavorites = (idMeal) => { 85 | const updatedFavorites = favorites.filter((meal) => meal.idMeal !== idMeal); 86 | setFavorites(updatedFavorites) 87 | localStorage.setItem("favorites", JSON.stringify(updatedFavorites)) 88 | } 89 | return ( 90 | 93 | {children} 94 | 95 | ) 96 | } 97 | // make sure use 98 | export const useGlobalContext = () => { 99 | return useContext(AppContext) 100 | } 101 | 102 | export { AppContext, AppProvider } -------------------------------------------------------------------------------- /replit_zip_error_log.txt: -------------------------------------------------------------------------------- 1 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/acorn","time":"2023-01-01T12:54:56Z"} 2 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/browserslist","time":"2023-01-01T12:54:56Z"} 3 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/browserslist-lint","time":"2023-01-01T12:54:56Z"} 4 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/esbuild","time":"2023-01-01T12:54:56Z"} 5 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/installServerIntoExtension","time":"2023-01-01T12:54:56Z"} 6 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/jsesc","time":"2023-01-01T12:54:56Z"} 7 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/json5","time":"2023-01-01T12:54:56Z"} 8 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/loose-envify","time":"2023-01-01T12:54:56Z"} 9 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/nanoid","time":"2023-01-01T12:54:56Z"} 10 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/node-which","time":"2023-01-01T12:54:56Z"} 11 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/parser","time":"2023-01-01T12:54:56Z"} 12 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/resolve","time":"2023-01-01T12:54:56Z"} 13 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/rimraf","time":"2023-01-01T12:54:56Z"} 14 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/rollup","time":"2023-01-01T12:54:56Z"} 15 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/semver","time":"2023-01-01T12:54:56Z"} 16 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/terser","time":"2023-01-01T12:54:56Z"} 17 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/tsc","time":"2023-01-01T12:54:56Z"} 18 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/tsserver","time":"2023-01-01T12:54:56Z"} 19 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/typescript-language-server","time":"2023-01-01T12:54:56Z"} 20 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/.bin/vite","time":"2023-01-01T12:54:56Z"} 21 | {"error":".zip archives do not support non-regular files","level":"error","msg":"unable to write file node_modules/typescript-language-server/node_modules/.bin/semver","time":"2023-01-01T12:55:33Z"} 22 | -------------------------------------------------------------------------------- /.cache/replit/nix/env.json: -------------------------------------------------------------------------------- 1 | {"entries":{"replit.nix":{"env":{"AR":"ar","AS":"as","CC":"gcc","CONFIG_SHELL":"/nix/store/bm7jr70d9ghn5cczb3q0w90apsm05p54-bash-5.1-p8/bin/bash","CXX":"g++","HOST_PATH":"/nix/store/ra8r42571xvv1m85wanh1ll9mxmp0mwl-nodejs-16.13.2/bin:/nix/store/rszcnphk27fvfr2hq5pcr07jccf2dqy1-typescript-language-server-0.9.6/bin:/nix/store/ylaybq7lj0yr3w5jcmrba9rhb8nlcflz-yarn-1.22.17/bin:/nix/store/z3f1lqsxb31bzv36wzmjj3qp9gp9ci14-jest-cli-23.6.0/bin:/nix/store/jd1y449cf66yx5d1hwyjvc4562b1p1am-coreutils-9.0/bin:/nix/store/jjvw20r6pz3ff7pn91yhvfx8s7izsqan-findutils-4.8.0/bin:/nix/store/ndd6gh8zigjy0j68arj7nyrbcw4kcw01-diffutils-3.8/bin:/nix/store/bpg0ia8nkavzw7s66avi1f9nz72i1p3r-gnused-4.8/bin:/nix/store/df3ff57sbkgbdhc4ar19zs4y0hrhggii-gnugrep-3.7/bin:/nix/store/4fv981732jqzirah3h2y6ynmlsfbxb37-gawk-5.1.1/bin:/nix/store/k5ifa08z0qlkknnb8s1bdh2kdrx0qmd0-gnutar-1.34/bin:/nix/store/dcird3wn9xywy49w3qq1hpjwvjfn3lph-gzip-1.11/bin:/nix/store/s85iyc3p9nbinwvwx9rcqirf1m68zpmm-bzip2-1.0.6.0.2-bin/bin:/nix/store/msncxcf5lgy5by9cqjyxchxd26x47d64-gnumake-4.3/bin:/nix/store/bm7jr70d9ghn5cczb3q0w90apsm05p54-bash-5.1-p8/bin:/nix/store/gi3psbgxbf2fmvgi36pmw77nnh58vq3l-patch-2.7.6/bin:/nix/store/sqb20f4rk80lw21j4is85qwljlphmn3g-xz-5.2.5-bin/bin","LD":"ld","LOCALE_ARCHIVE":"/usr/lib/locale/locale-archive","NIX_BINTOOLS":"/nix/store/spm7d6ncyx2k5w8yl6clzynv2s4wf1kp-binutils-wrapper-2.35.2","NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu":"1","NIX_BUILD_CORES":"4","NIX_BUILD_TOP":"/tmp","NIX_CC":"/nix/store/2qwnn6lizc9d119kp3zig3q19gbfm4n6-gcc-wrapper-10.3.0","NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu":"1","NIX_CFLAGS_COMPILE":" -frandom-seed=j8j1jdaqc6 -isystem /nix/store/ra8r42571xvv1m85wanh1ll9mxmp0mwl-nodejs-16.13.2/include -isystem /nix/store/ra8r42571xvv1m85wanh1ll9mxmp0mwl-nodejs-16.13.2/include","NIX_ENFORCE_NO_NATIVE":"1","NIX_HARDENING_ENABLE":"fortify stackprotector pic strictoverflow format relro bindnow","NIX_INDENT_MAKE":"1","NIX_LDFLAGS":"-rpath /nix/store/j8j1jdaqc6lfg77vpxp2sx4n4lbvfz25-nix-shell/lib64 -rpath /nix/store/j8j1jdaqc6lfg77vpxp2sx4n4lbvfz25-nix-shell/lib ","NIX_STORE":"/nix/store","NM":"nm","NODE_PATH":"/nix/store/ra8r42571xvv1m85wanh1ll9mxmp0mwl-nodejs-16.13.2/lib/node_modules:/nix/store/rszcnphk27fvfr2hq5pcr07jccf2dqy1-typescript-language-server-0.9.6/lib/node_modules:/nix/store/z3f1lqsxb31bzv36wzmjj3qp9gp9ci14-jest-cli-23.6.0/lib/node_modules","OBJCOPY":"objcopy","OBJDUMP":"objdump","PATH":"/nix/store/bccsypsvjskpzsgzww8bzjgqmck4bjbf-bash-interactive-5.1-p8/bin:/nix/store/bqmg7l0jn6nhgjlfc981g1imzb6ny8xl-patchelf-0.13/bin:/nix/store/2qwnn6lizc9d119kp3zig3q19gbfm4n6-gcc-wrapper-10.3.0/bin:/nix/store/6r5h4h7nqx73m87j5b9sjwy2x9kyri0k-gcc-10.3.0/bin:/nix/store/csz8v8xi2f644j26n84i20dnqmq43sih-glibc-2.33-117-bin/bin:/nix/store/jd1y449cf66yx5d1hwyjvc4562b1p1am-coreutils-9.0/bin:/nix/store/spm7d6ncyx2k5w8yl6clzynv2s4wf1kp-binutils-wrapper-2.35.2/bin:/nix/store/h19zwlkrp6b0hp3ypbqdcggnyarn3af6-binutils-2.35.2/bin:/nix/store/ra8r42571xvv1m85wanh1ll9mxmp0mwl-nodejs-16.13.2/bin:/nix/store/rszcnphk27fvfr2hq5pcr07jccf2dqy1-typescript-language-server-0.9.6/bin:/nix/store/ylaybq7lj0yr3w5jcmrba9rhb8nlcflz-yarn-1.22.17/bin:/nix/store/z3f1lqsxb31bzv36wzmjj3qp9gp9ci14-jest-cli-23.6.0/bin:/nix/store/jd1y449cf66yx5d1hwyjvc4562b1p1am-coreutils-9.0/bin:/nix/store/jjvw20r6pz3ff7pn91yhvfx8s7izsqan-findutils-4.8.0/bin:/nix/store/ndd6gh8zigjy0j68arj7nyrbcw4kcw01-diffutils-3.8/bin:/nix/store/bpg0ia8nkavzw7s66avi1f9nz72i1p3r-gnused-4.8/bin:/nix/store/df3ff57sbkgbdhc4ar19zs4y0hrhggii-gnugrep-3.7/bin:/nix/store/4fv981732jqzirah3h2y6ynmlsfbxb37-gawk-5.1.1/bin:/nix/store/k5ifa08z0qlkknnb8s1bdh2kdrx0qmd0-gnutar-1.34/bin:/nix/store/dcird3wn9xywy49w3qq1hpjwvjfn3lph-gzip-1.11/bin:/nix/store/s85iyc3p9nbinwvwx9rcqirf1m68zpmm-bzip2-1.0.6.0.2-bin/bin:/nix/store/msncxcf5lgy5by9cqjyxchxd26x47d64-gnumake-4.3/bin:/nix/store/bm7jr70d9ghn5cczb3q0w90apsm05p54-bash-5.1-p8/bin:/nix/store/gi3psbgxbf2fmvgi36pmw77nnh58vq3l-patch-2.7.6/bin:/nix/store/sqb20f4rk80lw21j4is85qwljlphmn3g-xz-5.2.5-bin/bin","RANLIB":"ranlib","READELF":"readelf","SIZE":"size","SOURCE_DATE_EPOCH":"315532800","STRINGS":"strings","STRIP":"strip","XDG_DATA_DIRS":"/nix/store/bqmg7l0jn6nhgjlfc981g1imzb6ny8xl-patchelf-0.13/share","_":"/nix/store/jd1y449cf66yx5d1hwyjvc4562b1p1am-coreutils-9.0/bin/env","__ETC_PROFILE_SOURCED":"1","buildInputs":"/nix/store/ra8r42571xvv1m85wanh1ll9mxmp0mwl-nodejs-16.13.2 /nix/store/rszcnphk27fvfr2hq5pcr07jccf2dqy1-typescript-language-server-0.9.6 /nix/store/ylaybq7lj0yr3w5jcmrba9rhb8nlcflz-yarn-1.22.17 /nix/store/z3f1lqsxb31bzv36wzmjj3qp9gp9ci14-jest-cli-23.6.0","builder":"/nix/store/bm7jr70d9ghn5cczb3q0w90apsm05p54-bash-5.1-p8/bin/bash","configureFlags":"","depsBuildBuild":"","depsBuildBuildPropagated":"","depsBuildTarget":"","depsBuildTargetPropagated":"","depsHostHost":"","depsHostHostPropagated":"","depsTargetTarget":"","depsTargetTargetPropagated":"","doCheck":"","doInstallCheck":"","name":"nix-shell","nativeBuildInputs":"","nobuildPhase":"echo\necho \"This derivation is not meant to be built, aborting\";\necho\nexit 1\n","out":"/nix/store/j8j1jdaqc6lfg77vpxp2sx4n4lbvfz25-nix-shell","outputs":"out","patches":"","phases":"nobuildPhase","propagatedBuildInputs":"","propagatedNativeBuildInputs":"","shell":"/nix/store/bm7jr70d9ghn5cczb3q0w90apsm05p54-bash-5.1-p8/bin/bash","shellHook":"","stdenv":"/nix/store/z7wz8q1i0j4jmfpn87wpakwma6q0k90n-stdenv-linux","strictDeps":"","system":"x86_64-linux"},"dependencies":[{"path":"replit.nix","mod_time":"2022-08-04T02:07:05.20542395Z"}],"channel":"stable-21_11","channel_nix_path":"/nix/store/ki2jvm59r70z94nhzxa1hqsn2amr5n62-nixpkgs-stable-21_11-21.11.tar.gz/nixpkgs-stable-21_11"}}} -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | *, 2 | ::after, 3 | ::before { 4 | box-sizing: border-box; 5 | margin:0; 6 | padding:0; 7 | } 8 | 9 | html { 10 | font-size: 100%; 11 | } /*16px*/ 12 | 13 | :root { 14 | /* colors */ 15 | --primary-100: #e6f0ff; 16 | --primary-200: #b4d3fe; 17 | --primary-300: #82b6fd; 18 | --primary-400: #5098fc; 19 | --primary-500: #03449d; 20 | --primary-600: #034caf; 21 | --primary-700: #02367d; 22 | --primary-800: #01214b; 23 | --primary-900: #000b19; 24 | 25 | /* grey */ 26 | --grey-50: #f8fafc; 27 | --grey-100: #f1f5f9; 28 | --grey-200: #e2e8f0; 29 | --grey-300: #cbd5e1; 30 | --grey-400: #94a3b8; 31 | --grey-500: #64748b; 32 | --grey-600: #475569; 33 | --grey-700: #334155; 34 | --grey-800: #1e293b; 35 | --grey-900: #0f172a; 36 | /* rest of the colors */ 37 | --black: #222; 38 | --white: #fff; 39 | --red-light: #f8d7da; 40 | --red-dark: #842029; 41 | --green-light: #d1e7dd; 42 | --green-dark: #0f5132; 43 | 44 | /* fonts */ 45 | --small-text: 0.875rem; 46 | --extra-small-text: 0.7em; 47 | /* rest of the vars */ 48 | /* --backgroundColor: var(--white); */ 49 | --backgroundColor: var(--grey-100); 50 | --textColor: var(--grey-900); 51 | --borderRadius: 0.25rem; 52 | --letterSpacing: 1px; 53 | --transition: 0.3s ease-in-out all; 54 | --max-width: 1120px; 55 | --fixed-width: 600px; 56 | --view-width: 90vw; 57 | /* box shadow*/ 58 | --shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 59 | --shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 60 | 0 2px 4px -1px rgba(0, 0, 0, 0.06); 61 | --shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 62 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 63 | --shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 64 | 0 10px 10px -5px rgba(0, 0, 0, 0.04); 65 | } 66 | 67 | body { 68 | background: var(--backgroundColor); 69 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 70 | Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, 71 | Segoe UI Emoji, Segoe UI Symbol; 72 | font-weight: 400; 73 | line-height: 1.75; 74 | color: var(--textColor); 75 | } 76 | 77 | p { 78 | margin-bottom: 1.5rem; 79 | max-width: 40em; 80 | } 81 | 82 | h1, 83 | h2, 84 | h3, 85 | h4, 86 | h5 { 87 | margin: 0; 88 | margin-bottom: 1.38rem; 89 | font-weight: 400; 90 | line-height: 1.3; 91 | text-transform: capitalize; 92 | letter-spacing: var(--letterSpacing); 93 | } 94 | 95 | h1 { 96 | margin-top: 0; 97 | font-size: 3.052rem; 98 | } 99 | 100 | h2 { 101 | font-size: 2.441rem; 102 | } 103 | 104 | h3 { 105 | font-size: 1.953rem; 106 | } 107 | 108 | h4 { 109 | font-size: 1.563rem; 110 | } 111 | 112 | h5 { 113 | font-size: 1.25rem; 114 | } 115 | 116 | small, 117 | .text-small { 118 | font-size: var(--small-text); 119 | } 120 | 121 | a { 122 | text-decoration: none; 123 | } 124 | ul { 125 | list-style-type: none; 126 | padding: 0; 127 | } 128 | 129 | .img { 130 | width: 100%; 131 | display: block; 132 | object-fit: cover; 133 | } 134 | /* buttons */ 135 | 136 | .btn { 137 | cursor: pointer; 138 | color: var(--white); 139 | background: var(--primary-500); 140 | border: transparent; 141 | border-radius: var(--borderRadius); 142 | letter-spacing: var(--letterSpacing); 143 | padding: 0.375rem 0.75rem; 144 | box-shadow: var(--shadow-1); 145 | transition: var(--transition); 146 | text-transform: capitalize; 147 | display: inline-block; 148 | } 149 | .btn:hover { 150 | background: var(--primary-700); 151 | box-shadow: var(--shadow-3); 152 | } 153 | .btn-hipster { 154 | color: var(--primary-500); 155 | background: var(--primary-200); 156 | } 157 | .btn-hipster:hover { 158 | color: var(--white); 159 | background: var(--black); 160 | } 161 | .btn-block { 162 | width: 100%; 163 | } 164 | /* End Of Global Styles */ 165 | /* End Of Global Styles */ 166 | /* End Of Global Styles */ 167 | 168 | /* Search */ 169 | .search-container{ 170 | height:5rem; 171 | background:var(--white); 172 | display:flex; 173 | align-items:center; 174 | justify-content:center; 175 | } 176 | .search-container form{ 177 | width:var(--view-width); 178 | max-width:var(--max-width); 179 | display:flex; 180 | gap:0.5rem; 181 | flex-wrap:wrap; 182 | } 183 | .search-container .form-input{ 184 | max-width:200px; 185 | padding:0.375rem 0.75rem; 186 | border-radius:var(--borderRadius); 187 | background:var(--backgroundColor); 188 | border:1px solid var(--grey-200); 189 | } 190 | ::placeholder{ 191 | font-family:inherit; 192 | color:var(--grey-400); 193 | } 194 | .search-container .btn{ 195 | font-size:0.75rem; 196 | } 197 | /* Favorites */ 198 | .favorites{ 199 | background:var(--black); 200 | color:var(--white); 201 | padding:1rem 0; 202 | } 203 | .favorites-content{ 204 | width:var(--view-width); 205 | max-width:var(--max-width); 206 | margin:0 auto; 207 | } 208 | .favorites-container{ 209 | display:flex; 210 | gap:0.5rem; 211 | flex-wrap:wrap; 212 | } 213 | .favorite-item{ 214 | text-align:center; 215 | } 216 | .favorites-img{ 217 | width:60px; 218 | border-radius:50%; 219 | border:5px solid var(--white); 220 | cursor:pointer; 221 | } 222 | .remove-btn{ 223 | margin-top:0.25rem; 224 | background:transparent; 225 | color:var(--white); 226 | border:transparent; 227 | cursor:pointer; 228 | font-size:0.5rem; 229 | transition:var(--transition); 230 | } 231 | .remove-btn:hover{ 232 | color:var(--red-dark) 233 | } 234 | /* Meals */ 235 | .section,.section-center{ 236 | padding:3rem 0; 237 | width:var(--view-width); 238 | max-width:var(--max-width); 239 | margin:0 auto; 240 | } 241 | 242 | .section-center{ 243 | display:grid; 244 | gap:2rem; 245 | } 246 | @media screen and (min-width:776px){ 247 | .section-center{ 248 | grid-template-columns:1fr 1fr; 249 | } 250 | } 251 | @media screen and (min-width:992px){ 252 | .section-center{ 253 | grid-template-columns:1fr 1fr 1fr; 254 | } 255 | } 256 | .single-meal{ 257 | background:var(--white); 258 | color:var(--textColor); 259 | border-radius:var(--borderRadius); 260 | box-shadow:var(--shadow-2); 261 | transition:var(--transition); 262 | } 263 | .single-meal:hover{ 264 | box-shadow:var(--shadow-4); 265 | } 266 | .single-meal img{ 267 | height:15rem; 268 | border-top-left-radius:var(--borderRadius); 269 | border-top-right-radius:var(--borderRadius); 270 | cursor:pointer; 271 | } 272 | .single-meal h5{ 273 | padding:0; 274 | margin:0; 275 | } 276 | .single-meal footer{ 277 | padding:1rem 1.5rem; 278 | display:flex; 279 | align-items:center; 280 | justify-content: space-between; 281 | flex-wrap:wrap; 282 | } 283 | .like-btn{ 284 | background:transparent; 285 | border:transparent; 286 | font-size:1.5rem; 287 | cursor:pointer; 288 | transition:var(--transition); 289 | } 290 | .like-btn:hover{ 291 | color:var(--red-dark); 292 | transform:translateY(-2px) 293 | } 294 | /* Modal */ 295 | 296 | .modal-overlay{ 297 | position:fixed; 298 | top:0; 299 | left:0; 300 | width:100%; 301 | height:100%; 302 | background:rgba(0,0,0,0.85); 303 | display:grid; 304 | place-items:center; 305 | transition:var(--transition); 306 | z-index:100; 307 | } 308 | .modal-container{ 309 | width:80vw; 310 | max-width:800px; 311 | height:80vh; 312 | overflow:scroll; 313 | background:var(--white); 314 | border-radius:var(--borderRadius); 315 | } 316 | .modal-img{ 317 | height:15rem; 318 | border-top-left-radius:var(--borderRadius); 319 | border-top-right-radius:var(--borderRadius); 320 | } 321 | .modal-content{ 322 | padding:1rem 1.5rem; 323 | } 324 | .modal-content p{ 325 | color:var(--grey-600); 326 | } 327 | .modal-content a{ 328 | display:block; 329 | color:var(--primary-500); 330 | margin-bottom:1rem; 331 | text-decoration:underline; 332 | transition:var(--transition); 333 | } 334 | .modal-content a:hover{ 335 | color:var(--black) 336 | } 337 | .close-btn{ 338 | background:var(--red-dark); 339 | color:var(--white); 340 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meals Application 2 | 3 | #### Create A New Project in REPLIT 4 | 5 | - login/register 6 | - in the dashboard click "create" 7 | - find react template 8 | - click run 9 | - change title in index.html 10 | 11 | 12 | #### Get Assets 13 | 14 | - copy styles from /src/App.css 15 | - copy README.md 16 | 17 | #### Global Styles Info 18 | 19 | #### Setup Structure 20 | 21 | - create /src/components 22 | - Favorites.jsx, Meals.jsx, Modal.jsx, Search.jsx 23 | - create component (arrow function) 24 | - setup basic return (component name) 25 | - or my personal favorite "shake and bake" 26 | - export default 27 | 28 | 29 | ```js 30 | const Search = () => { 31 | return

Dude, where is my car

32 | } 33 | export default Search 34 | ``` 35 | 36 | - import all of them in App.js 37 | - setup following structure 38 | 39 | ```js 40 | 41 | import './App.css' 42 | 43 | 44 | 45 | import Search from './components/Search' 46 | import Meals from './components/Meals' 47 | import Modal from './components/Modal' 48 | import Favorites from './components/Favorites' 49 | 50 | export default function App() { 51 | 52 | return ( 53 |
54 | 55 | 56 | 57 | 58 |
59 | ) 60 | } 61 | 62 | ``` 63 | - comment out Search, Favorites, Modal 64 | 65 | ```js 66 | export default function App() { 67 | 68 | return ( 69 |
70 | {/* */} 71 | {/* */} 72 | 73 | {/* */} 74 |
75 | ) 76 | } 77 | 78 | ``` 79 | 80 | #### App Level State 81 | 82 | - in App.js 83 | - Context API 84 | - 3rd Party State Management Library 85 | - Redux, Redux-Toolkit,....... 86 | 87 | 88 | #### Context API 89 | 90 | ##### Provider 91 | 92 | [Context API](https://www.freecodecamp.org/news/using-the-react-context-api-getting-started-2018/) 93 | 94 | - create context.js in the root 95 | 96 | 97 | context.jsx 98 | ```js 99 | 100 | import React, {useContext} from 'react' 101 | 102 | const AppContext = React.createContext() 103 | 104 | 105 | const AppProvider = ({ children }) => { 106 | 107 | return ( 108 | 110 | {children} 111 | 112 | ) 113 | } 114 | 115 | export { AppContext, AppProvider } 116 | ``` 117 | index.jsx 118 | ```js 119 | import React from 'react' 120 | import ReactDOM from 'react-dom/client' 121 | import App from './App' 122 | import { AppProvider } from './context' 123 | ReactDOM.createRoot(document.getElementById('root')).render( 124 | 125 | 126 | 127 | 128 | 129 | ) 130 | ``` 131 | 132 | #### Consume Data 133 | 134 | 135 | /components/Meals.jsx 136 | 137 | ```js 138 | import {useContext} from 'react' 139 | import {AppContext} from '../context' 140 | const Meals = () => { 141 | const context = useContext(AppContext); 142 | console.log(context) 143 | return

Meals Component

144 | } 145 | 146 | export default Meals 147 | 148 | ``` 149 | 150 | #### Custom Hook 151 | 152 | context.jsx 153 | ```js 154 | export const useGlobalContext = () => { 155 | return useContext(AppContext) 156 | } 157 | ``` 158 | 159 | 160 | ```js 161 | import {useGlobalContext} from '../context' 162 | const Meals = () => { 163 | const context = useGlobalContext() 164 | console.log(context) 165 | return

Meals Component

166 | } 167 | 168 | export default Meals 169 | 170 | ``` 171 | 172 | #### Data Fetching 173 | 174 | - where and how 175 | 176 | context.jsx 177 | 178 | ```js 179 | import React, { useContext,useEffect } from 'react' 180 | 181 | const AppContext = React.createContext() 182 | 183 | 184 | const AppProvider = ({ children }) => { 185 | 186 | useEffect(()=>{ 187 | console.log('fetch data here') 188 | },[]) 189 | 190 | 191 | 192 | return 193 | {children} 194 | 195 | } 196 | ``` 197 | 198 | - fetch data (fetch api or axios), from any url in useEffect cb 199 | - log result 200 | 201 | #### Fetch API 202 | 203 | - [Fetch API](https://www.freecodecamp.org/news/how-to-make-api-calls-with-fetch/) 204 | - [random user](https://randomuser.me/api/) 205 | 206 | context.jsx 207 | ```jsx 208 | const AppProvider = ({ children }) => { 209 | useEffect(()=>{ 210 | const fetchData = async() =>{ 211 | try { 212 | const response = await fetch('https://randomuser.me/api/') 213 | const data = await response.json(); 214 | console.log(data) 215 | } catch (error) { 216 | console.log(error) 217 | } 218 | } 219 | fetchData() 220 | },[]) 221 | ``` 222 | 223 | 224 | #### Meals DB 225 | 226 | - utilize search engine "meals db", follow the link 227 | - [Meals DB](https://www.themealdb.com/api.php) 228 | - get familiar with docs 229 | - get two url's 230 | - Search meal by name 231 | - Lookup a single random meal 232 | - (hint the "https://" is missing) 233 | - setup two variables in context.jsx 234 | - (allMealsUrl, randomMealUrl) and assign the corresponding values 235 | 236 | #### Get Meals By Name (with axios) 237 | 238 | [Axios](https://www.freecodecamp.org/news/how-to-use-axios-with-react/) 239 | 240 | - install axios 241 | - import in context.jsx 242 | - refactor fetchData 243 | - change name 244 | - switch to axios 245 | - add url parameter 246 | - switch to allMealsUrl 247 | - log response 248 | 249 | 250 | context.jsx 251 | ```jsx 252 | import React, { useState, useContext, useEffect } from 'react' 253 | 254 | const AppContext = React.createContext() 255 | 256 | import axios from 'axios' 257 | const allMealsUrl = 'https://www.themealdb.com/api/json/v1/1/search.php?s=' 258 | const randomMealUrl = 'https://www.themealdb.com/api/json/v1/1/random.php' 259 | 260 | 261 | 262 | 263 | const AppProvider = ({ children }) => { 264 | 265 | const fetchMeals = async (url) => { 266 | 267 | try { 268 | const response = await axios(url) 269 | console.log(response) 270 | 271 | } 272 | catch (e) { 273 | console.log(e.response) 274 | } 275 | 276 | } 277 | 278 | 279 | useEffect(() => { 280 | fetchMeals(allMealsUrl) 281 | }, []) 282 | 283 | 284 | ``` 285 | 286 | #### State Variable (meals) and render 287 | 288 | - import useState hook 289 | - setup state variable (meals) 290 | - set it equal to the meals from api (setMeals) 291 | - pass it down to entire app (value prop) 292 | - destructure meals in the Meals component 293 | - iterate over meals 294 | - log each meal 295 | - render something (anything) on the screen 296 | 297 | 298 | 299 | ```jsx 300 | import React, { useState, useContext, useEffect } from 'react' 301 | 302 | const AppProvider = ({ children }) => { 303 | const [meals, setMeals] = useState([]) 304 | 305 | const fetchMeals = async (url) => { 306 | 307 | try { 308 | const { data } = await axios.get(url) 309 | setMeals(data.meals) 310 | } 311 | catch (e) { 312 | 313 | console.log(e.response) 314 | } 315 | 316 | } 317 | 318 | 319 | useEffect(() => { 320 | 321 | fetchMeals(allMealsUrl) 322 | 323 | 324 | }, []) 325 | 326 | return ( 327 | 330 | {children} 331 | 332 | ) 333 | } 334 | 335 | ``` 336 | 337 | 338 | /components/Meals.jsx 339 | ```js 340 | import { useGlobalContext } from '../context' 341 | 342 | const Meals = () => { 343 | const { meals } = useGlobalContext(); 344 | 345 | 346 | 347 | 348 | return
349 | {meals.map((singleMeal) => { 350 | console.log(singleMeal) 351 | return

single meal

352 | 353 | })} 354 |
355 | 356 | } 357 | 358 | export default Meals 359 | ``` 360 | 361 | #### Meals Component - Display Card 362 | 363 | /components/Meals.jsx 364 | ```js 365 | import { useGlobalContext } from '../context' 366 | 367 | const Meals = () => { 368 | const { meals } = useGlobalContext(); 369 | 370 | 371 | return
372 | {meals.map((singleMeal) => { 373 | const { idMeal, strMeal: title, strMealThumb: image } = singleMeal 374 | return
375 | 376 | 380 |
381 | })} 382 |
383 | 384 | } 385 | 386 | export default Meals 387 | ``` 388 | 389 | #### Meals CSS 390 | 391 | #### React Icons 392 | 393 | [React Icons](https://react-icons.github.io/react-icons/) 394 | - install 395 | - import 396 | - set icon in like button 397 | 398 | #### Infinite Loop 399 | 400 | - Feel free to just watch 401 | 1. initial render (we invoke useEffect) 402 | 2. inside useEffect cb, we fetch data and change value for meals 403 | 3. it triggers re-render 404 | 4. we repeat steps 2 and 3 405 | 406 | 407 | 408 | ##### Loading 409 | 410 | - setup state variable "loading", with default value false 411 | - set loading to true as a first thing in fetchMeals 412 | - set loading to false as a last thing in fetchMeals 413 | - add loading to value prop (pass it down) 414 | - in Meals.jsx set condition for loading 415 | - it needs to be before current return 416 | - return

Loading...

if loading is true 417 | 418 | context.jsx 419 | ```js 420 | const AppProvider = ({ children }) => { 421 | const [meals, setMeals] = useState([]) 422 | const [loading, setLoading] = useState(false) 423 | 424 | 425 | const fetchMeals = async (url) => { 426 | setLoading(true) 427 | try { 428 | const { data } = await axios.get(url) 429 | setMeals(data.meals) 430 | } 431 | catch (e) { 432 | 433 | console.log(e.response) 434 | } 435 | setLoading(false) 436 | } 437 | return ( 438 | 441 | {children} 442 | 443 | ) 444 | } 445 | 446 | ``` 447 | /components/Meals.jsx 448 | ```js 449 | import { useGlobalContext } from '../context' 450 | import { BsHandThumbsUp } from 'react-icons/bs' 451 | const Meals = () => { 452 | const { loading, meals } = useGlobalContext(); 453 | 454 | if (loading) { 455 | return
456 |

Loading...

457 |
458 | } 459 | } 460 | ``` 461 | 462 | ##### No items 463 | 464 | - in fetchMeals check if data.meals is truthy 465 | - returns true 466 | - basically has some value 467 | - only if data.meals has items set it as meals state value 468 | - otherwise set meals variable as empty array 469 | - in Meals.jsx check if meals length is less than 1 470 | - if that's the case return

No items

471 | - place it between loading and current return (cards) 472 | context.jsx 473 | ```js 474 | const AppProvider = ({ children }) => { 475 | const [meals, setMeals] = useState([]) 476 | const [loading, setLoading] = useState(false) 477 | 478 | 479 | const fetchMeals = async (url) => { 480 | setLoading(true) 481 | try { 482 | const { data } = await axios.get(url) 483 | if (data.meals) { 484 | setMeals(data.meals) 485 | } 486 | else { 487 | setMeals([]) 488 | } 489 | } 490 | catch (e) { 491 | 492 | console.log(e.response) 493 | } 494 | setLoading(false) 495 | } 496 | return ( 497 | 500 | {children} 501 | 502 | ) 503 | } 504 | 505 | ``` 506 | /components/Meals.jsx 507 | ```js 508 | import { useGlobalContext } from '../context' 509 | import { BsHandThumbsUp } from 'react-icons/bs' 510 | const Meals = () => { 511 | const { loading, meals } = useGlobalContext(); 512 | 513 | if (loading) { 514 | return
515 |

Loading...

516 |
517 | } 518 | 519 | if (meals.length < 1) { 520 | return
521 |

No meals matched your search term. Please try again.

522 |
523 | } 524 | } 525 | ``` 526 | 527 | #### Search Component - Structure 528 | 529 | - in Search.jsx 530 | - import useState and useGlobalContext 531 | - setup return 532 | - header.search-container 533 | - form 534 | - input.form-input type="text" 535 | - button.btn type="submit" 536 | - button.btn.btn-hipster type="button" 537 | - in App.jsx display Search Component 538 | 539 | /components/Search.jsx 540 | ```js 541 | 542 | import { useState } from 'react' 543 | import {useGlobalContext} from '../context' 544 | 545 | 546 | 547 | const Search = () => { 548 | 549 | return
550 |
551 | 552 | 553 | 554 |
555 |
556 | } 557 | 558 | 559 | export default Search 560 | ``` 561 | 562 | #### Search Component - CSS 563 | 564 | #### HandleChange and Handle Submit 565 | - create "text" state variable 566 | - create two functions handleChange and handleSubmit 567 | - in the handleChange, grab e.target.value and set as text value 568 | - add onChange to input and set it equal to handleChange 569 | - in the handleSubmit set e.preventDefault() 570 | - add onSubmit to form element and set it equal to handleSubmit 571 | 572 | Search.jsx 573 | ```js 574 | 575 | import { useState } from 'react' 576 | import {useGlobalContext} from '../context' 577 | 578 | 579 | 580 | const Search = () => { 581 | 582 | const [text, setText] = useState('') 583 | 584 | const handleChange = (e) => { 585 | setText(e.target.value) 586 | } 587 | const handleSubmit = (e) => { 588 | e.preventDefault() 589 | 590 | } 591 | 592 | return
593 |
594 | 595 | 596 | 597 |
598 |
599 | } 600 | 601 | 602 | export default Search 603 | ``` 604 | 605 | #### Search Term 606 | 607 | - in context.jsx create new state variable "searchTerm" with default value '' 608 | - combine allMealsUrl with searchTerm and pass in the fetchMeals 609 | - add searchTerm to useEffect's dependency array 610 | - add setSearchTerm to value prop (pass it down) 611 | - grab setSearchTerm in Search.jsx 612 | - in the handleSubmit check setup a condition 613 | - if the "text" has a value set it equal to "searchTerm" 614 | 615 | context.jsx 616 | ```js 617 | const AppProvider = ({ children }) => { 618 | const [meals, setMeals] = useState([]) 619 | const [loading, setLoading] = useState(false) 620 | const [searchTerm, setSearchTerm] = useState('') 621 | 622 | 623 | const fetchMeals = async (url) => { 624 | setLoading(true) 625 | try { 626 | const { data } = await axios.get(url) 627 | if (data.meals) { 628 | setMeals(data.meals) 629 | } 630 | else { 631 | setMeals([]) 632 | } 633 | } 634 | catch (e) { 635 | 636 | console.log(e.response) 637 | } 638 | setLoading(false) 639 | } 640 | 641 | 642 | useEffect(() => { 643 | fetchMeals(`${allMealsUrl}${searchTerm}`) 644 | }, [searchTerm]) 645 | 646 | 647 | return ( 648 | 651 | {children} 652 | 653 | ) 654 | } 655 | ``` 656 | /components/Search.jsx 657 | ```js 658 | import { useState } from 'react' 659 | import {useGlobalContext} from '../context' 660 | 661 | 662 | 663 | const Search = () => { 664 | const { setSearchTerm } = useGlobalContext() 665 | const [text, setText] = useState('') 666 | 667 | const handleChange = (e) => { 668 | setText(e.target.value) 669 | } 670 | const handleSubmit = (e) => { 671 | e.preventDefault() 672 | if (text) { 673 | setSearchTerm(text) 674 | 675 | } 676 | } 677 | 678 | return
679 |
680 | 681 | 682 | 683 |
684 |
685 | } 686 | 687 | 688 | export default Search 689 | ``` 690 | 691 | #### Fetch Random Meal 692 | 693 | 694 | 695 | context.jsx 696 | ```js 697 | const AppProvider = ({ children }) => { 698 | 699 | 700 | const fetchRandomMeal = () => { 701 | fetchMeals(randomMealUrl) 702 | } 703 | 704 | 705 | return ( 706 | 709 | {children} 710 | 711 | ) 712 | } 713 | ``` 714 | 715 | /components/Search.jsx 716 | 717 | ```js 718 | import { useState } from 'react' 719 | import {useGlobalContext} from '../context' 720 | 721 | 722 | 723 | const Search = () => { 724 | const { setSearchTerm, fetchRandomMeal } = useGlobalContext() 725 | const [text, setText] = useState('') 726 | 727 | const handleChange = (e) => { 728 | setText(e.target.value) 729 | } 730 | const handleSubmit = (e) => { 731 | e.preventDefault() 732 | if (text) { 733 | setSearchTerm(text) 734 | 735 | } 736 | } 737 | 738 | return
739 |
740 | 741 | 742 | 743 |
744 |
745 | } 746 | 747 | 748 | export default Search 749 | ``` 750 | 751 | 752 | #### Fix Bugs 753 | /components/Search.jsx 754 | ```js 755 | import { useState } from 'react' 756 | import { useGlobalContext } from '../context' 757 | 758 | 759 | 760 | const Search = () => { 761 | const { setSearchTerm, fetchRandomMeal } = useGlobalContext() 762 | const [text, setText] = useState('') 763 | 764 | const handleChange = (e) => { 765 | setText(e.target.value) 766 | } 767 | const handleSubmit = (e) => { 768 | e.preventDefault() 769 | if (text) { 770 | setSearchTerm(text) 771 | } 772 | } 773 | 774 | const handleRandomMeal = () => { 775 | setSearchTerm('') 776 | setText('') 777 | fetchRandomMeal() 778 | } 779 | 780 | return
781 |
782 | 783 | 784 | 785 |
786 |
787 | } 788 | 789 | 790 | export default Search 791 | 792 | ``` 793 | 794 | 795 | 796 | context.jsx 797 | ```js 798 | const AppProvider = ({ children }) => { 799 | 800 | 801 | 802 | useEffect(() => { 803 | fetchMeals(allMealsUrl) 804 | }, []) 805 | 806 | useEffect(() => { 807 | if (!searchTerm) return 808 | fetchMeals(`${allMealsUrl}${searchTerm}`) 809 | }, [searchTerm]) 810 | 811 | 812 | 813 | return ( 814 | 817 | {children} 818 | 819 | ) 820 | } 821 | ``` 822 | 823 | #### Modal - Setup 824 | 825 | /components/Modal.jsx 826 | 827 | ```js 828 | import { useGlobalContext } from '../context' 829 | 830 | const Modal = () => { 831 | 832 | return 837 | } 838 | 839 | export default Modal 840 | 841 | ``` 842 | 843 | context.jsx 844 | 845 | 846 | ```js 847 | const AppProvider = ({ children }) => { 848 | 849 | 850 | const [showModal, setShowModal] = useState(false) 851 | 852 | 853 | 854 | return ( 855 | 858 | {children} 859 | 860 | ) 861 | } 862 | ``` 863 | 864 | App.jsx 865 | 866 | ```js 867 | import { useGlobalContext } from './context' 868 | import './App.css' 869 | 870 | 871 | 872 | import Search from './components/Search' 873 | import Meals from './components/Meals' 874 | import Modal from './components/Modal' 875 | import Favorites from './components/Favorites' 876 | export default function App() { 877 | const { showModal } = useGlobalContext() 878 | 879 | return ( 880 |
881 | 882 | 883 | {/**/} 884 | 885 | 886 | {showModal && } 887 |
888 | ) 889 | } 890 | 891 | 892 | ``` 893 | 894 | 895 | #### Modal CSS - Setup 896 | 897 | App.css 898 | ```css 899 | .modal-overlay { 900 | position: fixed; 901 | top: 0; 902 | left: 0; 903 | width: 100%; 904 | height: 100%; 905 | background: rgba(0, 0, 0, 0.85); 906 | display: grid; 907 | place-items: center; 908 | transition: var(--transition); 909 | z-index:100; 910 | } 911 | .modal-container{ 912 | width:80vw; 913 | max-width:800px; 914 | height:80vh; 915 | overflow:scroll; 916 | background:var(--white); 917 | border-radius:var(--borderRadius); 918 | } 919 | ``` 920 | 921 | #### Display Meal in the Modal 922 | 923 | context.jsx 924 | 925 | ```js 926 | const AppProvider = ({ children }) => { 927 | 928 | const [selectedMeal, setSelectedMeal] = useState(null) 929 | 930 | 931 | const selectMeal = (idMeal, favoriteMeal) => { 932 | let meal; 933 | 934 | meal = meals.find((meal) => meal.idMeal === idMeal); 935 | 936 | setSelectedMeal(meal); 937 | setShowModal(true) 938 | } 939 | 940 | return ( 941 | 944 | {children} 945 | 946 | ) 947 | } 948 | ``` 949 | 950 | /components/Meals.jsx 951 | ```js 952 | import { useGlobalContext } from '../context' 953 | import { BsHandThumbsUp } from 'react-icons/bs' 954 | const Meals = () => { 955 | const { loading, meals, selectMeal } = useGlobalContext(); 956 | 957 | if (loading) { 958 | return
959 |

Loading...

960 |
961 | } 962 | 963 | if (meals.length < 1) { 964 | return
965 |

No meals matched your search term. Please try again.

966 |
967 | } 968 | 969 | return
970 | {meals.map((singleMeal) => { 971 | const { idMeal, strMeal: title, strMealThumb: image } = singleMeal 972 | return
973 | selectMeal(idMeal)} /> 974 | 978 |
979 | })} 980 |
981 | 982 | } 983 | 984 | export default Meals 985 | 986 | ``` 987 | 988 | #### Display Selcted Meal and Close Modal 989 | 990 | context.jsx 991 | ```js 992 | const AppProvider = ({ children }) => { 993 | 994 | const closeModal = () => { 995 | setShowModal(false) 996 | } 997 | 998 | return ( 999 | 1002 | {children} 1003 | 1004 | ) 1005 | } 1006 | ``` 1007 | 1008 | /components/Modal.jsx 1009 | 1010 | ```js 1011 | import { useGlobalContext } from '../context' 1012 | 1013 | const Modal = () => { 1014 | const { selectedMeal, closeModal } = useGlobalContext() 1015 | 1016 | const { strMealThumb: image, strMeal: title, strInstructions: text, strSource: source } = selectedMeal 1017 | return 1029 | } 1030 | 1031 | export default Modal 1032 | 1033 | ``` 1034 | 1035 | #### Modal CSS - Complete 1036 | 1037 | App.css 1038 | 1039 | ```css 1040 | .modal-img{ 1041 | height:15rem; 1042 | border-top-left-radius:var(--borderRadius); 1043 | border-top-right-radius:var(--borderRadius); 1044 | 1045 | } 1046 | 1047 | .modal-content{ 1048 | padding:1rem 1.5rem; 1049 | } 1050 | .modal-content p{ 1051 | color:var(--grey-600); 1052 | } 1053 | .modal-content a{ 1054 | display:block; 1055 | color:var(--primary-500); 1056 | margin-bottom:1rem; 1057 | text-decoration:underline; 1058 | transition:var(--transition); 1059 | } 1060 | .modal-content a:hover{ 1061 | 1062 | color:var(--black); 1063 | } 1064 | .close-btn{ 1065 | background:var(--red-dark); 1066 | color:var(--white); 1067 | } 1068 | 1069 | ``` 1070 | 1071 | #### Favorites - Setup 1072 | 1073 | 1074 | context.jsx 1075 | 1076 | ```js 1077 | const AppProvider = ({ children }) => { 1078 | 1079 | const [favorites, setFavorites] = useState([]); 1080 | 1081 | 1082 | const addToFavorites = (idMeal) => { 1083 | const meal = meals.find((meal) => meal.idMeal === idMeal); 1084 | const alreadyFavorite = favorites.find((meal) => meal.idMeal === idMeal); 1085 | if (alreadyFavorite) return 1086 | const updatedFavorites = [...favorites, meal] 1087 | setFavorites(updatedFavorites) 1088 | } 1089 | const removeFromFavorites = (idMeal) => { 1090 | const updatedFavorites = favorites.filter((meal) => meal.idMeal !== idMeal); 1091 | setFavorites(updatedFavorites) 1092 | } 1093 | return ( 1094 | 1097 | {children} 1098 | 1099 | ) 1100 | } 1101 | ``` 1102 | 1103 | /components/Meals.jsx 1104 | 1105 | 1106 | ```js 1107 | import { useGlobalContext } from '../context' 1108 | import { BsHandThumbsUp } from 'react-icons/bs' 1109 | const Meals = () => { 1110 | const { loading, meals, selectMeal, addToFavorites } = useGlobalContext(); 1111 | 1112 | if (loading) { 1113 | return
1114 |

Loading...

1115 |
1116 | } 1117 | 1118 | if (meals.length < 1) { 1119 | return
1120 |

No meals matched your search term. Please try again.

1121 |
1122 | } 1123 | 1124 | return
1125 | {meals.map((singleMeal) => { 1126 | const { idMeal, strMeal: title, strMealThumb: image } = singleMeal 1127 | return
1128 | selectMeal(idMeal)} /> 1129 | 1133 |
1134 | })} 1135 |
1136 | 1137 | } 1138 | 1139 | export default Meals 1140 | 1141 | 1142 | 1143 | 1144 | ``` 1145 | 1146 | #### Render Favorites 1147 | 1148 | App.jsx 1149 | 1150 | 1151 | ```js 1152 | import { useGlobalContext } from './context' 1153 | import './App.css' 1154 | 1155 | 1156 | 1157 | import Search from './components/Search' 1158 | import Meals from './components/Meals' 1159 | import Modal from './components/Modal' 1160 | import Favorites from './components/Favorites' 1161 | export default function App() { 1162 | const { showModal, favorites } = useGlobalContext() 1163 | 1164 | return ( 1165 |
1166 | 1167 | 1168 | {favorites.length > 0 && } 1169 | 1170 | 1171 | {showModal && } 1172 |
1173 | ) 1174 | } 1175 | 1176 | ``` 1177 | 1178 | /components/Favorites 1179 | 1180 | ```js 1181 | import { useGlobalContext } from '../context' 1182 | 1183 | 1184 | const Favorites = () => { 1185 | const { favorites, selectMeal, removeFromFavorites } = useGlobalContext() 1186 | 1187 | return
1188 |
1189 |
Favorites
1190 |
1191 | {favorites.map((item) => { 1192 | const { idMeal, strMealThumb: image } = item; 1193 | 1194 | return
1195 | 1196 | 1197 |
1198 | })} 1199 |
1200 |
1201 |
1202 | } 1203 | 1204 | 1205 | export default Favorites 1206 | ``` 1207 | 1208 | ##### Favorites CSS 1209 | 1210 | App.css 1211 | 1212 | ```css 1213 | /* Favorites */ 1214 | 1215 | .favorites{ 1216 | background:var(--black); 1217 | color:var(--white); 1218 | padding:1rem 0; 1219 | } 1220 | 1221 | .favorites-content{ 1222 | width: var(--view-width); 1223 | max-width: var(--max-width); 1224 | margin:0 auto; 1225 | } 1226 | .favorites-container{ 1227 | display:flex; 1228 | gap:0.5rem; 1229 | flex-wrap:wrap; 1230 | } 1231 | .favorite-item{ 1232 | text-align:center; 1233 | } 1234 | .favorites-img{ 1235 | width:60px; 1236 | border-radius:50%; 1237 | border:5px solid var(--white); 1238 | cursor:pointer; 1239 | } 1240 | .remove-btn{ 1241 | margin-top:0.25rem; 1242 | background:transparent; 1243 | color:var(--white); 1244 | border:transparent; 1245 | cursor:pointer; 1246 | transition:var(--transition); 1247 | font-size:0.5rem; 1248 | } 1249 | .remove-btn:hover{ 1250 | color:var(--red-dark); 1251 | } 1252 | ``` 1253 | 1254 | #### SelectMeal Refactor 1255 | 1256 | context.jsx 1257 | 1258 | ```js 1259 | const selectMeal = (idMeal, favoriteMeal) => { 1260 | let meal; 1261 | if (favoriteMeal) { 1262 | meal = favorites.find((meal) => meal.idMeal === idMeal); 1263 | } else { 1264 | meal = meals.find((meal) => meal.idMeal === idMeal); 1265 | } 1266 | setSelectedMeal(meal); 1267 | setShowModal(true) 1268 | } 1269 | ``` 1270 | 1271 | /components/Favorites.jsx 1272 | 1273 | ```js 1274 | import { useGlobalContext } from '../context' 1275 | 1276 | 1277 | const Favorites = () => { 1278 | const { favorites, selectMeal, removeFromFavorites } = useGlobalContext() 1279 | 1280 | return
1281 |
1282 |
Favorites
1283 |
1284 | {favorites.map((item) => { 1285 | const { idMeal, strMealThumb: image } = item; 1286 | 1287 | return
1288 | selectMeal(idMeal, true)} /> 1289 | 1290 |
1291 | })} 1292 |
1293 |
1294 |
1295 | } 1296 | 1297 | 1298 | export default Favorites 1299 | ``` 1300 | 1301 | #### Add Favorites to Local Storage 1302 | 1303 | contex.jsx 1304 | 1305 | ```js 1306 | const getFavoritesFromLocalStorage = () => { 1307 | let favorites = localStorage.getItem('favorites'); 1308 | if (favorites) { 1309 | favorites = JSON.parse(localStorage.getItem('favorites')) 1310 | } 1311 | else { 1312 | favorites = [] 1313 | } 1314 | return favorites 1315 | } 1316 | 1317 | const AppProvider = ({ children }) => { 1318 | 1319 | const [favorites, setFavorites] = useState(getFavoritesFromLocalStorage()); 1320 | 1321 | 1322 | const addToFavorites = (idMeal) => { 1323 | const meal = meals.find((meal) => meal.idMeal === idMeal); 1324 | const alreadyFavorite = favorites.find((meal) => meal.idMeal === idMeal); 1325 | if (alreadyFavorite) return 1326 | const updatedFavorites = [...favorites, meal] 1327 | setFavorites(updatedFavorites) 1328 | localStorage.setItem("favorites", JSON.stringify(updatedFavorites)) 1329 | } 1330 | const removeFromFavorites = (idMeal) => { 1331 | const updatedFavorites = favorites.filter((meal) => meal.idMeal !== idMeal); 1332 | setFavorites(updatedFavorites) 1333 | localStorage.setItem("favorites", JSON.stringify(updatedFavorites)) 1334 | } 1335 | return ( 1336 | 1339 | {children} 1340 | 1341 | ) 1342 | } 1343 | ``` 1344 | 1345 | --------------------------------------------------------------------------------