From 24bc60af629a9705bb22a4d39fab789f264c0142 Mon Sep 17 00:00:00 2001 From: Anuraag Jajoo Date: Wed, 23 Oct 2024 23:45:34 -0400 Subject: [PATCH 01/43] Added landing page with Get Started button --- Code/backend/.env | 4 +- Code/frontend/src/App.js | 164 ++++++++++++-------- Code/frontend/src/components/LandingPage.js | 26 ++++ Code/frontend/src/components/Navbar.js | 120 +++++++------- 4 files changed, 190 insertions(+), 124 deletions(-) create mode 100644 Code/frontend/src/components/LandingPage.js diff --git a/Code/backend/.env b/Code/backend/.env index 6613871c..c1205b8e 100644 --- a/Code/backend/.env +++ b/Code/backend/.env @@ -1,4 +1,4 @@ -RECIPES_DB_URI=mongodb+srv://atharvajoshi067:ZgSvdar14OnteUZx@cluster0.9zuebnu.mongodb.net/?retryWrites=true&w=majority +RECIPES_DB_URI=mongodb+srv://anuraagjajoo:Q95hNDnuUkr44QvZ@cluster0.lb5su.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0 RECIPES_NS=recipe_recommender PORT=5000 -GMAIL= atharva.joshi17@siesgst.ac.in \ No newline at end of file +GMAIL= ajajoo3@ncsu.edu \ No newline at end of file diff --git a/Code/frontend/src/App.js b/Code/frontend/src/App.js index 08ea45f8..16f7ea22 100644 --- a/Code/frontend/src/App.js +++ b/Code/frontend/src/App.js @@ -1,4 +1,4 @@ -// +// import Form from "./components/Form.js"; import Header from "./components/Header"; import recipeDB from "./apis/recipeDB"; @@ -11,6 +11,7 @@ import Nav from "./components/Navbar.js"; import SearchByRecipe from "./components/SearchByRecipe.js"; import Login from "./components/Login.js"; import UserProfile from "./components/UserProfile.js"; +import LandingPage from "./components/LandingPage.js"; // Main component of the project class App extends Component { @@ -29,76 +30,76 @@ class App extends Component { isLoading: false, isLoggedIn: false, isProfileView: false, - userData: {} + userData: {}, }; } - handleBookMarks = ()=> { + handleBookMarks = () => { this.setState({ - isProfileView: true - }) - } + isProfileView: true, + }); + }; - handleProfileView = ()=> { + handleProfileView = () => { this.setState({ - isProfileView: false - }) - } + isProfileView: false, + }); + }; - handleSignup = async (userName, password)=> { + handleSignup = async (userName, password) => { try { const response = await recipeDB.post("/recipes/signup", { - userName, - password + userName, + password, }); - console.log(response.data) + console.log(response.data); if (response.data.success) { - alert("Successfully Signed up!") + alert("Successfully Signed up!"); this.setState({ isLoggedIn: true, - userData: response.data.user - }) - localStorage.setItem("userName", response.data.user.userName) - console.log(response.data.user) + userData: response.data.user, + }); + localStorage.setItem("userName", response.data.user.userName); + console.log(response.data.user); } else { - alert("User already exists") + alert("User already exists"); } } catch (err) { console.log(err); } - } + }; handleLogin = async (userName, password) => { try { const response = await recipeDB.get("/recipes/login", { params: { userName, - password + password, }, }); - console.log(response.data) + console.log(response.data); if (response.data.success) { this.setState({ isLoggedIn: true, - userData: response.data.user - }) - localStorage.setItem("userName", response.data.user.userName) - console.log(response.data.user) - alert("Successfully logged in!") + userData: response.data.user, + }); + localStorage.setItem("userName", response.data.user.userName); + console.log(response.data.user); + alert("Successfully logged in!"); } else { - console.log("Credentials are incorrect") + console.log("Credentials are incorrect"); } } catch (err) { console.log(err); } - } + }; // Function to get the user input from the Form component on Submit action handleSubmit = async (formDict) => { this.setState({ - isLoading: true - }) - console.log(formDict) + isLoading: true, + }); + console.log(formDict); this.setState({ // cuisine: cuisineInput, //NoIngredients: noIngredientsInput, @@ -118,20 +119,22 @@ class App extends Component { handleRecipesByName = (recipeName) => { this.setState({ - isLoading: true - }) - recipeDB.get("/recipes/getRecipeByName", { - params: { - recipeName: recipeName - } - }).then(res => { - console.log(res.data); - this.setState({ - recipeByNameList: res.data.recipes, - isLoading: false + isLoading: true, + }); + recipeDB + .get("/recipes/getRecipeByName", { + params: { + recipeName: recipeName, + }, }) - }) - } + .then((res) => { + console.log(res.data); + this.setState({ + recipeByNameList: res.data.recipes, + isLoading: false, + }); + }); + }; getRecipeDetails = async (ingredient, cuis, mail, flag) => { try { @@ -145,29 +148,39 @@ class App extends Component { }); this.setState({ recipeList: response.data.recipes, - isLoading: false + isLoading: false, }); } catch (err) { console.log(err); } }; - handleLogout = ()=> { - console.log("logged out") + handleLogout = () => { + console.log("logged out"); this.setState({ - isLoggedIn: false - }) - } + isLoggedIn: false, + showLogin: false, + userData: {}, + }); + }; render() { return (
-
); } diff --git a/Code/frontend/src/components/LandingPage.js b/Code/frontend/src/components/LandingPage.js new file mode 100644 index 00000000..fd4050ca --- /dev/null +++ b/Code/frontend/src/components/LandingPage.js @@ -0,0 +1,26 @@ +import { Box, Button, Heading, Text, Stack } from "@chakra-ui/react"; + +const LandingPage = ({ onGetStarted }) => { + return ( + + + Discover & Organize Your Favorite Recipes + + + Effortlessly search, organize, and share recipes with a few clicks. + + + + + + ); +}; + +export default LandingPage; diff --git a/Code/frontend/src/components/Navbar.js b/Code/frontend/src/components/Navbar.js index e4b582e6..32942528 100644 --- a/Code/frontend/src/components/Navbar.js +++ b/Code/frontend/src/components/Navbar.js @@ -1,4 +1,4 @@ -'use client' +"use client"; import { Box, @@ -16,86 +16,96 @@ import { Stack, useColorMode, Center, - Heading -} from '@chakra-ui/react' - + Heading, +} from "@chakra-ui/react"; // interface Props { // children: React.ReactNode // } const NavLink = (props) => { - const { children } = props + const { children } = props; return ( + href={"#"} + > {children} - ) -} + ); +}; export default function Nav(props) { - const { colorMode, toggleColorMode } = useColorMode() - const { isOpen, onOpen, onClose } = useDisclosure() - const handleBookMarks =()=> { + const { colorMode, toggleColorMode } = useColorMode(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const handleBookMarks = () => { props.handleBookMarks(); - } - const handleLogout = ()=> { - console.log("logged out") + }; + const handleLogout = () => { + console.log("logged out"); props.handleLogout(); - } + }; return ( <> - - - Saveurs Sélection + + + + Saveurs Sélection + - - - - - - - - -
-
+ + + {props.user ? ( + + -
-
-
-

{localStorage.getItem("userName")}

-
-
- - Bookmarks - Logout -
-
+ + +
+
+ +
+
+
+

{props.user.userName}

+
+
+ + Bookmarks + Logout +
+ + ) : ( + + )}
- ) -} \ No newline at end of file + ); +} From 6c3a53b7ab96291744827cc71d7bfb4257d55498 Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Fri, 25 Oct 2024 18:01:50 -0400 Subject: [PATCH 02/43] Replace existing .env with sample.env file --- Code/backend/.env | 4 ---- Code/backend/sample.env | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 Code/backend/.env create mode 100644 Code/backend/sample.env diff --git a/Code/backend/.env b/Code/backend/.env deleted file mode 100644 index c1205b8e..00000000 --- a/Code/backend/.env +++ /dev/null @@ -1,4 +0,0 @@ -RECIPES_DB_URI=mongodb+srv://anuraagjajoo:Q95hNDnuUkr44QvZ@cluster0.lb5su.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0 -RECIPES_NS=recipe_recommender -PORT=5000 -GMAIL= ajajoo3@ncsu.edu \ No newline at end of file diff --git a/Code/backend/sample.env b/Code/backend/sample.env new file mode 100644 index 00000000..2d38ca06 --- /dev/null +++ b/Code/backend/sample.env @@ -0,0 +1,4 @@ +RECIPES_DB_URI=[MongoDB Cluster URL] +RECIPES_NS=recipe_recommender +PORT=5000 +GMAIL= [unityID]@ncsu.edu \ No newline at end of file From ae585250d2766d3d666dd9d10ce5285f8f1361e8 Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Fri, 25 Oct 2024 21:23:37 -0400 Subject: [PATCH 03/43] Added function to rate existing recipes in database --- Code/backend/dao/recipesDAO.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Code/backend/dao/recipesDAO.js b/Code/backend/dao/recipesDAO.js index 50b7a208..153cd299 100644 --- a/Code/backend/dao/recipesDAO.js +++ b/Code/backend/dao/recipesDAO.js @@ -215,6 +215,7 @@ export default class RecipesDAO { inputRecipe["TotalTimeInMins"] = recipe["cookingTime"]; inputRecipe["Diet-type"] = recipe["dietType"]; inputRecipe["Recipe-rating"] = recipe["recipeRating"]; + inputRecipe["Times-rated"] = 1 inputRecipe["Cuisine"] = recipe["cuisine"]; inputRecipe["image-url"] = recipe["imageURL"]; inputRecipe["URL"] = recipe["recipeURL"]; @@ -244,6 +245,18 @@ export default class RecipesDAO { } } + static async rateRecipe(recipeID, rating) { + //console.log("Inside rateRecipe") + let r = await recipes.find({_id: recipeID}).collation({ locale: "en", strength: 2 }).toArray(); + let recipe = r[0] + let timesRated = recipe["Times-rated"] ? Number(recipe["Times-rated"]) : 1 + let newRating = Number(recipe["Recipe-rating"]) * timesRated + newRating += rating + timesRated++ + newRating /= timesRated + await recipes.updateOne({_id: recipeID}, {$set: {'Times-rated': timesRated, 'Recipe-rating': newRating}}) + } + //function to add recipe to user profile static async addRecipeToProfile(userName, recipe) { let response; From aec08591c5f399d77a1fd828f097708adfc2c33a Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Fri, 25 Oct 2024 22:05:01 -0400 Subject: [PATCH 04/43] Tests now use correct cluster --- Code/backend/__tests__/test.spec.js | 3 +-- Code/backend/package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Code/backend/__tests__/test.spec.js b/Code/backend/__tests__/test.spec.js index 9460c698..b4224eda 100644 --- a/Code/backend/__tests__/test.spec.js +++ b/Code/backend/__tests__/test.spec.js @@ -23,8 +23,7 @@ function test_connectivity_func() { // Connection URI. Update username, password, and your-cluster-url to reflect your cluster. // See httpsdocs.mongodb.comecosystemdriversnode for more details - const uri = - "mongodb+srv://atharvajoshi067:ZgSvdar14OnteUZx@cluster0.9zuebnu.mongodb.net/recipe_recommender?retryWrites=true&w=majority"; + const uri = process.env.RECIPES_DB_URI; var result = false; try { // Connect to the MongoDB cluster diff --git a/Code/backend/package.json b/Code/backend/package.json index 20c53293..647cbad1 100644 --- a/Code/backend/package.json +++ b/Code/backend/package.json @@ -5,7 +5,7 @@ "main": "index.js", "type": "module", "scripts": { - "test": "set CI=true && jest", + "test": "set CI=true && jest --setupFiles dotenv/config", "lint": "eslint 'dao/**.js'", "format": "prettier --check ./dao", "format:fix": "prettier --write ./dao" From 72207668dea1b1f3fba3c9e615fcd6ba1b018ce5 Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Fri, 25 Oct 2024 22:10:29 -0400 Subject: [PATCH 05/43] Created api route for rating existing recipes --- Code/backend/api/recipes.controller.js | 10 ++++++++++ Code/backend/api/recipes.route.js | 2 ++ Code/backend/dao/recipesDAO.js | 6 +++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Code/backend/api/recipes.controller.js b/Code/backend/api/recipes.controller.js index 79a30bfd..ef54603e 100644 --- a/Code/backend/api/recipes.controller.js +++ b/Code/backend/api/recipes.controller.js @@ -118,6 +118,16 @@ export default class RecipesController { } } + static async apiPatchRecipeRating(req, res, next) { + try { + let response = await RecipesDAO.rateRecipe(req.body) + res.json(response) + } catch (e) { + console.log(`api, ${e}`); + res.status(500).json({ error: e }); + } + } + static async apiGetIngredients(req, res, next) { try { let ingredients = await RecipesDAO.getIngredients(); diff --git a/Code/backend/api/recipes.route.js b/Code/backend/api/recipes.route.js index 3c3d7134..8b7919ac 100644 --- a/Code/backend/api/recipes.route.js +++ b/Code/backend/api/recipes.route.js @@ -22,4 +22,6 @@ router.route("/addRecipeToProfile").post(RecipesCtrl.apiPostRecipeToProfile); router.route("/getRecipeByName").get(RecipesCtrl.apiGetRecipeByName); +router.route("/rateRecipe").patch(RecipesCtrl.apiPatchRecipeRating) + export default router; diff --git a/Code/backend/dao/recipesDAO.js b/Code/backend/dao/recipesDAO.js index 153cd299..a613c93e 100644 --- a/Code/backend/dao/recipesDAO.js +++ b/Code/backend/dao/recipesDAO.js @@ -245,13 +245,13 @@ export default class RecipesDAO { } } - static async rateRecipe(recipeID, rating) { + static async rateRecipe(ratingBody) { //console.log("Inside rateRecipe") - let r = await recipes.find({_id: recipeID}).collation({ locale: "en", strength: 2 }).toArray(); + let r = await recipes.find({_id: ratingBody.recipeID}).collation({ locale: "en", strength: 2 }).toArray(); let recipe = r[0] let timesRated = recipe["Times-rated"] ? Number(recipe["Times-rated"]) : 1 let newRating = Number(recipe["Recipe-rating"]) * timesRated - newRating += rating + newRating += ratingBody.rating timesRated++ newRating /= timesRated await recipes.updateOne({_id: recipeID}, {$set: {'Times-rated': timesRated, 'Recipe-rating': newRating}}) From c010e88bd1a82a22f344079f1da6e23b041cf283 Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Fri, 25 Oct 2024 23:10:17 -0400 Subject: [PATCH 06/43] added unit test for new rating system --- Code/backend/__tests__/testRatings.js | 21 +++++++++++++++++++++ Code/backend/api/recipes.controller.js | 1 + Code/backend/dao/recipesDAO.js | 5 ++--- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 Code/backend/__tests__/testRatings.js diff --git a/Code/backend/__tests__/testRatings.js b/Code/backend/__tests__/testRatings.js new file mode 100644 index 00000000..841fd897 --- /dev/null +++ b/Code/backend/__tests__/testRatings.js @@ -0,0 +1,21 @@ +const { ObjectId } = require("mongodb"); + +const request = require("supertest")("http://localhost:5000/api/v1/recipes"); +const expect = require("chai").expect; + +describe("Ratings", function() { + + describe("PATCH /rateRecipe", function() { + it("Should correctly update the rating to be new average", async function() { + const response0 = await request.get("/getRecipeByName?recipeName=BLT"); + const res0JSON = JSON.parse(response0.text) + expect(response0.status).to.eql(200); + const response = await request.patch("/rateRecipe") + .send({ recipeID: res0JSON["recipes"][0]['_id'], rating: 3 }); + expect(response.status).to.eql(200); + const response2 = await request.get("/getRecipeByName?recipeName=BLT"); + const res2JSON = JSON.parse(response2.text) + expect(res2JSON["recipes"][0]['Recipe-rating'] === 4).true + }) + }) +}) \ No newline at end of file diff --git a/Code/backend/api/recipes.controller.js b/Code/backend/api/recipes.controller.js index ef54603e..446f4f99 100644 --- a/Code/backend/api/recipes.controller.js +++ b/Code/backend/api/recipes.controller.js @@ -120,6 +120,7 @@ export default class RecipesController { static async apiPatchRecipeRating(req, res, next) { try { + console.log(req.body) let response = await RecipesDAO.rateRecipe(req.body) res.json(response) } catch (e) { diff --git a/Code/backend/dao/recipesDAO.js b/Code/backend/dao/recipesDAO.js index a613c93e..40defe97 100644 --- a/Code/backend/dao/recipesDAO.js +++ b/Code/backend/dao/recipesDAO.js @@ -246,15 +246,14 @@ export default class RecipesDAO { } static async rateRecipe(ratingBody) { - //console.log("Inside rateRecipe") - let r = await recipes.find({_id: ratingBody.recipeID}).collation({ locale: "en", strength: 2 }).toArray(); + let r = await recipes.find({_id: new ObjectId(ratingBody.recipeID)}).collation({ locale: "en", strength: 2 }).toArray(); let recipe = r[0] let timesRated = recipe["Times-rated"] ? Number(recipe["Times-rated"]) : 1 let newRating = Number(recipe["Recipe-rating"]) * timesRated newRating += ratingBody.rating timesRated++ newRating /= timesRated - await recipes.updateOne({_id: recipeID}, {$set: {'Times-rated': timesRated, 'Recipe-rating': newRating}}) + await recipes.updateOne({_id: new ObjectId(ratingBody.recipeID)}, {$set: {'Times-rated': timesRated, 'Recipe-rating': newRating}}) } //function to add recipe to user profile From 70fa6e65c9d0fff17238fc771fcbb88f334be6eb Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Sat, 26 Oct 2024 13:42:36 -0400 Subject: [PATCH 07/43] Replaced displayed numeric rating with star ratings --- Code/frontend/src/components/Rating.js | 39 ++++++++++++++++++ Code/frontend/src/components/RecipeCard.js | 6 ++- Code/frontend/src/components/RecipeList.js | 6 ++- .../components/componentImages/Empty_star.png | Bin 0 -> 7664 bytes .../componentImages/Filled_star.png | Bin 0 -> 8167 bytes .../components/componentImages/Half_star.png | Bin 0 -> 8244 bytes 6 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 Code/frontend/src/components/Rating.js create mode 100644 Code/frontend/src/components/componentImages/Empty_star.png create mode 100644 Code/frontend/src/components/componentImages/Filled_star.png create mode 100644 Code/frontend/src/components/componentImages/Half_star.png diff --git a/Code/frontend/src/components/Rating.js b/Code/frontend/src/components/Rating.js new file mode 100644 index 00000000..871bbcd6 --- /dev/null +++ b/Code/frontend/src/components/Rating.js @@ -0,0 +1,39 @@ +import React from "react"; +import { Image, Box } from "@chakra-ui/react"; + +const Rating = (props) => { + var rate = Math.floor(Number(props.rating)) + var partial = Number(props.rating) - rate + var stars = [] + for(var i = 0; i < rate; i++) { + stars.push() + } + if(partial > 0.25 && partial < 0.75) { + stars.push() + } + while(stars.length < 5) { + stars.push() + } + return stars + +} + +export default Rating \ No newline at end of file diff --git a/Code/frontend/src/components/RecipeCard.js b/Code/frontend/src/components/RecipeCard.js index d16d867a..090d821e 100644 --- a/Code/frontend/src/components/RecipeCard.js +++ b/Code/frontend/src/components/RecipeCard.js @@ -1,6 +1,7 @@ import React from "react"; import { Box, SimpleGrid, Card, CardHeader, Heading, Text, CardBody, CardFooter, Button, Image, Tag } from "@chakra-ui/react" import recipeDB from "../apis/recipeDB"; +import Rating from "./Rating"; const RecipeCard = (props) => { @@ -29,7 +30,10 @@ const RecipeCard = (props) => { Cooking Time: {props.recipe.TotalTimeInMins} mins - Rating: {props.recipe['Recipe-rating']} + + Rating: + + Diet Type: {props.recipe['Diet-type']} save recipe diff --git a/Code/frontend/src/components/RecipeList.js b/Code/frontend/src/components/RecipeList.js index f4c2142d..b6f5eaa3 100644 --- a/Code/frontend/src/components/RecipeList.js +++ b/Code/frontend/src/components/RecipeList.js @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { Avatar, Flex, Modal, ModalBody, ModalCloseButton, ModalOverlay, ModalHeader, ModalFooter, ModalContent, Box, SimpleGrid, Text, Button } from "@chakra-ui/react" import RecipeCard from "./RecipeCard"; +import Rating from "./Rating"; // component to handle all the recipes const RecipeList = ({ recipes }) => { @@ -45,7 +46,10 @@ const RecipeList = ({ recipes }) => { Cooking Time: {currentRecipe.TotalTimeInMins} mins - Rating: {currentRecipe['Recipe-rating']} + + Rating: + + Diet Type: {currentRecipe['Diet-type']}
diff --git a/Code/frontend/src/components/componentImages/Empty_star.png b/Code/frontend/src/components/componentImages/Empty_star.png new file mode 100644 index 0000000000000000000000000000000000000000..d8989a8b28ce5787ecc51382343080de36fb4fb8 GIT binary patch literal 7664 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 C!(D{{ literal 0 HcmV?d00001 diff --git a/Code/frontend/src/components/componentImages/Filled_star.png b/Code/frontend/src/components/componentImages/Filled_star.png new file mode 100644 index 0000000000000000000000000000000000000000..98649b625deb72b6a58a4bb364cc77079c9926e7 GIT binary patch literal 8167 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

<1a)TQ2*jg(^*lz1v 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 YC7;f)?M+GoALsGZpXxlRQig~BAD*O!d;kCd literal 0 HcmV?d00001 diff --git a/Code/frontend/src/components/componentImages/Half_star.png b/Code/frontend/src/components/componentImages/Half_star.png new file mode 100644 index 0000000000000000000000000000000000000000..d92092327d096b14bc7e520fa9bd8c10d62549b2 GIT binary patch literal 8244 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 literal 0 HcmV?d00001 From 714525d1c5f96c2a07fd95f9afd802de89ecc998 Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Sat, 26 Oct 2024 14:59:23 -0400 Subject: [PATCH 08/43] Added button to rate existing recipes from frontend --- Code/frontend/src/App.js | 4 +- Code/frontend/src/components/RateRecipe.js | 64 ++++++++++++++++++ Code/frontend/src/components/RecipeList.js | 11 ++- .../componentImages/Filled_star_to_rate.png | Bin 0 -> 8232 bytes 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 Code/frontend/src/components/RateRecipe.js create mode 100644 Code/frontend/src/components/componentImages/Filled_star_to_rate.png diff --git a/Code/frontend/src/App.js b/Code/frontend/src/App.js index 16f7ea22..a746e346 100644 --- a/Code/frontend/src/App.js +++ b/Code/frontend/src/App.js @@ -25,6 +25,7 @@ class App extends Component { ingredients: new Set(), recipeList: [], recipeByNameList: [], + searchName: "", email: "", flag: false, isLoading: false, @@ -120,6 +121,7 @@ class App extends Component { handleRecipesByName = (recipeName) => { this.setState({ isLoading: true, + searchName: recipeName }); recipeDB .get("/recipes/getRecipeByName", { @@ -206,7 +208,7 @@ class App extends Component { {this.state.isLoading ? ( ) : ( - + )} diff --git a/Code/frontend/src/components/RateRecipe.js b/Code/frontend/src/components/RateRecipe.js new file mode 100644 index 00000000..ec2f430e --- /dev/null +++ b/Code/frontend/src/components/RateRecipe.js @@ -0,0 +1,64 @@ +import React, { useState } from "react" +import { Image, Box, Button, Text } from "@chakra-ui/react"; +import recipeDB from "../apis/recipeDB" + +const RateRecipe = (props) => { + const [rating, setRating] = useState(0); + const [show, setShow] = useState(true) + + const rateRecipe = (r) => { + const body = { + recipeID: props.recipe['_id'], + rating: r + } + recipeDB.patch("/recipes/rateRecipe", body).then(() => { + setShow(false) + props.setChange(true) + }) + } + + + var stars = [] + for(var i = 1; i < 6; i++) { + stars.push() + } + if(show) { + return + {stars} + + + } + else { + return + Thank you for rating! + + } + +} + +const Star = (props) => { + if(props.index <= props.rating) { + return { + props.set(props.index) + }}> + } + else { + return { + props.set(props.index) + }}> + } +} + +export default RateRecipe \ No newline at end of file diff --git a/Code/frontend/src/components/RecipeList.js b/Code/frontend/src/components/RecipeList.js index b6f5eaa3..09a92c3e 100644 --- a/Code/frontend/src/components/RecipeList.js +++ b/Code/frontend/src/components/RecipeList.js @@ -2,9 +2,10 @@ import React, { useState } from "react"; import { Avatar, Flex, Modal, ModalBody, ModalCloseButton, ModalOverlay, ModalHeader, ModalFooter, ModalContent, Box, SimpleGrid, Text, Button } from "@chakra-ui/react" import RecipeCard from "./RecipeCard"; import Rating from "./Rating"; +import RateRecipe from "./RateRecipe"; // component to handle all the recipes -const RecipeList = ({ recipes }) => { +const RecipeList = ({ recipes, refresh, searchName }) => { // mapping each recipe item to the Recipe container // const renderedRecipes = recipes.map((recipe) => { // // return ; @@ -15,6 +16,7 @@ const RecipeList = ({ recipes }) => { console.log(recipes) const [isOpen, setIsOpen] = useState(false); const [currentRecipe, setCurrentRecipe] = useState({}); + const [isChange, setIsChange] = useState(false); var youtube_videos = "https://www.youtube.com/results?search_query=" + currentRecipe["TranslatedRecipeName"]; @@ -25,6 +27,11 @@ const RecipeList = ({ recipes }) => { } const onClose = () => { setIsOpen(false) + setCurrentRecipe({}) + if(isChange) { + refresh(searchName) + } + } // all the recipes are being returned in the form of a table return ( @@ -55,9 +62,11 @@ const RecipeList = ({ recipes }) => { Instructions: {currentRecipe["TranslatedInstructions"]} Video Url: Youtube + + diff --git a/Code/frontend/src/components/componentImages/Filled_star_to_rate.png b/Code/frontend/src/components/componentImages/Filled_star_to_rate.png new file mode 100644 index 0000000000000000000000000000000000000000..b2161cda55ad2bef3bfcca06c2911f159557e09f GIT binary patch literal 8232 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 SyRp@MfYvj8wQ3be#Qy+(X_Q?6 literal 0 HcmV?d00001 From 8bc607079f8a8f9a736475e27a4db38fd5873839 Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Sat, 26 Oct 2024 17:01:02 -0400 Subject: [PATCH 09/43] Added initialization for test data prior to tests being run --- Code/backend/package.json | 3 +- Code/backend/test_resources/setup.js | 24 +++++++++++++++ Code/backend/test_resources/teardown.js | 20 ++++++++++++ Code/backend/test_resources/testRecipes.json | 32 ++++++++++++++++++++ Code/backend/test_resources/testUsers.json | 8 +++++ 5 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 Code/backend/test_resources/setup.js create mode 100644 Code/backend/test_resources/teardown.js create mode 100644 Code/backend/test_resources/testRecipes.json create mode 100644 Code/backend/test_resources/testUsers.json diff --git a/Code/backend/package.json b/Code/backend/package.json index 647cbad1..d9c5e491 100644 --- a/Code/backend/package.json +++ b/Code/backend/package.json @@ -38,6 +38,7 @@ "/node_modules/", "package.json", "package-lock.json" - ] + ], + "globalSetup": "./test_resources/setup.js" } } diff --git a/Code/backend/test_resources/setup.js b/Code/backend/test_resources/setup.js new file mode 100644 index 00000000..c216d755 --- /dev/null +++ b/Code/backend/test_resources/setup.js @@ -0,0 +1,24 @@ +import mongodb from "mongodb" +const MongoClient = mongodb.MongoClient; +import recipes from "./testRecipes.json" assert {type: 'json'} +import users from "./testUsers.json" assert {type: 'json'} +import dotenv from 'dotenv' +dotenv.config() + +export default async function (globalConfig, projectConfig) { + const uri = process.env.RECIPES_DB_URI; + console.log(uri) + var mongoClient = MongoClient.connect(uri, { + useNewUrlParser: true, + maxPoolSize: 50, + wtimeoutMS: 2500, + }).then(async (client) => { + const recipeCollection = await client.db(process.env.RECIPES_NS).collection("recipe")//.then(async (recipeCollection) => { + await recipeCollection.deleteMany({}) + await recipeCollection.insertMany(recipes.recipes) + const userCollection = await client.db(process.env.RECIPES_NS).collection("user")//.then(async (recipeCollection) => { + await userCollection.deleteMany({}) + await userCollection.insertMany(users.users) + client.close() + }); +}; \ No newline at end of file diff --git a/Code/backend/test_resources/teardown.js b/Code/backend/test_resources/teardown.js new file mode 100644 index 00000000..6ba2e1d8 --- /dev/null +++ b/Code/backend/test_resources/teardown.js @@ -0,0 +1,20 @@ +import mongodb from "mongodb" +const MongoClient = mongodb.MongoClient; +import dotenv from 'dotenv' +dotenv.config() + +export default async function (globalConfig, projectConfig) { + const uri = process.env.RECIPES_DB_URI; + console.log(uri) + var mongoClient = MongoClient.connect(uri, { + useNewUrlParser: true, + maxPoolSize: 50, + wtimeoutMS: 2500, + }).then(async (client) => { + const recipeCollection = await client.db(process.env.RECIPES_NS).collection("recipe")//.then(async (recipeCollection) => { + await recipeCollection.deleteMany({}) + const userCollection = await client.db(process.env.RECIPES_NS).collection("user")//.then(async (recipeCollection) => { + await userCollection.deleteMany({}) + client.close() + }); +}; \ No newline at end of file diff --git a/Code/backend/test_resources/testRecipes.json b/Code/backend/test_resources/testRecipes.json new file mode 100644 index 00000000..e9a76269 --- /dev/null +++ b/Code/backend/test_resources/testRecipes.json @@ -0,0 +1,32 @@ +{ + "recipes": [ + { + "TranslatedRecipeName": "BLT", + "TotalTimeInMins": "15", + "Diet-type": "", + "Recipe-rating": 5, + "Times-rated": 1, + "Cuisine": "", + "image-url": "", + "URL": "", + "TranslatedInstructions": "Cook sandwich", + "Cleaned-Ingredients": "Bacon%Lettuce%Tomato%Bread%", + "Restaurant": "", + "Restaurant-Location": "" + }, + { + "TranslatedRecipeName": "Andhra", + "TotalTimeInMins": "20", + "Diet-type": "Vegetarian", + "Recipe-rating": 5, + "Times-rated": 1, + "Cuisine": "Indian", + "image-url": "", + "URL": "", + "TranslatedInstructions": "Cook the food", + "Cleaned-Ingredients": "Mango%Rice%", + "Restaurant": "", + "Restaurant-Location": "" + } + ] +} \ No newline at end of file diff --git a/Code/backend/test_resources/testUsers.json b/Code/backend/test_resources/testUsers.json new file mode 100644 index 00000000..c33780e2 --- /dev/null +++ b/Code/backend/test_resources/testUsers.json @@ -0,0 +1,8 @@ +{ + "users": [ + { + "userName": "Test", + "password": "admin" + } + ] +} \ No newline at end of file From da03335771273f61e528b9595e13455d679c6b20 Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Sat, 26 Oct 2024 22:00:55 -0400 Subject: [PATCH 10/43] Created API route to add a recipe to the meal plan --- Code/backend/__tests__/testMealPlan.js | 18 ++++++++++++++++++ Code/backend/api/recipes.controller.js | 9 +++++++++ Code/backend/api/recipes.route.js | 2 ++ Code/backend/dao/recipesDAO.js | 13 +++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 Code/backend/__tests__/testMealPlan.js diff --git a/Code/backend/__tests__/testMealPlan.js b/Code/backend/__tests__/testMealPlan.js new file mode 100644 index 00000000..4f6e6e7c --- /dev/null +++ b/Code/backend/__tests__/testMealPlan.js @@ -0,0 +1,18 @@ +const { ObjectId } = require("mongodb"); + +const request = require("supertest")("http://localhost:5000/api/v1/recipes"); +const expect = require("chai").expect; + +describe("Meal Plan", function() { + + describe("Put /mealPlan", function() { + it("Should correctly update the meal plan with new recipes", async function() { + const response0 = await request.get("/getRecipeByName?recipeName=BLT"); + const res0JSON = JSON.parse(response0.text) + expect(response0.status).to.eql(200); + const response = await request.put("/mealPlan") + .send({ recipeID: res0JSON["recipes"][0]['_id'], userName: "Test", weekDay: "monday" }); + expect(response.status).to.eql(200); + }) + }) +}) \ No newline at end of file diff --git a/Code/backend/api/recipes.controller.js b/Code/backend/api/recipes.controller.js index 446f4f99..45ea67b0 100644 --- a/Code/backend/api/recipes.controller.js +++ b/Code/backend/api/recipes.controller.js @@ -137,6 +137,15 @@ export default class RecipesController { res.status(500).json({ error: e }); } } + + static async apiAddtoPlan(req, res, next) { + try { + let response = await RecipesDAO.addRecipeToMealPlan(req.body.userName, req.body.recipeID, req.body.weekDay) + res.json(response) + }catch(e) { + res.status(500).json({ error: e }); + } + } } diff --git a/Code/backend/api/recipes.route.js b/Code/backend/api/recipes.route.js index 8b7919ac..a2383183 100644 --- a/Code/backend/api/recipes.route.js +++ b/Code/backend/api/recipes.route.js @@ -24,4 +24,6 @@ router.route("/getRecipeByName").get(RecipesCtrl.apiGetRecipeByName); router.route("/rateRecipe").patch(RecipesCtrl.apiPatchRecipeRating) +router.route("/mealPlan").put(RecipesCtrl.apiAddtoPlan) + export default router; diff --git a/Code/backend/dao/recipesDAO.js b/Code/backend/dao/recipesDAO.js index 40defe97..fde75744 100644 --- a/Code/backend/dao/recipesDAO.js +++ b/Code/backend/dao/recipesDAO.js @@ -271,6 +271,19 @@ export default class RecipesDAO { console.log(`Unable to add recipe, ${e}`) } } + + static async addRecipeToMealPlan(userName, recipeID, weekDay) { + let response; + try { + let updateBody = JSON.parse('{ "meal-plan.' + weekDay + '": "' + recipeID + '" }') + response = await users.updateOne( + { userName: userName }, + { $set: updateBody }) + return response + } catch (e) { + console.log(`Unable to add recipe to meal plan, ${e}`) + } + } static async getIngredients(){ let response = {}; From abb89e04b13bbfea19a80b7f0976bb0efcbb3ad7 Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Sat, 26 Oct 2024 22:11:59 -0400 Subject: [PATCH 11/43] Created API route to get the meal plan for a user --- Code/backend/__tests__/testMealPlan.js | 3 +++ Code/backend/api/recipes.controller.js | 9 +++++++++ Code/backend/api/recipes.route.js | 2 +- Code/backend/dao/recipesDAO.js | 25 +++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Code/backend/__tests__/testMealPlan.js b/Code/backend/__tests__/testMealPlan.js index 4f6e6e7c..e1c3276c 100644 --- a/Code/backend/__tests__/testMealPlan.js +++ b/Code/backend/__tests__/testMealPlan.js @@ -13,6 +13,9 @@ describe("Meal Plan", function() { const response = await request.put("/mealPlan") .send({ recipeID: res0JSON["recipes"][0]['_id'], userName: "Test", weekDay: "monday" }); expect(response.status).to.eql(200); + const response2 = await request.get("/mealPlan?userName=Test") + expect(response2.text.includes("monday")).true + expect(response2.text.includes(res0JSON["recipes"][0]['_id'])).true }) }) }) \ No newline at end of file diff --git a/Code/backend/api/recipes.controller.js b/Code/backend/api/recipes.controller.js index 45ea67b0..bc58039e 100644 --- a/Code/backend/api/recipes.controller.js +++ b/Code/backend/api/recipes.controller.js @@ -146,6 +146,15 @@ export default class RecipesController { res.status(500).json({ error: e }); } } + + static async apiGetMealPlan(req, res, next) { + try { + let response = await RecipesDAO.getMealPlan(req.query.userName) + res.json(response) + }catch(e) { + res.status(500).json({ error: e }); + } + } } diff --git a/Code/backend/api/recipes.route.js b/Code/backend/api/recipes.route.js index a2383183..1dbb5c8d 100644 --- a/Code/backend/api/recipes.route.js +++ b/Code/backend/api/recipes.route.js @@ -24,6 +24,6 @@ router.route("/getRecipeByName").get(RecipesCtrl.apiGetRecipeByName); router.route("/rateRecipe").patch(RecipesCtrl.apiPatchRecipeRating) -router.route("/mealPlan").put(RecipesCtrl.apiAddtoPlan) +router.route("/mealPlan").put(RecipesCtrl.apiAddtoPlan).get(RecipesCtrl.apiGetMealPlan) export default router; diff --git a/Code/backend/dao/recipesDAO.js b/Code/backend/dao/recipesDAO.js index fde75744..6d2dd08d 100644 --- a/Code/backend/dao/recipesDAO.js +++ b/Code/backend/dao/recipesDAO.js @@ -284,6 +284,31 @@ export default class RecipesDAO { console.log(`Unable to add recipe to meal plan, ${e}`) } } + + static async getMealPlan(userName) { + let cursor; + let mealPlanResponse = { + sunday: "", + monday: "", + tuesday: "", + wednesday: "", + thursday: "", + friday: "", + saturday: "" + } + try { + cursor = await users.findOne({ "userName": userName }); + if (cursor.userName) { + let plan = cursor['meal-plan'] ? cursor['meal-plan'] : {} + mealPlanResponse = {...mealPlanResponse, ...plan} + return mealPlanResponse + } else { + throw new Error(`Cannot find user with name ${userName}`); + } + } catch (e) { + console.log(`error: ${e}`) + } + } static async getIngredients(){ let response = {}; From dc85e7d959b180bb5329593b5ac96002ea77794d Mon Sep 17 00:00:00 2001 From: Anuraag Jajoo Date: Sun, 27 Oct 2024 02:06:54 -0400 Subject: [PATCH 12/43] Made UI Changes, need to fix Remove Bookmark --- Code/backend/api/recipes.controller.js | 75 ++++---- Code/backend/api/recipes.route.js | 4 +- Code/backend/dao/recipesDAO.js | 169 +++++++++++------ Code/backend/sample.env | 4 - Code/frontend/src/App.js | 72 +++++++- .../src/components/BookMarksRecipeCard.js | 158 ++++++++++++---- .../src/components/BookMarksRecipeList.js | 124 +++++++++---- Code/frontend/src/components/Form.js | 172 ++++++++++++++---- Code/frontend/src/components/RecipeCard.js | 167 ++++++++++++----- Code/frontend/src/components/RecipeList.js | 164 +++++++++++++---- .../frontend/src/components/SearchByRecipe.js | 70 +++---- 11 files changed, 863 insertions(+), 316 deletions(-) delete mode 100644 Code/backend/sample.env diff --git a/Code/backend/api/recipes.controller.js b/Code/backend/api/recipes.controller.js index 446f4f99..083ed66c 100644 --- a/Code/backend/api/recipes.controller.js +++ b/Code/backend/api/recipes.controller.js @@ -2,56 +2,67 @@ import RecipesDAO from "../dao/recipesDAO.js"; export default class RecipesController { static async apiAuthLogin(req, res) { - let filters = {} - filters.userName = req.query.userName - filters.password = req.query.password + let filters = {}; + filters.userName = req.query.userName; + filters.password = req.query.password; const { success, user } = await RecipesDAO.getUser({ - filters - }) + filters, + }); res.json({ success, user }); } static async apiAuthSignup(req, res) { if (req.body) { - let data = {} - data.userName = req.body.userName - data.password = req.body.password + let data = {}; + data.userName = req.body.userName; + data.password = req.body.password; const { success, user } = await RecipesDAO.addUser({ - data - }) + data, + }); res.json({ success, user }); } } static async apiGetBookmarks(req, res) { if (req.query.userName) { - const bookmarks = await RecipesDAO.getBookmarks(req.query.userName) - console.log(bookmarks) + const bookmarks = await RecipesDAO.getBookmarks(req.query.userName); + console.log(bookmarks); res.json({ bookmarks }); } else { - res.json("Username not given") + res.json("Username not given"); } } static async apiPostRecipeToProfile(req, res) { - if (req.body) { - const { userName, recipe } = req.body; - try { - const response = RecipesDAO.addRecipeToProfile(userName, recipe) - res.json(response) - } catch (e) { - console.log(`error: ${e}`) - } + console.log("Received request to add recipe to profile"); + console.log("Request body:", JSON.stringify(req.body, null, 2)); - } else { - res.json({ success: false }) + const { userName, recipe } = req.body; + + if (!userName || !recipe) { + console.log("Missing userName or recipe in request"); + return res + .status(400) + .json({ success: false, message: "Missing userName or recipe" }); } + try { + const result = await RecipesDAO.addRecipeToProfile(userName, recipe); + console.log("Result of adding recipe:", result); + res.json(result); + } catch (e) { + console.error("Error in apiPostRecipeToProfile:", e); + res.status(500).json({ + success: false, + message: "Internal server error", + error: e.message, + }); + } } - + static async apiGetRecipeByName(req, res) { let filters = {}; //Checking the query to find the required results - console.log(req.query) + console.log(req.query); if (req.query.recipeName) { filters.recipeName = req.query.recipeName; } @@ -61,7 +72,7 @@ export default class RecipesController { }); let response = { - recipes: recipesList + recipes: recipesList, }; res.json(response); } @@ -112,7 +123,7 @@ export default class RecipesController { try { let response = await RecipesDAO.addRecipe(req.body); res.json(response); - } catch(e) { + } catch (e) { console.log(`api, ${e}`); res.status(500).json({ error: e }); } @@ -120,9 +131,9 @@ export default class RecipesController { static async apiPatchRecipeRating(req, res, next) { try { - console.log(req.body) - let response = await RecipesDAO.rateRecipe(req.body) - res.json(response) + console.log(req.body); + let response = await RecipesDAO.rateRecipe(req.body); + res.json(response); } catch (e) { console.log(`api, ${e}`); res.status(500).json({ error: e }); @@ -133,10 +144,8 @@ export default class RecipesController { try { let ingredients = await RecipesDAO.getIngredients(); res.json(ingredients); - } catch(e) { + } catch (e) { res.status(500).json({ error: e }); } } } - - diff --git a/Code/backend/api/recipes.route.js b/Code/backend/api/recipes.route.js index 8b7919ac..29f3febc 100644 --- a/Code/backend/api/recipes.route.js +++ b/Code/backend/api/recipes.route.js @@ -10,7 +10,7 @@ router.route("/cuisines").get(RecipesCtrl.apiGetRecipeCuisines); router.route("/addRecipe").post(RecipesCtrl.apiPostRecipe); -router.route('/callIngredients').get(RecipesCtrl.apiGetIngredients); +router.route("/callIngredients").get(RecipesCtrl.apiGetIngredients); router.route("/signup").post(RecipesCtrl.apiAuthSignup); @@ -22,6 +22,6 @@ router.route("/addRecipeToProfile").post(RecipesCtrl.apiPostRecipeToProfile); router.route("/getRecipeByName").get(RecipesCtrl.apiGetRecipeByName); -router.route("/rateRecipe").patch(RecipesCtrl.apiPatchRecipeRating) +router.route("/rateRecipe").patch(RecipesCtrl.apiPatchRecipeRating); export default router; diff --git a/Code/backend/dao/recipesDAO.js b/Code/backend/dao/recipesDAO.js index 40defe97..b9f7c1a1 100644 --- a/Code/backend/dao/recipesDAO.js +++ b/Code/backend/dao/recipesDAO.js @@ -16,7 +16,9 @@ export default class RecipesDAO { } try { recipes = await conn.db(process.env.RECIPES_NS).collection("recipe"); - ingredients = await conn.db(process.env.RECIPES_NS).collection("ingredient_list"); + ingredients = await conn + .db(process.env.RECIPES_NS) + .collection("ingredient_list"); users = await conn.db(process.env.RECIPES_NS).collection("user"); } catch (e) { console.error( @@ -29,17 +31,17 @@ export default class RecipesDAO { let query; let cursor; let user; - query = { "userName": filters.userName } + query = { userName: filters.userName }; if (filters) { cursor = await users.findOne(query); if (cursor.userName) { if (cursor.password == filters.password) { - return { success: true, user: cursor } + return { success: true, user: cursor }; } else { - return { success: false } + return { success: false }; } } else { - return { success: false } + return { success: false }; } } } @@ -48,36 +50,36 @@ export default class RecipesDAO { let query; let cursor; let user; - query = { "userName": data.userName } - console.log(query) + query = { userName: data.userName }; + console.log(query); if (data) { cursor = await users.findOne(query); - console.log(cursor) - if (cursor!==null) { - return {success: false} + console.log(cursor); + if (cursor !== null) { + return { success: false }; } else { - const res = await users.insertOne(data) - return { success: true } + const res = await users.insertOne(data); + return { success: true }; } } } - + //function to get bookmarks static async getBookmarks(userName) { let query; let cursor; let user; - query = { "userName": userName } - console.log(query) + query = { userName: userName }; + console.log(query); try { cursor = await users.findOne(query); if (cursor.userName) { return cursor.bookmarks; } else { - return { bookmarks: [] } + return { bookmarks: [] }; } } catch (e) { - console.log(`error: ${e}`) + console.log(`error: ${e}`); } } @@ -87,23 +89,25 @@ export default class RecipesDAO { if (filters) { if ("recipeName" in filters) { const words = filters["recipeName"].split(" "); - const regexPattern = words.map(word => `(?=.*\\b${word}\\b)`).join(''); + const regexPattern = words + .map((word) => `(?=.*\\b${word}\\b)`) + .join(""); const regex = new RegExp(regexPattern, "i"); - query = { "TranslatedRecipeName": { $regex: regex } }; + query = { TranslatedRecipeName: { $regex: regex } }; // query["Cuisine"] = "Indian"; } let recipesList; try { recipesList = await recipes .find(query) - .collation({ locale: "en", strength: 2 }).toArray(); - return { recipesList } + .collation({ locale: "en", strength: 2 }) + .toArray(); + return { recipesList }; } catch (e) { console.error(`Unable to issue find command, ${e}`); return { recipesList: [], totalNumRecipess: 0 }; } } - } //Function to get the Recipe List @@ -215,7 +219,7 @@ export default class RecipesDAO { inputRecipe["TotalTimeInMins"] = recipe["cookingTime"]; inputRecipe["Diet-type"] = recipe["dietType"]; inputRecipe["Recipe-rating"] = recipe["recipeRating"]; - inputRecipe["Times-rated"] = 1 + inputRecipe["Times-rated"] = 1; inputRecipe["Cuisine"] = recipe["cuisine"]; inputRecipe["image-url"] = recipe["imageURL"]; inputRecipe["URL"] = recipe["recipeURL"]; @@ -236,50 +240,111 @@ export default class RecipesDAO { console.log("Input Recipe"); console.log(inputRecipe); let response = {}; - try{ + try { response = await recipes.insertOne(inputRecipe); return response; - } catch(e){ + } catch (e) { console.error(`Unable to add recipe, ${e}`); return response; } } static async rateRecipe(ratingBody) { - let r = await recipes.find({_id: new ObjectId(ratingBody.recipeID)}).collation({ locale: "en", strength: 2 }).toArray(); - let recipe = r[0] - let timesRated = recipe["Times-rated"] ? Number(recipe["Times-rated"]) : 1 - let newRating = Number(recipe["Recipe-rating"]) * timesRated - newRating += ratingBody.rating - timesRated++ - newRating /= timesRated - await recipes.updateOne({_id: new ObjectId(ratingBody.recipeID)}, {$set: {'Times-rated': timesRated, 'Recipe-rating': newRating}}) + let r = await recipes + .find({ _id: new ObjectId(ratingBody.recipeID) }) + .collation({ locale: "en", strength: 2 }) + .toArray(); + let recipe = r[0]; + let timesRated = recipe["Times-rated"] ? Number(recipe["Times-rated"]) : 1; + let newRating = Number(recipe["Recipe-rating"]) * timesRated; + newRating += ratingBody.rating; + timesRated++; + newRating /= timesRated; + await recipes.updateOne( + { _id: new ObjectId(ratingBody.recipeID) }, + { $set: { "Times-rated": timesRated, "Recipe-rating": newRating } } + ); } - //function to add recipe to user profile - static async addRecipeToProfile(userName, recipe) { - let response; - console.log(userName) - try { - response = await users.updateOne( - { userName: userName }, - { $push: { bookmarks: recipe } } - ) - console.log(response) - return response; - } catch (e) { - console.log(`Unable to add recipe, ${e}`) + //function to add recipe to user profile + static async addRecipeToProfile(userName, recipe) { + try { + console.log(`Attempting to add recipe to profile for user: ${userName}`); + + // First, check if the recipe already exists in the user's bookmarks + const user = await users.findOne({ userName: userName }); + if (!user) { + return { success: false, message: "User not found" }; } + + const existingBookmark = user.bookmarks.find( + (bookmark) => bookmark._id.toString() === recipe._id.toString() + ); + if (existingBookmark) { + console.log("Recipe already bookmarked"); + return { success: false, message: "Recipe already bookmarked" }; + } + + // If the recipe doesn't exist, add it to the bookmarks + const updateResult = await users.updateOne( + { userName: userName }, + { $addToSet: { bookmarks: recipe } } + ); + + console.log("Update result:", updateResult); + + if (updateResult.modifiedCount === 0) { + console.log("No changes made to bookmarks"); + return { success: false, message: "No changes made to bookmarks" }; + } + + console.log("Recipe added to bookmarks successfully"); + return { + success: true, + message: "Recipe added to bookmarks successfully", + }; + } catch (e) { + console.error(`Error in addRecipeToProfile: ${e}`); + throw e; + } + } + + static async removeBookmark(userName, recipeId) { + try { + console.log("DAO: Removing bookmark for:", { userName, recipeId }); + const updateResponse = await this.users.updateOne( + { userName: userName }, + { $pull: { savedRecipes: { _id: recipeId } } } + ); + console.log("DAO: Update response:", updateResponse); + + if (updateResponse.modifiedCount === 1) { + console.log("DAO: Bookmark removed successfully"); + return { success: true, message: "Bookmark removed successfully" }; + } else if (updateResponse.matchedCount === 0) { + console.log("DAO: User not found"); + return { success: false, message: "User not found" }; + } else { + console.log("DAO: Bookmark not found or already removed"); + return { + success: false, + message: "Bookmark not found or already removed", + }; + } + } catch (e) { + console.error(`DAO: Unable to remove bookmark:`, e); + throw e; } - - static async getIngredients(){ + } + + static async getIngredients() { let response = {}; - try{ - response = await ingredients.distinct('item_name'); + try { + response = await ingredients.distinct("item_name"); return response; - }catch(e){ + } catch (e) { console.error(`Unable to get ingredients, ${e}`); return response; } - } + } } diff --git a/Code/backend/sample.env b/Code/backend/sample.env deleted file mode 100644 index 2d38ca06..00000000 --- a/Code/backend/sample.env +++ /dev/null @@ -1,4 +0,0 @@ -RECIPES_DB_URI=[MongoDB Cluster URL] -RECIPES_NS=recipe_recommender -PORT=5000 -GMAIL= [unityID]@ncsu.edu \ No newline at end of file diff --git a/Code/frontend/src/App.js b/Code/frontend/src/App.js index a746e346..ed48e555 100644 --- a/Code/frontend/src/App.js +++ b/Code/frontend/src/App.js @@ -12,6 +12,7 @@ import SearchByRecipe from "./components/SearchByRecipe.js"; import Login from "./components/Login.js"; import UserProfile from "./components/UserProfile.js"; import LandingPage from "./components/LandingPage.js"; +import BookMarksRecipeList from "./components/BookMarksRecipeList"; // Import BookMarksRecipeList // Main component of the project class App extends Component { @@ -31,7 +32,9 @@ class App extends Component { isLoading: false, isLoggedIn: false, isProfileView: false, - userData: {}, + userData: { + bookmarks: [], + }, }; } @@ -166,6 +169,59 @@ class App extends Component { }); }; + handleBookMarks = async () => { + // Fetch bookmarks when navigating to profile view + const userName = localStorage.getItem("userName"); + try { + const response = await recipeDB.get("/recipes/getBookmarks", { + params: { userName }, + }); + this.setState({ + isProfileView: true, + userData: { + ...this.state.userData, + bookmarks: response.data.recipes, // Set fetched bookmarks to state + }, + }); + } catch (err) { + console.error("Error fetching bookmarks", err); + } + }; + + handleRemoveBookmark = async (recipeId) => { + const userName = localStorage.getItem("userName"); + + try { + const response = await recipeDB.post("/recipes/removeBookmark", { + userName, + recipeId, + }); + + if (response.data.success) { + // Update the bookmarks in the state + this.setState((prevState) => ({ + userData: { + ...prevState.userData, + bookmarks: prevState.userData.bookmarks.filter( + (recipe) => recipe.id !== recipeId // Remove based on recipeId + ), + }, + })); + } else { + throw new Error(response.data.message || "Failed to remove bookmark"); + } + } catch (error) { + console.error("Failed to remove bookmark:", error); + // You might want to show a toast or some other error message to the user here + } + }; + + handleProfileView = () => { + this.setState({ + isProfileView: false, + }); + }; + render() { return (

@@ -173,7 +229,7 @@ class App extends Component { handleLogout={this.handleLogout} handleBookMarks={this.handleBookMarks} user={this.state.isLoggedIn ? this.state.userData : null} - onLoginClick={() => this.setState({ isLoggedIn: false })} // To show the login page/modal + onLoginClick={() => this.setState({ isLoggedIn: false })} /> {this.state.isLoggedIn ? ( <> @@ -181,7 +237,13 @@ class App extends Component { + > + {/* Add BookMarksRecipeList to render bookmarks */} + + ) : ( @@ -229,10 +291,6 @@ class App extends Component { )} )} - {/* handleSubmit function is being sent as a prop to the form component*/} - - {/* RecipeList is the component where results are displayed. - App's recipeList state item is being sent as a prop */}
); } diff --git a/Code/frontend/src/components/BookMarksRecipeCard.js b/Code/frontend/src/components/BookMarksRecipeCard.js index 54f8b348..98b922e1 100644 --- a/Code/frontend/src/components/BookMarksRecipeCard.js +++ b/Code/frontend/src/components/BookMarksRecipeCard.js @@ -1,40 +1,128 @@ -/* MIT License - -Copyright (c) 2023 Pannaga Rao, Harshitha, Prathima, Karthik */ - import React from "react"; -import { Box, HStack, SimpleGrid, Card, CardHeader, Heading, Text, CardBody, CardFooter, Button, Image, Tag } from "@chakra-ui/react" +import { + Card, + CardHeader, + Heading, + Text, + CardBody, + Image, + Tag, + useToast, +} from "@chakra-ui/react"; import recipeDB from "../apis/recipeDB"; - const BookMarksRecipeCard = (props) => { - const handleClick = ()=> { - props.handler(props.recipe); + const toast = useToast(); + + const handleClick = () => { + // This will be called only when the name is clicked + props.handler(props.recipe); + }; + + const handleRemove = async () => { + const userName = localStorage.getItem("userName"); + if (!userName) { + console.error("Username not found in localStorage"); + // Show error toast + return; } - - return ( - <> - - - {props.recipe.TranslatedRecipeName} - - - Cooking Time: {props.recipe.TotalTimeInMins} mins - Rating: {props.recipe['Recipe-rating']} - Diet Type: {props.recipe['Diet-type']} - - - - - ) -} - -export default BookMarksRecipeCard; \ No newline at end of file + + const recipeId = props.recipe._id || props.recipe.id; + + console.log("Attempting to remove bookmark:", { userName, recipeId }); + + try { + const response = await recipeDB.post("/recipes/removeBookmark", { + userName, + recipeId, + }); + + console.log("Remove bookmark response:", response.data); + + if (response.data.success) { + // Show success toast + toast({ + title: "Success", + description: "Bookmark removed successfully", + status: "success", + duration: 3000, + isClosable: true, + }); + if (typeof props.onRemove === "function") { + props.onRemove(recipeId); + } + } else { + throw new Error(response.data.message || "Failed to remove bookmark"); + } + } catch (error) { + console.error("Error removing bookmark:", error); + toast({ + title: "Error", + description: error.message || "Failed to remove bookmark", + status: "error", + duration: 3000, + isClosable: true, + }); + } + }; + + return ( + + + + {props.recipe.TranslatedRecipeName} + + + + + + Cooking Time: {props.recipe.TotalTimeInMins} mins + + + Rating: {props.recipe["Recipe-rating"]} + + + Diet Type: {props.recipe["Diet-type"]} + + + Remove Bookmark + + + + ); +}; + +export default BookMarksRecipeCard; diff --git a/Code/frontend/src/components/BookMarksRecipeList.js b/Code/frontend/src/components/BookMarksRecipeList.js index 6682855b..9093d03f 100644 --- a/Code/frontend/src/components/BookMarksRecipeList.js +++ b/Code/frontend/src/components/BookMarksRecipeList.js @@ -3,60 +3,113 @@ Copyright (c) 2023 Pannaga Rao, Harshitha, Prathima, Karthik */ import React, { useState } from "react"; -import { Avatar, Flex, Modal, ModalBody, ModalCloseButton, ModalOverlay, ModalHeader, ModalFooter, ModalContent, Box, SimpleGrid, Text, Button } from "@chakra-ui/react" +import { + Avatar, + Flex, + Modal, + ModalBody, + ModalCloseButton, + ModalOverlay, + ModalHeader, + ModalFooter, + ModalContent, + Box, + SimpleGrid, + Text, + Button, +} from "@chakra-ui/react"; import BookMarksRecipeCard from "./BookMarksRecipeCard"; -// component to handle all the recipes -const BookMarksRecipeList = ({ recipes }) => { - // mapping each recipe item to the Recipe container - // const renderedRecipes = recipes.map((recipe) => { - // // return ; - // return( - - // ) - // }); - console.log(recipes) +const BookMarksRecipeList = ({ recipes, onRemove }) => { const [isOpen, setIsOpen] = useState(false); const [currentRecipe, setCurrentRecipe] = useState({}); - var youtube_videos = - "https://www.youtube.com/results?search_query=" + - currentRecipe["TranslatedRecipeName"]; + const youtubeVideosURL = `https://www.youtube.com/results?search_query=${currentRecipe["TranslatedRecipeName"]}`; + const handleViewRecipe = (data) => { - setIsOpen(true) - console.log(data) + setIsOpen(true); setCurrentRecipe(data); - } + }; + const onClose = () => { - setIsOpen(false) - } - // all the recipes are being returned in the form of a table + setIsOpen(false); + }; + return ( <> - - - {recipes.length !==0 ? recipes.map((recipe) => ( - - )) : Searching for a recipe?} + + + {recipes.length !== 0 ? ( + recipes.map((recipe) => ( + onRemove(recipe.id)} // Pass the onRemove function with the recipe id + /> + )) + ) : ( + + No bookmarks available. + + )} - + {currentRecipe.TranslatedRecipeName} - - + + - Cooking Time: {currentRecipe.TotalTimeInMins} mins - Rating: {currentRecipe['Recipe-rating']} - Diet Type: {currentRecipe['Diet-type']} + + Cooking Time: + {currentRecipe.TotalTimeInMins} mins + + + Rating: {" "} + {currentRecipe["Recipe-rating"]} + + + Diet Type: {currentRecipe["Diet-type"]} + - Instructions: {currentRecipe["TranslatedInstructions"]} - Video Url: Youtube + + Instructions: {" "} + {currentRecipe["TranslatedInstructions"]} + + + + Video URL:{" "} + + + Youtube + + - + - - {this.printHander()} - + {this.printHander()} {/* */} - + {/* */} @@ -210,17 +294,33 @@ class Form extends Component { */} - - - - - Enable email alert? - - - - - + + + + + Enable email alert? + + + + {/*
diff --git a/Code/frontend/src/components/RecipeCard.js b/Code/frontend/src/components/RecipeCard.js index 090d821e..dfd9dad6 100644 --- a/Code/frontend/src/components/RecipeCard.js +++ b/Code/frontend/src/components/RecipeCard.js @@ -1,54 +1,127 @@ import React from "react"; -import { Box, SimpleGrid, Card, CardHeader, Heading, Text, CardBody, CardFooter, Button, Image, Tag } from "@chakra-ui/react" +import { + Box, + SimpleGrid, + Card, + CardHeader, + Heading, + Text, + CardBody, + CardFooter, + Button, + Image, + Tag, + useToast, // For displaying notifications +} from "@chakra-ui/react"; import recipeDB from "../apis/recipeDB"; import Rating from "./Rating"; - const RecipeCard = (props) => { - const handleClick = ()=> { - props.handler(props.recipe); + const toast = useToast(); + + const handleClick = () => { + props.handler(props.recipe); + }; + + const handleSave = async () => { + const userName = localStorage.getItem("userName"); + if (!userName) { + console.error("No user logged in"); + // Show an error message to the user + return; } - const handleSave = ()=> { - console.log("saved") - var userName = localStorage.getItem("userName") - try { - const response = recipeDB.post("/recipes/addRecipeToProfile", { - "userName": userName, - "recipe": props.recipe - }) - alert("Recipe saved to your profile!") - } catch(e) { - console.log(`Error adding recipe to user-${userName}`) - console.log("caught exception") - } + + try { + console.log("Attempting to save recipe:", props.recipe); + console.log("User:", userName); + + const response = await recipeDB.post("/recipes/addRecipeToProfile", { + userName, + recipe: props.recipe, + }); + + console.log("Save recipe response:", response.data); + // Handle successful save + } catch (error) { + console.error("Error saving recipe:", error); + + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + console.error("Error data:", error.response.data); + console.error("Error status:", error.response.status); + console.error("Error headers:", error.response.headers); + } else if (error.request) { + // The request was made but no response was received + console.error("No response received:", error.request); + } else { + // Something happened in setting up the request that triggered an Error + console.error("Error message:", error.message); + } + + // Show an error message to the user } - return ( - <> - - - {props.recipe.TranslatedRecipeName} - - - Cooking Time: {props.recipe.TotalTimeInMins} mins - - Rating: - - - Diet Type: {props.recipe['Diet-type']} - save recipe - - - - - ) -} - -export default RecipeCard; \ No newline at end of file + }; + + return ( + + + + {props.recipe.TranslatedRecipeName} + + + + + + Rating: + + + + Cooking Time: {props.recipe.TotalTimeInMins} mins + + + Rating: {props.recipe["Recipe-rating"]} + + + Diet Type: {props.recipe["Diet-type"]} + + + Save Recipe + + + + ); +}; + +export default RecipeCard; diff --git a/Code/frontend/src/components/RecipeList.js b/Code/frontend/src/components/RecipeList.js index 09a92c3e..821e6c87 100644 --- a/Code/frontend/src/components/RecipeList.js +++ b/Code/frontend/src/components/RecipeList.js @@ -1,5 +1,19 @@ import React, { useState } from "react"; -import { Avatar, Flex, Modal, ModalBody, ModalCloseButton, ModalOverlay, ModalHeader, ModalFooter, ModalContent, Box, SimpleGrid, Text, Button } from "@chakra-ui/react" +import { + Avatar, + Flex, + Modal, + ModalBody, + ModalCloseButton, + ModalOverlay, + ModalHeader, + ModalFooter, + ModalContent, + Box, + SimpleGrid, + Text, + Button, +} from "@chakra-ui/react"; import RecipeCard from "./RecipeCard"; import Rating from "./Rating"; import RateRecipe from "./RateRecipe"; @@ -13,60 +27,144 @@ const RecipeList = ({ recipes, refresh, searchName }) => { // ) // }); - console.log(recipes) + console.log(recipes); const [isOpen, setIsOpen] = useState(false); const [currentRecipe, setCurrentRecipe] = useState({}); + const [isChange, setIsChange] = useState(false); var youtube_videos = "https://www.youtube.com/results?search_query=" + currentRecipe["TranslatedRecipeName"]; const handleViewRecipe = (data) => { - setIsOpen(true) - console.log(data) + setIsOpen(true); setCurrentRecipe(data); - } + }; + const onClose = () => { - setIsOpen(false) - setCurrentRecipe({}) - if(isChange) { - refresh(searchName) + setIsOpen(false); + setIsOpen(false); + setCurrentRecipe({}); + if (isChange) { + refresh(searchName); } - - } + }; // all the recipes are being returned in the form of a table return ( <> - - - {recipes.length !==0 ? recipes.map((recipe) => ( - - )) : Searching for a recipe?} + + + Recipe Collection + + + {recipes.length !== 0 ? ( + recipes.map((recipe) => ( + + )) + ) : ( + + Searching for a recipe? + + )} - - {currentRecipe.TranslatedRecipeName} + + + {currentRecipe.TranslatedRecipeName || "Recipe Details"} + - - - - Cooking Time: {currentRecipe.TotalTimeInMins} mins - - Rating: - - - Diet Type: {currentRecipe['Diet-type']} + + + + + Cooking Time: {currentRecipe.TotalTimeInMins} mins + + + Rating: {currentRecipe["Recipe-rating"]} + + + Diet Type: {currentRecipe["Diet-type"]} + + + + + + Cooking Time: + {currentRecipe.TotalTimeInMins} mins + + + Rating: + + + + Diet Type: {" "} + {currentRecipe["Diet-type"]} + + + + + Instructions:{" "} + {currentRecipe["TranslatedInstructions"]} + + + + Video URL: + + + + YouTube + + + - Instructions: {currentRecipe["TranslatedInstructions"]} - Video Url: Youtube - - - + @@ -74,6 +172,6 @@ const RecipeList = ({ recipes, refresh, searchName }) => { - ) + ); }; export default RecipeList; diff --git a/Code/frontend/src/components/SearchByRecipe.js b/Code/frontend/src/components/SearchByRecipe.js index 16896776..50f5c6f9 100644 --- a/Code/frontend/src/components/SearchByRecipe.js +++ b/Code/frontend/src/components/SearchByRecipe.js @@ -2,40 +2,46 @@ Copyright (c) 2023 Pannaga Rao, Harshitha, Prathima, Karthik */ -import { Box, Input, InputGroup, InputRightElement, Button } from "@chakra-ui/react"; +import { + Box, + Input, + InputGroup, + InputRightElement, + Button, +} from "@chakra-ui/react"; import { useState } from "react"; import recipeDB from "../apis/recipeDB"; const SearchByRecipe = (props) => { - const [recipeName, setRecipeName] = useState(""); - const [recipes, setRecipes] = useState([]) - const handleNameChange = (e) => { - e.preventDefault(); - setRecipeName(e.target.value) - } - const handleSearchByRecipeClick = (e) => { - e.preventDefault(); - // console.log(recipeName) - props.sendRecipeData(recipeName) - } - return ( - <> - - - - - - - - - - ) -} + const [recipeName, setRecipeName] = useState(""); + const [recipes, setRecipes] = useState([]); + const handleNameChange = (e) => { + e.preventDefault(); + setRecipeName(e.target.value); + }; + const handleSearchByRecipeClick = (e) => { + e.preventDefault(); + // console.log(recipeName) + props.sendRecipeData(recipeName); + }; + return ( + <> + + + + + + + + + + ); +}; -export default SearchByRecipe; \ No newline at end of file +export default SearchByRecipe; From ffdfd667877792332a2fe3ba3de3e494767cd1e5 Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Sun, 27 Oct 2024 20:32:59 -0400 Subject: [PATCH 13/43] Fixed errors with removing and adding bookmarks --- Code/backend/api/recipes.controller.js | 10 ++++++++++ Code/backend/api/recipes.route.js | 2 ++ Code/backend/dao/recipesDAO.js | 8 ++++---- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Code/backend/api/recipes.controller.js b/Code/backend/api/recipes.controller.js index 083ed66c..e0f287e2 100644 --- a/Code/backend/api/recipes.controller.js +++ b/Code/backend/api/recipes.controller.js @@ -59,6 +59,16 @@ export default class RecipesController { } } + static async apiRemoveRecipeFromProfile(req, res) { + const { userName, recipeId } = req.body; + try { + const result = await RecipesDAO.removeBookmark(userName, recipeId) + res.json(result) + } catch (e) { + res.status(500).json({error: e}) + } + } + static async apiGetRecipeByName(req, res) { let filters = {}; //Checking the query to find the required results diff --git a/Code/backend/api/recipes.route.js b/Code/backend/api/recipes.route.js index 29f3febc..cff1f7b3 100644 --- a/Code/backend/api/recipes.route.js +++ b/Code/backend/api/recipes.route.js @@ -24,4 +24,6 @@ router.route("/getRecipeByName").get(RecipesCtrl.apiGetRecipeByName); router.route("/rateRecipe").patch(RecipesCtrl.apiPatchRecipeRating); +router.route("/removeBookmark").post(RecipesCtrl.apiRemoveRecipeFromProfile); + export default router; diff --git a/Code/backend/dao/recipesDAO.js b/Code/backend/dao/recipesDAO.js index b9f7c1a1..f8303c5e 100644 --- a/Code/backend/dao/recipesDAO.js +++ b/Code/backend/dao/recipesDAO.js @@ -277,9 +277,9 @@ export default class RecipesDAO { return { success: false, message: "User not found" }; } - const existingBookmark = user.bookmarks.find( + const existingBookmark = user.bookmarks ? user.bookmarks.find( (bookmark) => bookmark._id.toString() === recipe._id.toString() - ); + ) : null; if (existingBookmark) { console.log("Recipe already bookmarked"); return { success: false, message: "Recipe already bookmarked" }; @@ -312,9 +312,9 @@ export default class RecipesDAO { static async removeBookmark(userName, recipeId) { try { console.log("DAO: Removing bookmark for:", { userName, recipeId }); - const updateResponse = await this.users.updateOne( + const updateResponse = await users.updateOne( { userName: userName }, - { $pull: { savedRecipes: { _id: recipeId } } } + { $pull: { bookmarks: { _id: recipeId } } } ); console.log("DAO: Update response:", updateResponse); From f17148194dfed0bc84ebdf214243638e75818bb5 Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Sun, 27 Oct 2024 23:57:49 -0400 Subject: [PATCH 14/43] Test for removing recipe from meal plan --- Code/backend/__tests__/testMealPlan.js | 9 +++++++++ Code/backend/test_resources/setup.js | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Code/backend/__tests__/testMealPlan.js b/Code/backend/__tests__/testMealPlan.js index e1c3276c..b4def3d0 100644 --- a/Code/backend/__tests__/testMealPlan.js +++ b/Code/backend/__tests__/testMealPlan.js @@ -16,6 +16,15 @@ describe("Meal Plan", function() { const response2 = await request.get("/mealPlan?userName=Test") expect(response2.text.includes("monday")).true expect(response2.text.includes(res0JSON["recipes"][0]['_id'])).true + }); + it("Should remove a recipe from the meal plan", async function() { + const response = await request.put("/mealPlan") + .send({ recipeID: "", userName: "Test", weekDay: "monday" }); + expect(response.status).to.eql(200); + const response2 = await request.get("/mealPlan?userName=Test") + const res2JSON = JSON.parse(response2.text) + expect(response2.text.includes("monday")).true + expect(res2JSON.monday === "").true }) }) }) \ No newline at end of file diff --git a/Code/backend/test_resources/setup.js b/Code/backend/test_resources/setup.js index c216d755..f7147686 100644 --- a/Code/backend/test_resources/setup.js +++ b/Code/backend/test_resources/setup.js @@ -7,7 +7,6 @@ dotenv.config() export default async function (globalConfig, projectConfig) { const uri = process.env.RECIPES_DB_URI; - console.log(uri) var mongoClient = MongoClient.connect(uri, { useNewUrlParser: true, maxPoolSize: 50, From 6270d38e0bca881f47afa1b632293c4de11bb930 Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Mon, 28 Oct 2024 00:04:54 -0400 Subject: [PATCH 15/43] Fixed error from resolving merge conflict --- Code/backend/dao/recipesDAO.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Code/backend/dao/recipesDAO.js b/Code/backend/dao/recipesDAO.js index 7fc5e04c..9c740efb 100644 --- a/Code/backend/dao/recipesDAO.js +++ b/Code/backend/dao/recipesDAO.js @@ -335,6 +335,7 @@ export default class RecipesDAO { console.error(`DAO: Unable to remove bookmark:`, e); throw e; } + } static async addRecipeToMealPlan(userName, recipeID, weekDay) { let response; From f8d98144abb45c892d7c7c1044268d4504b9cd76 Mon Sep 17 00:00:00 2001 From: Anuraag Jajoo Date: Mon, 28 Oct 2024 19:20:43 -0400 Subject: [PATCH 16/43] Updated Login/Signup forms --- Code/backend/api/recipes.controller.js | 22 ++- Code/backend/dao/recipesDAO.js | 35 ++-- Code/frontend/src/App.js | 22 ++- .../src/components/BookMarksRecipeCard.js | 3 - .../src/components/BookMarksRecipeList.js | 6 +- Code/frontend/src/components/Login.js | 156 +++++++++++++----- Code/frontend/src/components/RecipeCard.js | 34 ++-- 7 files changed, 185 insertions(+), 93 deletions(-) diff --git a/Code/backend/api/recipes.controller.js b/Code/backend/api/recipes.controller.js index 47579221..dc5c7549 100644 --- a/Code/backend/api/recipes.controller.js +++ b/Code/backend/api/recipes.controller.js @@ -62,10 +62,10 @@ export default class RecipesController { static async apiRemoveRecipeFromProfile(req, res) { const { userName, recipeId } = req.body; try { - const result = await RecipesDAO.removeBookmark(userName, recipeId) - res.json(result) + const result = await RecipesDAO.removeBookmark(userName, recipeId); + res.json(result); } catch (e) { - res.status(500).json({error: e}) + res.status(500).json({ error: e }); } } @@ -161,18 +161,22 @@ export default class RecipesController { static async apiAddtoPlan(req, res, next) { try { - let response = await RecipesDAO.addRecipeToMealPlan(req.body.userName, req.body.recipeID, req.body.weekDay) - res.json(response) - }catch(e) { + let response = await RecipesDAO.addRecipeToMealPlan( + req.body.userName, + req.body.recipeID, + req.body.weekDay + ); + res.json(response); + } catch (e) { res.status(500).json({ error: e }); } } static async apiGetMealPlan(req, res, next) { try { - let response = await RecipesDAO.getMealPlan(req.query.userName) - res.json(response) - }catch(e) { + let response = await RecipesDAO.getMealPlan(req.query.userName); + res.json(response); + } catch (e) { res.status(500).json({ error: e }); } } diff --git a/Code/backend/dao/recipesDAO.js b/Code/backend/dao/recipesDAO.js index 9c740efb..abeb25b2 100644 --- a/Code/backend/dao/recipesDAO.js +++ b/Code/backend/dao/recipesDAO.js @@ -277,9 +277,11 @@ export default class RecipesDAO { return { success: false, message: "User not found" }; } - const existingBookmark = user.bookmarks ? user.bookmarks.find( - (bookmark) => bookmark._id.toString() === recipe._id.toString() - ) : null; + const existingBookmark = user.bookmarks + ? user.bookmarks.find( + (bookmark) => bookmark._id.toString() === recipe._id.toString() + ) + : null; if (existingBookmark) { console.log("Recipe already bookmarked"); return { success: false, message: "Recipe already bookmarked" }; @@ -336,17 +338,20 @@ export default class RecipesDAO { throw e; } } - + static async addRecipeToMealPlan(userName, recipeID, weekDay) { let response; try { - let updateBody = JSON.parse('{ "meal-plan.' + weekDay + '": "' + recipeID + '" }') + let updateBody = JSON.parse( + '{ "meal-plan.' + weekDay + '": "' + recipeID + '" }' + ); response = await users.updateOne( { userName: userName }, - { $set: updateBody }) - return response + { $set: updateBody } + ); + return response; } catch (e) { - console.log(`Unable to add recipe to meal plan, ${e}`) + console.log(`Unable to add recipe to meal plan, ${e}`); } } @@ -359,19 +364,19 @@ export default class RecipesDAO { wednesday: "", thursday: "", friday: "", - saturday: "" - } + saturday: "", + }; try { - cursor = await users.findOne({ "userName": userName }); + cursor = await users.findOne({ userName: userName }); if (cursor.userName) { - let plan = cursor['meal-plan'] ? cursor['meal-plan'] : {} - mealPlanResponse = {...mealPlanResponse, ...plan} - return mealPlanResponse + let plan = cursor["meal-plan"] ? cursor["meal-plan"] : {}; + mealPlanResponse = { ...mealPlanResponse, ...plan }; + return mealPlanResponse; } else { throw new Error(`Cannot find user with name ${userName}`); } } catch (e) { - console.log(`error: ${e}`) + console.log(`error: ${e}`); } } diff --git a/Code/frontend/src/App.js b/Code/frontend/src/App.js index ed48e555..2c0724af 100644 --- a/Code/frontend/src/App.js +++ b/Code/frontend/src/App.js @@ -17,8 +17,10 @@ import BookMarksRecipeList from "./components/BookMarksRecipeList"; // Import Bo // Main component of the project class App extends Component { // constructor for the App Component - constructor() { - super(); + constructor(props) { + super(props); + + // this.handleRemoveBookmark = this.handleRemoveBookmark.bind(this); this.state = { cuisine: "", @@ -124,7 +126,7 @@ class App extends Component { handleRecipesByName = (recipeName) => { this.setState({ isLoading: true, - searchName: recipeName + searchName: recipeName, }); recipeDB .get("/recipes/getRecipeByName", { @@ -198,12 +200,11 @@ class App extends Component { }); if (response.data.success) { - // Update the bookmarks in the state this.setState((prevState) => ({ userData: { ...prevState.userData, bookmarks: prevState.userData.bookmarks.filter( - (recipe) => recipe.id !== recipeId // Remove based on recipeId + (recipe) => (recipe.id || recipe._id) !== recipeId // Remove based on recipeId ), }, })); @@ -212,7 +213,6 @@ class App extends Component { } } catch (error) { console.error("Failed to remove bookmark:", error); - // You might want to show a toast or some other error message to the user here } }; @@ -238,10 +238,10 @@ class App extends Component { handleProfileView={this.handleProfileView} user={this.state.userData} > - {/* Add BookMarksRecipeList to render bookmarks */} + {} ) : ( @@ -270,7 +270,11 @@ class App extends Component { {this.state.isLoading ? ( ) : ( - + )} diff --git a/Code/frontend/src/components/BookMarksRecipeCard.js b/Code/frontend/src/components/BookMarksRecipeCard.js index 98b922e1..4cb28340 100644 --- a/Code/frontend/src/components/BookMarksRecipeCard.js +++ b/Code/frontend/src/components/BookMarksRecipeCard.js @@ -48,9 +48,6 @@ const BookMarksRecipeCard = (props) => { duration: 3000, isClosable: true, }); - if (typeof props.onRemove === "function") { - props.onRemove(recipeId); - } } else { throw new Error(response.data.message || "Failed to remove bookmark"); } diff --git a/Code/frontend/src/components/BookMarksRecipeList.js b/Code/frontend/src/components/BookMarksRecipeList.js index 9093d03f..b2152650 100644 --- a/Code/frontend/src/components/BookMarksRecipeList.js +++ b/Code/frontend/src/components/BookMarksRecipeList.js @@ -20,7 +20,7 @@ import { } from "@chakra-ui/react"; import BookMarksRecipeCard from "./BookMarksRecipeCard"; -const BookMarksRecipeList = ({ recipes, onRemove }) => { +const BookMarksRecipeList = ({ recipes }) => { const [isOpen, setIsOpen] = useState(false); const [currentRecipe, setCurrentRecipe] = useState({}); const youtubeVideosURL = `https://www.youtube.com/results?search_query=${currentRecipe["TranslatedRecipeName"]}`; @@ -53,10 +53,10 @@ const BookMarksRecipeList = ({ recipes, onRemove }) => { {recipes.length !== 0 ? ( recipes.map((recipe) => ( onRemove(recipe.id)} // Pass the onRemove function with the recipe id + // onRemove={() => onRemove(recipe.id)} // Pass the onRemove function with the recipe id /> )) ) : ( diff --git a/Code/frontend/src/components/Login.js b/Code/frontend/src/components/Login.js index 9bb2ab7a..fcfee27a 100644 --- a/Code/frontend/src/components/Login.js +++ b/Code/frontend/src/components/Login.js @@ -1,57 +1,135 @@ -/* MIT License - -Copyright (c) 2023 Pannaga Rao, Harshitha, Prathima, Karthik */ - -import { useState } from "react" -import {Modal, ModalOverlay, ModalContent, ModalHeader, - ModalCloseButton, ModalBody, FormControl, FormLabel, Input, ModalFooter, Button} from "@chakra-ui/react" - -const Login = (props)=> { - const [userName, setUserName] = useState("") - const [password, setPassword] = useState("") - const handleUserName = (e)=>{ - setUserName(e.target.value) - } - const handlePassword = (e)=>{ - setPassword(e.target.value) - } - const handleLogin = (e)=> { - e.preventDefault(); - props.handleLogin(userName, password); - } - const handleSignup = (e)=> { - props.handleSignup(userName, password); - } - return ( - <> - +import { useState } from "react"; +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + FormControl, + FormLabel, + Input, + ModalFooter, + Button, + Text, + Link, + useToast, +} from "@chakra-ui/react"; + +const Login = (props) => { + const [userName, setUserName] = useState(""); + const [password, setPassword] = useState(""); + const [isLoginMode, setIsLoginMode] = useState(true); + const toast = useToast(); + + const handleUserName = (e) => setUserName(e.target.value); + const handlePassword = (e) => setPassword(e.target.value); + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + let result; + if (isLoginMode) { + // Call the login function passed as a prop + result = await props.handleLogin(userName, password); + } else { + // Call the signup function passed as a prop + result = await props.handleSignup(userName, password); + } + + // Check the result of login/signup + if (result.success) { + toast({ + title: isLoginMode ? "Login successful" : "Signup successful", + status: "success", + duration: 3000, + isClosable: true, + }); + // Optionally reset the form or perform other actions + setUserName(""); + setPassword(""); + props.onClose(); // Close modal on successful login/signup + } else { + toast({ + title: "Error", + description: result.message || "An unexpected error occurred.", + status: "error", + duration: 3000, + isClosable: true, + }); + } + } catch (error) {} + }; + + const toggleMode = () => { + setIsLoginMode(!isLoginMode); + // Reset fields when toggling + setUserName(""); + setPassword(""); + }; + + return ( + <> + + {" "} + {/* Pass onClose prop */} - LOG IN + {isLoginMode ? "LOG IN" : "SIGN UP"} + User Name - + Password - + + + + {isLoginMode ? ( + <> + New user?{" "} + + Sign Up here + + + ) : ( + <> + Already have an account?{" "} + + Log in here + + + )} + - - - - ) -} + + ); +}; -export default Login; \ No newline at end of file +export default Login; diff --git a/Code/frontend/src/components/RecipeCard.js b/Code/frontend/src/components/RecipeCard.js index dfd9dad6..190a8a3c 100644 --- a/Code/frontend/src/components/RecipeCard.js +++ b/Code/frontend/src/components/RecipeCard.js @@ -41,25 +41,29 @@ const RecipeCard = (props) => { }); console.log("Save recipe response:", response.data); + if (response.data.success) { + // Show success toast + toast({ + title: "Success", + description: "Bookmark saved successfully", + status: "success", + duration: 3000, + isClosable: true, + }); + } else { + throw new Error(response.data.message || "Failed to save bookmark"); + } // Handle successful save } catch (error) { console.error("Error saving recipe:", error); - - if (error.response) { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - console.error("Error data:", error.response.data); - console.error("Error status:", error.response.status); - console.error("Error headers:", error.response.headers); - } else if (error.request) { - // The request was made but no response was received - console.error("No response received:", error.request); - } else { - // Something happened in setting up the request that triggered an Error - console.error("Error message:", error.message); - } - // Show an error message to the user + toast({ + title: "Error", + description: error.message || "Failed to save bookmark", + status: "error", + duration: 3000, + isClosable: true, + }); } }; From f871ecd189760534f2a66b3cb7903da75631c75e Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Mon, 28 Oct 2024 20:04:06 -0400 Subject: [PATCH 17/43] Updated getMealPlan to return full recipes instead of just ids --- Code/backend/dao/recipesDAO.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Code/backend/dao/recipesDAO.js b/Code/backend/dao/recipesDAO.js index 9c740efb..a5866d5e 100644 --- a/Code/backend/dao/recipesDAO.js +++ b/Code/backend/dao/recipesDAO.js @@ -365,7 +365,15 @@ export default class RecipesDAO { cursor = await users.findOne({ "userName": userName }); if (cursor.userName) { let plan = cursor['meal-plan'] ? cursor['meal-plan'] : {} - mealPlanResponse = {...mealPlanResponse, ...plan} + for(const day in plan) { + if(plan[day] != "") { + let recipe = await recipes.findOne({_id: new ObjectId(plan[day])}) + let dayPlan = {} + dayPlan[day] = recipe + mealPlanResponse = {...mealPlanResponse, ...dayPlan} + } + } + console.log(mealPlanResponse) return mealPlanResponse } else { throw new Error(`Cannot find user with name ${userName}`); From 8e34c41335435b79493d99296e8cd6d7c6b79c3d Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Mon, 28 Oct 2024 20:04:26 -0400 Subject: [PATCH 18/43] Created basic meal plan ui page --- Code/frontend/src/App.js | 23 +++--- .../src/components/MealPlanRecipeCard.js | 71 +++++++++++++++++++ .../src/components/MealPlanRecipeList.js | 51 +++++++++++++ Code/frontend/src/components/UserMealPlan.js | 41 +++++++++++ 4 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 Code/frontend/src/components/MealPlanRecipeCard.js create mode 100644 Code/frontend/src/components/MealPlanRecipeList.js create mode 100644 Code/frontend/src/components/UserMealPlan.js diff --git a/Code/frontend/src/App.js b/Code/frontend/src/App.js index ed48e555..33799bf0 100644 --- a/Code/frontend/src/App.js +++ b/Code/frontend/src/App.js @@ -13,6 +13,7 @@ import Login from "./components/Login.js"; import UserProfile from "./components/UserProfile.js"; import LandingPage from "./components/LandingPage.js"; import BookMarksRecipeList from "./components/BookMarksRecipeList"; // Import BookMarksRecipeList +import UserMealPlan from "./components/UserMealPlan.js"; // Main component of the project class App extends Component { @@ -234,16 +235,18 @@ class App extends Component { {this.state.isLoggedIn ? ( <> {this.state.isProfileView ? ( - - {/* Add BookMarksRecipeList to render bookmarks */} - - + // + // {/* Add BookMarksRecipeList to render bookmarks */} + // + // + ) : ( diff --git a/Code/frontend/src/components/MealPlanRecipeCard.js b/Code/frontend/src/components/MealPlanRecipeCard.js new file mode 100644 index 00000000..9e476787 --- /dev/null +++ b/Code/frontend/src/components/MealPlanRecipeCard.js @@ -0,0 +1,71 @@ +import React from "react"; +import { + Card, + CardHeader, + Heading, + Text, + CardBody, + Image, + Tag, +} from "@chakra-ui/react"; + + +const MealPlanRecipeCard = (props) => { + return ( + + + + {props.recipe.TranslatedRecipeName} + + + + + + Cooking Time: {props.recipe.TotalTimeInMins} mins + + + Rating: {props.recipe["Recipe-rating"]} + + + Diet Type: {props.recipe["Diet-type"]} + + + Remove Bookmark + + + + ); +}; + +export default MealPlanRecipeCard \ No newline at end of file diff --git a/Code/frontend/src/components/MealPlanRecipeList.js b/Code/frontend/src/components/MealPlanRecipeList.js new file mode 100644 index 00000000..10ca8f5b --- /dev/null +++ b/Code/frontend/src/components/MealPlanRecipeList.js @@ -0,0 +1,51 @@ +import React, { useState } from "react"; +import { + Avatar, + Flex, + Modal, + ModalBody, + ModalCloseButton, + ModalOverlay, + ModalHeader, + ModalFooter, + ModalContent, + Box, + SimpleGrid, + Text, + Button, +} from "@chakra-ui/react"; +import MealPlanRecipeCard from "./MealPlanRecipeCard"; + +const MealPlanRecipeList = (props) => { + const plan = [] + for(const day in props.mealPlan) { + plan.push(<> + {day} + + ) + } + + return ( + <> + + + {plan} + + + + ); +}; + +export default MealPlanRecipeList \ No newline at end of file diff --git a/Code/frontend/src/components/UserMealPlan.js b/Code/frontend/src/components/UserMealPlan.js new file mode 100644 index 00000000..f21a04a5 --- /dev/null +++ b/Code/frontend/src/components/UserMealPlan.js @@ -0,0 +1,41 @@ +import { useEffect, useState } from "react"; +import { Heading, Flex, Button, Spacer } from "@chakra-ui/react" +import recipeDB from "../apis/recipeDB"; +import MealPlanRecipeList from "./MealPlanRecipeList"; + +const UserMealPlan = (props) => { + useEffect(() => { + const plan = recipeDB.get("/recipes/mealPlan", { + params: { + userName: localStorage.getItem("userName") + } + }) + plan.then(res => { + console.log(res) + if (res.data) { + console.log(res.data) + setMealPlan(res.data) + } + }) + }, []) + const [mealPlan, setMealPlan] = useState({}) + const handleClick = () => { + props.handleProfileView() + } + return ( + <> + + Meal Plan for {props.user.userName} + + + + {mealPlan.length === 0 ? + <> + : + + } + + ) +} + +export default UserMealPlan; \ No newline at end of file From 28243cd556313d553d6f3afea48f213bae92ebd2 Mon Sep 17 00:00:00 2001 From: rtthoma3 Date: Mon, 28 Oct 2024 22:34:48 -0400 Subject: [PATCH 19/43] Refined meal plan UI page --- Code/frontend/src/App.js | 35 ++++--- .../src/components/MealPlanRecipeCard.js | 22 +++-- .../src/components/MealPlanRecipeList.js | 93 +++++++++++++++++-- Code/frontend/src/components/Navbar.js | 4 + Code/frontend/src/components/Rating.js | 6 +- Code/frontend/src/components/UserMealPlan.js | 6 +- 6 files changed, 133 insertions(+), 33 deletions(-) diff --git a/Code/frontend/src/App.js b/Code/frontend/src/App.js index 33799bf0..a7f5263a 100644 --- a/Code/frontend/src/App.js +++ b/Code/frontend/src/App.js @@ -33,6 +33,7 @@ class App extends Component { isLoading: false, isLoggedIn: false, isProfileView: false, + isMealPlanView: false, userData: { bookmarks: [], }, @@ -42,12 +43,21 @@ class App extends Component { handleBookMarks = () => { this.setState({ isProfileView: true, + isMealPlanView: false }); }; + handleMealPlan = () => { + this.setState({ + isProfileView: false, + isMealPlanView: true + }) + } + handleProfileView = () => { this.setState({ isProfileView: false, + isMealPlanView: false, }); }; @@ -220,6 +230,7 @@ class App extends Component { handleProfileView = () => { this.setState({ isProfileView: false, + isMealPlanView: false }); }; @@ -229,22 +240,24 @@ class App extends Component {