├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── frontendwireframe.png ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── Pages ├── Form.js ├── Home.js ├── InfoPage.js ├── Login.js └── Profile.js ├── Routes.js ├── components ├── Comments.js ├── FoodDisplay.js ├── FoodSearch.js └── NavBar.js ├── index.css ├── index.js ├── reportWebVitals.js ├── setupTests.js └── styles ├── FoodForm.module.css ├── FoodInfo.module.css ├── Navbar.module.css └── Signup.module.css /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Howedible Frontend 2 | 3 | This is the The backend github repository 4 | https://github.com/han18/Backend.git 5 | 6 | Frontend deployment link 7 | https://elegant-starburst-9dc3a6.netlify.app/ 8 | ## Description of the project: 9 | 10 | HowEdible is a recipe search app that contains recipes from all over the world with images and cooking instructions. 11 | 12 | ## future implementations of this project 13 | 14 | Convert the app into a food social network where people who are passionate about cooking can create channels and upload short videos of their recipes. 15 | 16 | ## List of installed dependencies 17 | 18 | react-router-dom, 19 | bootstrap, 20 | axios 21 | 22 | ## Source References 23 | 24 | Form: sign up 25 | https://youtu.be/adMD46G5BXU?si=uPdY0tBqmz9D0jf- 26 | https://blog.logrocket.com/how-to-make-http-requests-like-a-pro-with-axios/ 27 | 28 | ## FoodSearch Component: 29 | 30 | api: www.themealdb.com/api/json/v1/1/search.php?f=a 31 | 32 | ## YT: 33 | 34 | https://youtu.be/HYSspU4XhC4?si=yegOMb-UetBGi893 35 | https://youtu.be/KRJvlxhLXxk?si=wNeW-FviPtoBl3cm 36 | https://youtu.be/oU2c_02YaME?si=0w42wwgzfwVQBMDZ 37 | 38 | Routing: 39 | https://www.youtube.com/watch?v=SLfhMt5OUPI 40 | 41 | Redirect users to pages in the profile 42 | https://www.geeksforgeeks.org/how-to-redirect-to-a-relative-url-in-javascript/ 43 | 44 | ## Sources to use the useNavigate and local storage and 45 | 46 | https://www.youtube.com/watch?v=NvNJtZaDt8E 47 | https://reactrouter.com/en/main/hooks/use-navigate 48 | https://www.youtube.com/watch?v=rWfhwW9forg 49 | https://stackoverflow.com/questions/60688411/assign-local-storage-to-react-state-react-hooks 50 | https://www.youtube.com/watch?v=fTP2gi7e3k8 51 | 52 | ## Using the Crud operation to create comments source 53 | 54 | https://www.youtube.com/watch?v=XYrNkTkx8Uc 55 | 56 | ## optional chaining 57 | 58 | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining 59 | https://www.youtube.com/watch?v=K7rHYyf6E0g 60 | https://javascript.info/optional-chaining 61 | 62 | ## css reference and sources 63 | 64 | https://www.youtube.com/watch?v=ZawAwPYrxGA 65 | https://create-react-app.dev/docs/adding-a-css-modules-stylesheet/ 66 | 67 | ## This is the frontend wire-frame of the app 68 | 69 | frontendwireframe 70 | 71 | ## This is the frontend structure of the app 72 | 73 | Screenshot 2024-02-13 at 12 25 30 PM 74 | 75 | \***\*\*\*\*\***\*\*\***\*\*\*\*\***\*\*\***\*\*\*\*\***\*\*\***\*\*\*\*\***\*\*\*\*\***\*\*\*\*\***\*\*\***\*\*\*\*\***\*\*\***\*\*\*\*\***\*\*\***\*\*\*\*\*** 76 | 77 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 78 | 79 | ## Available Scripts 80 | 81 | In the project directory, you can run: 82 | 83 | ### `npm start` 84 | 85 | Runs the app in the development mode.\ 86 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 87 | 88 | The page will reload when you make changes.\ 89 | You may also see any lint errors in the console. 90 | 91 | ### `npm test` 92 | 93 | Launches the test runner in the interactive watch mode.\ 94 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 95 | 96 | ### `npm run build` 97 | 98 | Builds the app for production to the `build` folder.\ 99 | It correctly bundles React in production mode and optimizes the build for the best performance. 100 | 101 | The build is minified and the filenames include the hashes.\ 102 | Your app is ready to be deployed! 103 | 104 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 105 | 106 | ### `npm run eject` 107 | 108 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 109 | 110 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 111 | 112 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 113 | 114 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 115 | 116 | ## Learn More 117 | 118 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 119 | 120 | To learn React, check out the [React documentation](https://reactjs.org/). 121 | 122 | ### Code Splitting 123 | 124 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 125 | 126 | ### Analyzing the Bundle Size 127 | 128 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 129 | 130 | ### Making a Progressive Web App 131 | 132 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 133 | 134 | ### Advanced Configuration 135 | 136 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 137 | 138 | ### Deployment 139 | 140 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 141 | 142 | ### `npm run build` fails to minify 143 | 144 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 145 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.17.0", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "axios": "^1.6.7", 10 | "bootstrap": "^5.3.2", 11 | "react": "^18.2.0", 12 | "react-bootstrap": "^2.10.0", 13 | "react-dom": "^18.2.0", 14 | "react-router-dom": "^6.22.0", 15 | "react-scripts": "5.0.1", 16 | "web-vitals": "^2.1.4" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/han18/Frontend/b7a3ed6576eec0a8bb93861a9249d802a36ad92a/public/favicon.ico -------------------------------------------------------------------------------- /public/frontendwireframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/han18/Frontend/b7a3ed6576eec0a8bb93861a9249d802a36ad92a/public/frontendwireframe.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/han18/Frontend/b7a3ed6576eec0a8bb93861a9249d802a36ad92a/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/han18/Frontend/b7a3ed6576eec0a8bb93861a9249d802a36ad92a/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* .App { 2 | text-align: center; 3 | } */ 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Route, Routes } from "react-router-dom"; 3 | import Form from "./Pages/Form"; 4 | // import "./styles/FoodForm.module.css"; 5 | import "./App.css"; 6 | import Home from "./Pages/Home"; 7 | import InfoPage from "./Pages/InfoPage"; 8 | import Login from "./Pages/Login"; 9 | import Profile from "./Pages/Profile"; 10 | import NavBar from "./components/NavBar"; 11 | 12 | const App = () => { 13 | const [user, setUser] = useState({}); 14 | 15 | // also used to set the user in the local storage 16 | useEffect(() => { 17 | let userData = localStorage.getItem("user"); 18 | userData = JSON.parse(userData); 19 | if (userData?.email) { 20 | setUser(userData); 21 | } 22 | }, []); 23 | 24 | return ( 25 |
26 | 27 | 28 | } /> 29 | } /> 30 | } 33 | /> 34 | } /> 35 | } 38 | /> 39 | 40 |
41 | ); 42 | }; 43 | 44 | export default App; 45 | 46 | // this is the old code 47 | // return ( 48 | //
49 | //

Meal Search App

50 | // 51 | // 59 | // 60 | // } /> 61 | // 62 | //
63 | // ); 64 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/Pages/Form.js: -------------------------------------------------------------------------------- 1 | // this file is the form reflects the users collection schema 2 | import "../styles/Signup.module.css"; 3 | 4 | import axios from "axios"; 5 | import { useState } from "react"; 6 | import { useNavigate } from "react-router-dom"; 7 | 8 | const Form = ({ setUser }) => { 9 | const [formData, setFormData] = useState({ 10 | username: "", 11 | first_name: "", 12 | last_name: "", 13 | email: "", 14 | password: "", 15 | }); 16 | 17 | // to redirect the user to the profile page 18 | const navigate = useNavigate(); 19 | 20 | const handleChange = (e) => { 21 | setFormData({ ...formData, [e.target.name]: e.target.value }); 22 | }; 23 | 24 | // function to submit the form 25 | const handleSubmit = async (e) => { 26 | e.preventDefault(); 27 | 28 | try { 29 | const response = await axios.post( 30 | "https://howedible-backend.onrender.com/api/users", 31 | formData 32 | ); 33 | 34 | console.log(formData); 35 | if (response.status === 201) { 36 | console.log("User added successfully", response.data.user); 37 | 38 | // taking the user id from the response and redirecting them to the login page 39 | // window.location.href = `/profile/${response.data.user._id}`; 40 | navigate("/login"); 41 | } else { 42 | console.error("Failed to add user"); 43 | } 44 | } catch (error) { 45 | console.error("Error:", error); 46 | } 47 | }; 48 | 49 | return ( 50 |
51 |
52 |

Create an account

53 | 54 | 66 | 67 | 68 | 77 | 78 | 79 | 88 | 89 | 90 | 99 | 100 | 101 | 113 | 114 | 115 |
116 |
117 | ); 118 | }; 119 | 120 | export default Form; 121 | -------------------------------------------------------------------------------- /src/Pages/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import FoodDisplay from "../components/FoodDisplay"; 3 | import FoodSearch from "../components/FoodSearch"; 4 | 5 | const Home = () => { 6 | const [searchTerm, setSearchTerm] = useState(""); 7 | const [searchResults, setSearchResults] = useState([]); 8 | 9 | useEffect(() => { 10 | const fetchData = async () => { 11 | try { 12 | const response = await fetch( 13 | `https://www.themealdb.com/api/json/v1/1/search.php?f=${searchTerm}` 14 | ); 15 | const data = await response.json(); 16 | console.log("data", data); 17 | // if recipe is there or return an empty array 18 | setSearchResults(data.meals || []); 19 | console.log(data); 20 | } catch (e) { 21 | console.error(e); 22 | } 23 | }; 24 | 25 | fetchData(); 26 | }, [searchTerm]); 27 | return ( 28 | <> 29 |

30 | Recipe Search App 31 |

32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default Home; 39 | -------------------------------------------------------------------------------- /src/Pages/InfoPage.js: -------------------------------------------------------------------------------- 1 | // this is the info page that displays the info about the recipes 2 | 3 | import { useEffect, useState } from "react"; 4 | import { useParams } from "react-router-dom"; 5 | import Comments from "../components/Comments"; 6 | import styles from "../styles/FoodInfo.module.css"; 7 | 8 | const Info = ({ user }) => { 9 | const [meal, setMeal] = useState({}); 10 | const { id } = useParams(); 11 | 12 | useEffect(() => { 13 | if (id) { 14 | const fetchData = async () => { 15 | try { 16 | // get single meal using meal id 17 | const response = await fetch( 18 | `https://www.themealdb.com/api/json/v1/1/lookup.php?i=${id}` 19 | ); 20 | const data = await response.json(); 21 | // if recipe is there or return an empty array 22 | setMeal(data.meals[0] || {}); 23 | } catch (e) { 24 | console.error(e); 25 | } 26 | }; 27 | 28 | fetchData(); 29 | } 30 | }, [id]); 31 | 32 | return ( 33 |
34 |
35 | {meal?.strMeal} 36 |
37 |
38 |

{meal?.strMeal}

39 | Country: {meal?.strArea} 40 |
41 |

Instructions:

42 |

{meal?.strInstructions}

43 | 44 | {/* user id comments */} 45 | 46 |
47 | ); 48 | }; 49 | 50 | export default Info; 51 | 52 | //
    53 | // {searchResults.map((meal) => ( 54 | //
  • {meal.strMeal}
  • 55 | // ))} 56 | //
57 | -------------------------------------------------------------------------------- /src/Pages/Login.js: -------------------------------------------------------------------------------- 1 | // this file is the form reflects the users collection schema 2 | import "../styles/Signup.module.css"; 3 | 4 | import axios from "axios"; 5 | import { useState } from "react"; 6 | import { useNavigate } from "react-router-dom"; 7 | 8 | const Login = ({ setUser }) => { 9 | const [formData, setFormData] = useState({ 10 | email: "", 11 | password: "", 12 | }); 13 | 14 | const navigate = useNavigate(); 15 | 16 | const handleChange = (e) => { 17 | setFormData({ ...formData, [e.target.name]: e.target.value }); 18 | }; 19 | 20 | const handleSubmit = async (e) => { 21 | e.preventDefault(); 22 | 23 | try { 24 | const response = await axios.post( 25 | "https://howedible-backend.onrender.com/api/users/login", 26 | formData 27 | ); 28 | console.log(response?.data); 29 | 30 | // If success it saves user information to the local storag sets the state, and redirects the user to the profile 31 | if (response.data?.user?._id) { 32 | localStorage.setItem("user", JSON.stringify(response.data?.user)); 33 | setUser(response.data?.user); 34 | 35 | // taking the user id from the response and redirecting them to the profile page 36 | // window.location.href = `/profile/${response.data.user._id}`; 37 | navigate("/profile"); 38 | } else { 39 | console.error("Failed to login"); 40 | alert("Incorrect Email or Password."); 41 | } 42 | } catch (error) { 43 | console.error("Error:", error); 44 | } 45 | }; 46 | 47 | return ( 48 |
49 |
50 |

Welcome Back

51 | 52 | 53 | 62 | 63 | 64 | 76 | 77 | 78 |
79 |
80 | ); 81 | }; 82 | 83 | export default Login; 84 | -------------------------------------------------------------------------------- /src/Pages/Profile.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | 4 | const Profile = ({ user }) => { 5 | const navigate = useNavigate(); 6 | 7 | // the optional chaining ?. if for some reason the id property is not present in the response object, then no error will be given. 8 | // if user is logged in display the welcome message 9 | if (!user?.email) { 10 | // if email does exist in the user object 11 | return navigate("/"); 12 | } 13 | 14 | return ( 15 |
16 |
20 |

Welcome, {user.first_name}

21 |

Username: {user.username}

22 |

First Name: {user.first_name}

23 |

Last Name: {user.last_name}

24 |

Email: {user.email}

25 |

26 |

{user.first_name}, we are excited for you to try new recipes!

27 |
28 |
29 | ); 30 | }; 31 | 32 | export default Profile; 33 | -------------------------------------------------------------------------------- /src/Routes.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 3 | import Form from "./Pages/Form"; 4 | import Profile from "./components/Profile"; 5 | 6 | const Routes = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default Routes; 18 | -------------------------------------------------------------------------------- /src/components/Comments.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import styles from "../styles/FoodInfo.module.css"; 3 | 4 | const Comments = ({ id, user }) => { 5 | const [comments, setComments] = useState([]); 6 | const [comment, setComment] = useState(""); 7 | const [editableComment, setEditableComment] = useState({}); 8 | 9 | // when editbale comment state change 10 | useEffect(() => { 11 | if (editableComment?._id) { 12 | setComment(editableComment?.content); 13 | } 14 | }, [editableComment]); 15 | 16 | useEffect(() => { 17 | if (id) { 18 | const fetchData = async () => { 19 | try { 20 | // get all comments 21 | const commentResponse = await fetch( 22 | `https://howedible-backend.onrender.com/api/comments` 23 | ); 24 | const commentsData = await commentResponse.json(); 25 | // if recipe is there or return an empty array 26 | setComments(commentsData || []); 27 | } catch (e) { 28 | console.error(e); 29 | } 30 | }; 31 | 32 | fetchData(); 33 | } 34 | }, [id]); 35 | 36 | // add new comment and update comment 37 | const submitHanlder = (e) => { 38 | e.preventDefault(); 39 | 40 | // to edit comment 41 | if (editableComment?._id) { 42 | const options = { 43 | // put request 44 | method: "PUT", 45 | headers: { "Content-Type": "application/json" }, 46 | body: JSON.stringify({ content: comment, user_id: user?._id }), 47 | }; 48 | 49 | fetch( 50 | `https://howedible-backend.onrender.com/api/comments/${editableComment?._id}`, 51 | options 52 | ) 53 | .then((response) => response.json()) 54 | .then((response) => { 55 | if (response?._id) { 56 | const updatedComments = comments?.map((item) => { 57 | if (item?._id === response?._id) { 58 | return { 59 | ...response, 60 | user_id: user, 61 | }; 62 | } else { 63 | return item; 64 | } 65 | }); 66 | 67 | setComments(updatedComments); 68 | setEditableComment({}); 69 | setComment(""); 70 | } 71 | }) 72 | .catch((err) => console.error(err)); 73 | } else { 74 | const options = { 75 | // post new comment 76 | method: "POST", 77 | headers: { "Content-Type": "application/json" }, 78 | body: JSON.stringify({ content: comment, user_id: user?._id }), 79 | }; 80 | 81 | fetch("https://howedible-backend.onrender.com/api/comments", options) 82 | .then((response) => response.json()) 83 | .then((response) => { 84 | if (response?._id) { 85 | setComments([ 86 | ...comments, 87 | { 88 | ...response, 89 | user_id: user, 90 | }, 91 | ]); 92 | setComment(""); 93 | } 94 | }) 95 | .catch((err) => console.error(err)); 96 | } 97 | }; 98 | 99 | // delete comment handler 100 | const deleteComment = (id) => { 101 | // to delete comment 102 | const options = { 103 | method: "DELETE", 104 | headers: { "Content-Type": "application/json" }, 105 | }; 106 | 107 | fetch(`https://howedible-backend.onrender.com/api/comments/${id}`, options) 108 | .then((response) => response.json()) 109 | .then((response) => { 110 | if (response?._id) { 111 | const updatedComments = comments?.filter( 112 | (item) => item?._id !== response?._id 113 | ); // filter out the deleted comment from the comments list 114 | setComments(updatedComments); 115 | } 116 | }) 117 | .catch((err) => console.error(err)); 118 | }; 119 | return ( 120 |
121 |

Comments

122 | {/* if users are logged in, show all comments */} 123 | {user?.email && ( 124 |
125 | Add New Comment 126 |
127 |
128 |
129 |