├── .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
8 |
9 |
10 |
11 |
{title}
12 |
Cooking Instructions
13 |
{text}
14 |
Original Source
15 |
close
16 |
17 |
18 |
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 |
removeFromFavorites(idMeal)}>remove
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
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
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 |
24 | {title}
25 | addToFavorites(idMeal)}>
26 |
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 |
377 | {title}
378 | click me
379 |
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
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
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
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
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
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
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
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
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 |
975 | {title}
976 |
977 |
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
1018 |
1019 |
1020 |
1021 |
{title}
1022 |
Cooking Instructions
1023 |
{text}
1024 |
Original Source
1025 |
close
1026 |
1027 |
1028 |
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
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 |
1130 | {title}
1131 | addToFavorites(idMeal)}>
1132 |
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 |
removeFromFavorites(idMeal)}>remove
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 |
removeFromFavorites(idMeal)}>remove
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 |
--------------------------------------------------------------------------------