diff --git a/.github/workflows/backendCoverage.yml b/.github/workflows/backendCoverage.yml new file mode 100644 index 00000000..c2f642b6 --- /dev/null +++ b/.github/workflows/backendCoverage.yml @@ -0,0 +1,36 @@ +name: Running Backend Tests With Coverage + +on: + pull_request: + branches: + - group31-fall24 + - master + +jobs: + run_backend_tests_Coverage: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [14.x] + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + fetch-depth: 2 + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: | + cd Code/backend + touch .env + echo RECIPES_DB_URI=${{ secrets.TESTING_DB_URI }} >> .env + echo RECIPES_NS=${{ secrets.DB_NAME }} >> .env + echo PORT=${{ secrets.API_PORT }} >> .env + echo GMAIL=${{ secrets.GMAIL }} >> .env + npm install + - name: Run backend tests with coverage + run: | + cd Code/backend + npm run test-cov diff --git a/.github/workflows/backendTests.yml b/.github/workflows/backendTests.yml new file mode 100644 index 00000000..e456b42c --- /dev/null +++ b/.github/workflows/backendTests.yml @@ -0,0 +1,33 @@ +name: Running Backend Tests + +on: push + +jobs: + run_backend_tests: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [14.x] + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + fetch-depth: 2 + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: | + cd Code/backend + touch .env + echo RECIPES_DB_URI=${{ secrets.TESTING_DB_URI }} >> .env + echo RECIPES_NS=${{ secrets.DB_NAME }} >> .env + echo PORT=${{ secrets.API_PORT }} >> .env + echo GMAIL=${{ secrets.GMAIL }} >> .env + npm install + - name: Run backend tests + run: | + cd Code/backend + npm test + diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 4a29016f..d1862a67 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,6 +1,6 @@ name: Running Code Coverage -on: [push, pull_request] +on: workflow_dispatch jobs: build: diff --git a/Code/backend/.babelrc b/Code/backend/.babelrc new file mode 100644 index 00000000..522c7e1c --- /dev/null +++ b/Code/backend/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "current" + } + } + ] + ] +} \ No newline at end of file diff --git a/Code/backend/.env b/Code/backend/.env deleted file mode 100644 index 6613871c..00000000 --- a/Code/backend/.env +++ /dev/null @@ -1,4 +0,0 @@ -RECIPES_DB_URI=mongodb+srv://atharvajoshi067:ZgSvdar14OnteUZx@cluster0.9zuebnu.mongodb.net/?retryWrites=true&w=majority -RECIPES_NS=recipe_recommender -PORT=5000 -GMAIL= atharva.joshi17@siesgst.ac.in \ No newline at end of file diff --git a/Code/backend/__tests__/test.spec.js b/Code/backend/__tests__/test.spec.js index 9460c698..c34b03ff 100644 --- a/Code/backend/__tests__/test.spec.js +++ b/Code/backend/__tests__/test.spec.js @@ -1,7 +1,11 @@ -const mongodb = require("mongodb"); +//const mongodb = require("mongodb"); +import { TextEncoder, TextDecoder } from 'util'; +Object.assign(global, { TextDecoder, TextEncoder }); +import * as mongodb from "mongodb" const MongoClient = mongodb.MongoClient; // const request = require("supertest")(httplocalhost5000apiv1); const expect = require("chai").expect; +//import expect from "chai" // var util= require('util'); // var encoder = new util.TextEncoder('utf-8'); // @@ -23,8 +27,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/__tests__/test1.js b/Code/backend/__tests__/test1.js index ee33f112..48581904 100644 --- a/Code/backend/__tests__/test1.js +++ b/Code/backend/__tests__/test1.js @@ -1,67 +1,80 @@ -const request = require("supertest")("http://localhost:5000/api/v1"); +import app from "../server"; +const baseURL = "/api/v1"; + +const request = require("supertest"); const expect = require("chai").expect; describe("GET /recipes", function () { it("is the API is functional test 1", async function () { - const response = await request.get("/recipes?CleanedIngredients=coconut"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=coconut"); expect(response.status).to.eql(200); }); it("is the API is functional test 2", async function () { - const response = await request.get("/recipes?CleanedIngredients=COCONUT"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=COCONUT"); expect(response.status).to.eql(200); }); it("is the API is functional test 3", async function () { - const response = await request.get("/recipes?CleanedIngredients=mango"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=mango"); expect(response.status).to.eql(200); }); it("is the API is functional test 4", async function () { - const response = await request.get("/recipes?CleanedIngredients=MANGO"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=MANGO"); expect(response.status).to.eql(200); }); it("is the API is functional test 5", async function () { - const response = await request.get("/recipes?CleanedIngredients=Mango"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=Mango"); expect(response.status).to.eql(200); }); it("is the API is functional test 6", async function () { - const response = await request.get("/recipes?CleanedIngredients=mANGO"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=mANGO"); expect(response.status).to.eql(200); }); it("is the API is functional test 7", async function () { - const response = await request.get( - "/recipes?CleanedIngredients={mango, salt}" + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get( + baseURL + "/recipes?CleanedIngredients={mango, salt}" ); expect(response.status).to.eql(200); }); it("is the API is functional test 8", async function () { - const response = await request.get( - "/recipes?CleanedIngredients={salt, mango}" + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get( + baseURL + "/recipes?CleanedIngredients={salt, mango}" ); expect(response.status).to.eql(200); }); it("is the API is functional test 8", async function () { - const response = await request.get("/recipes?CleanedIngredients={}"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients={}"); expect(response.status).to.eql(200); }); it("is the API is fetching the filtered ingredient", async function () { - const response = await request.get("/recipes?CleanedIngredients=pear"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=pear"); expect(response.body.filters.CleanedIngredients).to.eql("pear"); }); diff --git a/Code/backend/__tests__/test2.js b/Code/backend/__tests__/test2.js index ead4559a..7f6514ff 100644 --- a/Code/backend/__tests__/test2.js +++ b/Code/backend/__tests__/test2.js @@ -1,57 +1,69 @@ -const request = require("supertest")("http://localhost:5000/api/v1"); +import app from "../server"; +const baseURL = "/api/v1"; + +const request = require("supertest"); const expect = require("chai").expect; describe("GET /recipes", function () { it("is the API is fetching the filtered ingredient", async function () { - const response = await request.get("/recipes?CleanedIngredients=pear"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=pear"); expect(response.body.filters.CleanedIngredients).to.eql("pear"); }); it("is the API is fetching the filtered ingredient", async function () { - const response = await request.get("/recipes?CleanedIngredients=peach"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=peach"); expect(response.body.filters.CleanedIngredients).to.eql("peach"); }); it("is the API is fetching the filtered ingredient", async function () { - const response = await request.get("/recipes?CleanedIngredients=salt"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=salt"); expect(response.body.filters.CleanedIngredients).to.eql("salt"); }); it("is the API is fetching the filtered ingredient", async function () { - const response = await request.get("/recipes?CleanedIngredients=sugar"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=sugar"); expect(response.body.filters.CleanedIngredients).to.eql("sugar"); }); it("is the API is fetching the filtered ingredient", async function () { - const response = await request.get("/recipes?CleanedIngredients=cheese"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=cheese"); expect(response.body.filters.CleanedIngredients).to.eql("cheese"); }); it("is the API is fetching the filtered ingredient", async function () { - const response = await request.get("/recipes?CleanedIngredients=lemon"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=lemon"); expect(response.body.filters.CleanedIngredients).to.eql("lemon"); }); it("is the API is fetching the filtered ingredient", async function () { - const response = await request.get("/recipes?CleanedIngredients=spinach"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=spinach"); expect(response.body.filters.CleanedIngredients).to.eql("spinach"); }); it("is the API is fetching the filtered ingredient", async function () { - const response = await request.get("/recipes?CleanedIngredients=apple"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=apple"); expect(response.body.filters.CleanedIngredients).to.eql("apple"); }); it("is the API is fetching the filtered ingredient", async function () { - const response = await request.get("/recipes?CleanedIngredients=tortillas"); + await request(app).get(baseURL + "/recipes/initDB") + const response = await request(app).get(baseURL + "/recipes?CleanedIngredients=tortillas"); expect(response.body.filters.CleanedIngredients).to.eql("tortillas"); }); diff --git a/Code/backend/__tests__/testAddRecipes.js b/Code/backend/__tests__/testAddRecipes.js new file mode 100644 index 00000000..8febfd93 --- /dev/null +++ b/Code/backend/__tests__/testAddRecipes.js @@ -0,0 +1,65 @@ +import app from "../server"; +const baseURL = "/api/v1/recipes"; + +const request = require("supertest"); +const expect = require("chai").expect; + +describe("POST /addRecipes", function () { + it("Should add a recipe if all fields are provided", async function() { + await request(app).get(baseURL + "/initDB"); + const recipe = { + recipeName: "Test_Recipe_1", + cookingTime: 20, + dietType: "Normal", + recipeRating: 3, + cuisine: "American", + imageURL: "", + recipeURL: "", + instructions: "1) Cook the food \n2) Eat the food", + ingredients: ["Meat", "Pasta", "Sauce"], + restaurants: ["Pasta House"], + locations: ["Raleigh"] + } + const response = await request(app).post(baseURL + "/addRecipe").send(recipe) + expect(response.status).to.equal(200) + const response2 = await request(app).get( + baseURL + "/getRecipeByName?recipeName=Test_Recipe_1" + ); + expect(response2.status).to.equal(200) + expect(response2.text.includes("Test_Recipe_1")).true; + }); + it("Should add a recipe if some fields are provided", async function() { + await request(app).get(baseURL + "/initDB"); + const recipe = { + recipeName: "Test_Recipe_2", + cookingTime: 20, + recipeRating: 3, + cuisine: "American", + instructions: "1) Cook the food \n2) Eat the food", + ingredients: ["Meat", "Pasta", "Sauce"], + restaurants: [], + locations: [] + } + const response = await request(app).post(baseURL + "/addRecipe").send(recipe) + expect(response.status).to.equal(200) + const response2 = await request(app).get( + baseURL + "/getRecipeByName?recipeName=Test_Recipe_2" + ); + expect(response2.status).to.equal(200) + expect(response2.text.includes("Test_Recipe_2")).true; + }); + it("Should not add a recipe if no name is provided", async function() { + await request(app).get(baseURL + "/initDB"); + const recipe = { + cookingTime: 20, + recipeRating: 3, + cuisine: "American", + instructions: "1) Cook the food \n2) Eat the food", + ingredients: ["Meat", "Pasta", "Sauce"], + restaurants: [], + locations: [] + } + const response = await request(app).post(baseURL + "/addRecipe").send(recipe) + expect(response.status).to.not.equal(200) + }); +}) \ No newline at end of file diff --git a/Code/backend/__tests__/testBookmarksAndUserProfile.js b/Code/backend/__tests__/testBookmarksAndUserProfile.js index 2e704b1e..d040a5a7 100644 --- a/Code/backend/__tests__/testBookmarksAndUserProfile.js +++ b/Code/backend/__tests__/testBookmarksAndUserProfile.js @@ -2,29 +2,83 @@ Copyright (c) 2023 Pannaga Rao, Harshitha, Prathima, Karthik */ -const request = require("supertest")("http://localhost:5000/api/v1/recipes"); +import app from "../server"; +const baseURL = "/api/v1/recipes"; + +const request = require("supertest"); const expect = require("chai").expect; -describe("Recipes API Tests", function () { +describe("Bookmarks API Tests", function () { describe("POST /addRecipeToProfile", function () { it("should successfully add a recipe to user's profile", async function () { - const recipeData = { recipeId: "1", title: "Test Recipe" }; - const response = await request.post("/addRecipeToProfile") - .send(recipeData) - + await request(app).get(baseURL + "/initDB") + const recipeData = { recipeId: "1", title: "Test Recipe"}; + const response = await request(app).post(baseURL + "/addRecipeToProfile") + .send({userName: "Test", recipe: recipeData}) expect(response.status).to.eql(200); + const response2 = await request(app).get(baseURL + "/getBookmarks") + .query({ userName: "Test" }); + expect(response2.text.includes('"recipeId":"1"')).true + }); + it("should successfully not add a recipe if no user is given", async function () { + await request(app).get(baseURL + "/initDB") + const recipeData = { recipeId: "2", title: "Test Recipe"}; + const response = await request(app).post(baseURL + "/addRecipeToProfile") + .send({recipe: recipeData}) + expect(response.status).not.to.eql(200); + expect(response.text.includes('"success":false')).true + }); + it("should successfully not add a recipe if no recipe is given", async function () { + await request(app).get(baseURL + "/initDB") + const response = await request(app).post(baseURL + "/addRecipeToProfile") + .send({userName: "Test"}) + + expect(response.status).not.to.eql(200); + expect(response.text.includes('"success":false')).true }); }); describe("Recipes API - Get Bookmarks Tests", function () { // Test with userName provided it("should return bookmarks for a given user", async function () { - const response = await request.get("/getBookmarks") + await request(app).get(baseURL + "/initDB") + const response = await request(app).get(baseURL + "/getBookmarks") .query({ userName: "Test" }); expect(response.status).to.eql(200); }); + it("should not return bookmarks for a nonexistent user", async function () { + await request(app).get(baseURL + "/initDB") + const response = await request(app).get(baseURL + "/getBookmarks") + .query({ userName: "Fake" }); + + expect(response.status).not.to.eql(200); + }); }); + describe("Recipes API - Delete Bookmarks Tests", function() { + it("Should remove an existing bookmark from a user", async function() { + await request(app).get(baseURL + "/initDB"); + const response = await request(app).post(baseURL + "/removeBookmark").send({userName:"TestB", recipeId:85}) + expect(response.status).to.equal(200) + expect(response.text.includes('"success":true')).true + const response2 = await request(app).get(baseURL + "/getBookmarks") + .query({ userName: "TestB" }); + expect(response2.text.includes("Test_Recipe_B")).false + }) + it("Should not remove a bookmark that does not exist", async function() { + await request(app).get(baseURL + "/initDB"); + const response = await request(app).post(baseURL + "/removeBookmark").send({userName:"TestB", recipeId:55}) + expect(response.status).to.equal(200) + expect(response.text.includes('"success":false')).true + }) + it("Should not remove a bookmark from a user that does not exist", async function() { + await request(app).get(baseURL + "/initDB"); + const response = await request(app).post(baseURL + "/removeBookmark").send({userName:"Fake", recipeId:85}) + expect(response.status).to.equal(200) + expect(response.text.includes('"success":false')).true + }) + }) + }); diff --git a/Code/backend/__tests__/testGetRecipes.js b/Code/backend/__tests__/testGetRecipes.js index 31ec32ce..9b1ff7b7 100644 --- a/Code/backend/__tests__/testGetRecipes.js +++ b/Code/backend/__tests__/testGetRecipes.js @@ -1,71 +1,96 @@ -const request = require("supertest")("http://localhost:5000/api/v1"); +import app from "../server"; +const baseURL = "/api/v1/recipes"; + +const request = require("supertest"); const expect = require("chai").expect; describe("GET /recipes", function () { it("is the API is functional test 1", async function () { - const response = await request.get("/recipes?CleanedIngredients=Tomato"); + await request(app).get(baseURL + "/initDB") + const response = await request(app).get(baseURL + "?CleanedIngredients=Tomato"); expect(response.status).to.eql(200); }); it("is the API is functional test 2", async function () { - const response = await request.get("/recipes?Cuisine=Indian"); + await request(app).get(baseURL + "/initDB") + const response = await request(app).get(baseURL + "?Cuisine=Indian"); expect(response.status).to.eql(200); }); it("is the API is fetching the filtered ingredient", async function () { - const response = await request.get( - "/recipes?CleanedIngredients=Mango&Cuisine=Indian" + await request(app).get(baseURL + "/initDB") + const response = await request(app).get( + baseURL + "?CleanedIngredients=Mango&Cuisine=Indian" ); expect(response.body.filters.CleanedIngredients).to.eql("Mango"); }); it("is the API is fetching the filtered ingredient", async function () { - const response = await request.get( - "/recipes?CleanedIngredients=Mango&Cuisine=Mexican" + await request(app).get(baseURL + "/initDB") + const response = await request(app).get( + baseURL + "?CleanedIngredients=Mango&Cuisine=Mexican" ); expect(response.body.filters.Cuisine).to.eql("Mexican"); }); it("is the API fetching the recipe components", async function () { - const response = await request.get( - "/recipes?CleanedIngredients=Mango&Cuisine=Indian" + await request(app).get(baseURL + "/initDB") + const response = await request(app).get( + baseURL + "?CleanedIngredients=Mango&Cuisine=Indian" ); expect(response.text.includes("Cleaned-Ingredients")).true; }); it("is the API fetching the recipe components", async function () { - const response = await request.get( - "/recipes?CleanedIngredients=Mango&Cuisine=Indian" + await request(app).get(baseURL + "/initDB") + const response = await request(app).get( + baseURL + "?CleanedIngredients=Mango&Cuisine=Indian" ); expect(response.text.includes('"Cuisine":"Indian"')).true; }); it("is the API fetching the recipe components", async function () { - const response = await request.get( - "/recipes?CleanedIngredients=Mango&Cuisine=Indian" + await request(app).get(baseURL + "/initDB") + const response = await request(app).get( + baseURL + "?CleanedIngredients=Mango&Cuisine=Indian" ); expect(response.text.includes("TotalTimeInMins")).true; }); it("is the API fetching the recipe components", async function () { - const response = await request.get( - "/recipes?CleanedIngredients=Mango&Cuisine=Indian" + await request(app).get(baseURL + "/initDB") + const response = await request(app).get( + baseURL + "?CleanedIngredients=Mango&Cuisine=Indian" ); expect(response.text.includes("Diet-type")).true; }); it("is the API fetching the recipe components", async function () { - const response = await request.get( - "/recipes?CleanedIngredients=Mango&Cuisine=Indian" + await request(app).get(baseURL + "/initDB") + const response = await request(app).get( + baseURL + "?CleanedIngredients=Mango&Cuisine=Indian" ); expect(response.text.includes("Recipe-rating")).true; }); it("is the API fetching the recipe by name", async function () { - const response = await request.get( - "/recipes/getRecipeByName?recipeName=andhra" + await request(app).get(baseURL + "/initDB") + const response = await request(app).get( + baseURL + "/getRecipeByName?recipeName=andhra" ); expect(response.text.includes("Andhra")).true; }); }); + +describe("GET /cuisines", function() { + it("Should return a list of distinct cuisine types", async function() { + await request(app).get(baseURL + "/initDB"); + const response = await request(app).get(baseURL + "/cuisines"); + + expect(response.status).to.equal(200); + const resArray = JSON.parse(response.text) + expect(resArray.indexOf('Indian')).to.equal(resArray.lastIndexOf("Indian")) + expect(resArray.indexOf('American')).to.equal(resArray.lastIndexOf("American")) + }) +}) diff --git a/Code/backend/__tests__/testIngredients.js b/Code/backend/__tests__/testIngredients.js new file mode 100644 index 00000000..e0765c97 --- /dev/null +++ b/Code/backend/__tests__/testIngredients.js @@ -0,0 +1,17 @@ +import app from "../server"; +const baseURL = "/api/v1/recipes"; + +const request = require("supertest"); +const expect = require("chai").expect; + +describe("GET /callIngredients", function () { + it("Should retrieve all ingredients in the ingredients list", async function() { + await request(app).get(baseURL + "/initDB"); + const response = await request(app).get(baseURL + "/callIngredients") + console.log(response.text) + expect(response.status).to.equal(200) + expect(response.text.includes("Bread")).true + const resArray = JSON.parse(response.text) + expect(resArray.length).to.equal(4) + }) +}); \ No newline at end of file diff --git a/Code/backend/__tests__/testLoginPage.js b/Code/backend/__tests__/testLoginPage.js index 7629050d..c1ecfb56 100644 --- a/Code/backend/__tests__/testLoginPage.js +++ b/Code/backend/__tests__/testLoginPage.js @@ -1,28 +1,64 @@ /* MIT License Copyright (c) 2023 Pannaga Rao, Harshitha, Prathima, Karthik */ +import app from "../server"; +const baseURL = "/api/v1/recipes"; -const request = require("supertest")("http://localhost:5000/api/v1/recipes"); +const request = require("supertest"); const expect = require("chai").expect; -describe("Recipes API Tests", function () { +describe("Login API Tests", function () { // Login Tests describe("GET /login", function () { it("should log in successfully with correct credentials", async function () { - const response = await request.get("/login") + await request(app).get(baseURL + "/initDB") + const response = await request(app).get(baseURL + "/login") .query({ userName: "Test", password: "admin" }); expect(response.status).to.eql(200); }); + it("should fail to log in with nonexistent user", async function () { + await request(app).get(baseURL + "/initDB") + const response = await request(app).get(baseURL + "/login") + .query({ userName: "NotReal", password: "fake" }); + expect(response.status).to.eql(200); + const resJSON = JSON.parse(response.text); + expect(resJSON.success).false; + expect(resJSON.user).undefined; + }); + it("should fail to log in with incorrect password", async function () { + await request(app).get(baseURL + "/initDB") + const response = await request(app).get(baseURL + "/login") + .query({ userName: "Test", password: "fake" }); + expect(response.status).to.eql(200); + const resJSON = JSON.parse(response.text); + expect(resJSON.success).false; + expect(resJSON.user).undefined; + }); }); describe("GET /signup", function () { it("should not sign up for existing users", async function () { - const response = await request.get("/signup") + await request(app).get(baseURL + "/initDB") + const response = await request(app).post(baseURL + "/signup") .query({ userName: "Test", password: "admin" }); expect(response.status).not.to.eql(200); }); + it("should sign up for new users", async function () { + await request(app).get(baseURL + "/initDB") + const response = await request(app).post(baseURL + "/signup") + .query({ userName: "Test2", password: "admin2" }); + + expect(response.status).to.eql(200); + }); + it("should not sign up for users without a password", async function () { + await request(app).get(baseURL + "/initDB") + const response = await request(app).post(baseURL + "/signup") + .query({ userName: "Test3" }); + + expect(response.status).not.to.eql(200); + }); }); }); diff --git a/Code/backend/__tests__/testMealPlan.js b/Code/backend/__tests__/testMealPlan.js new file mode 100644 index 00000000..05cc5758 --- /dev/null +++ b/Code/backend/__tests__/testMealPlan.js @@ -0,0 +1,33 @@ +const { ObjectId } = require("mongodb"); +import app from "../server"; +const baseURL = "/api/v1/recipes"; + +const request = require("supertest") +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() { + await request(app).get(baseURL + "/initDB") + const response0 = await request(app).get(baseURL + "/getRecipeByName?recipeName=BLT"); + const res0JSON = JSON.parse(response0.text) + expect(response0.status).to.eql(200); + const response = await request(app).put(baseURL + "/mealPlan") + .send({ recipeID: res0JSON["recipes"][0]['_id'], userName: "Test", weekDay: "monday" }); + expect(response.status).to.eql(200); + const response2 = await request(app).get(baseURL + "/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() { + await request(app).get(baseURL + "/initDB") + const response = await request(app).put(baseURL + "/mealPlan") + .send({ recipeID: "", userName: "Test", weekDay: "monday" }); + expect(response.status).to.eql(200); + const response2 = await request(app).get(baseURL + "/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/__tests__/testRatings.js b/Code/backend/__tests__/testRatings.js new file mode 100644 index 00000000..f5030ab0 --- /dev/null +++ b/Code/backend/__tests__/testRatings.js @@ -0,0 +1,24 @@ +const { ObjectId } = require("mongodb"); +import app from "../server"; +const baseURL = "/api/v1/recipes"; + +const request = require("supertest"); +const expect = require("chai").expect; + +describe("Ratings", function() { + + describe("PATCH /rateRecipe", function() { + it("Should correctly update the rating to be new average", async function() { + await request(app).get(baseURL + "/initDB") + const response0 = await request(app).get(baseURL + "/getRecipeByName?recipeName=BLT"); + const res0JSON = JSON.parse(response0.text) + expect(response0.status).to.eql(200); + const response = await request(app).patch(baseURL + "/rateRecipe") + .send({ recipeID: res0JSON["recipes"][0]['_id'], rating: 3 }); + expect(response.status).to.eql(200); + const response2 = await request(app).get(baseURL + "/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 79a30bfd..75dbdf60 100644 --- a/Code/backend/api/recipes.controller.js +++ b/Code/backend/api/recipes.controller.js @@ -2,56 +2,71 @@ 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); 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}`) - } - } else { - res.json({ success: false }) + const { userName, recipe } = req.body; + + if (!userName || !recipe) { + return res + .status(400) + .json({ success: false, message: "Missing userName or recipe" }); } + try { + const result = await RecipesDAO.addRecipeToProfile(userName, recipe); + 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 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 - console.log(req.query) if (req.query.recipeName) { filters.recipeName = req.query.recipeName; } @@ -61,7 +76,7 @@ export default class RecipesController { }); let response = { - recipes: recipesList + recipes: recipesList, }; res.json(response); } @@ -112,7 +127,17 @@ 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 }); + } + } + + 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 }); } @@ -122,10 +147,39 @@ export default class RecipesController { try { let ingredients = await RecipesDAO.getIngredients(); res.json(ingredients); - } catch(e) { + } catch (e) { + 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 }); } } -} + 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 }); + } + } + static async apiInitDB(req, res) { + try { + let response = await RecipesDAO.initDB(); + 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 3c3d7134..ae74e1f0 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,4 +22,12 @@ router.route("/addRecipeToProfile").post(RecipesCtrl.apiPostRecipeToProfile); router.route("/getRecipeByName").get(RecipesCtrl.apiGetRecipeByName); +router.route("/rateRecipe").patch(RecipesCtrl.apiPatchRecipeRating); + +router.route("/removeBookmark").post(RecipesCtrl.apiRemoveRecipeFromProfile); + +router.route("/mealPlan").put(RecipesCtrl.apiAddtoPlan).get(RecipesCtrl.apiGetMealPlan) + +router.route("/initDB").get(RecipesCtrl.apiInitDB) + export default router; diff --git a/Code/backend/dao/recipesDAO.js b/Code/backend/dao/recipesDAO.js index 50b7a208..15f2f9c9 100644 --- a/Code/backend/dao/recipesDAO.js +++ b/Code/backend/dao/recipesDAO.js @@ -1,4 +1,4 @@ -import mongodb from "mongodb"; +import * as mongodb from "mongodb"; import nodemailer from "nodemailer"; import password from "./mail_param.js"; const pass = password.password; @@ -16,8 +16,11 @@ 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"); + //console.log("db started") } catch (e) { console.error( `Unable to establish a collection handle in recipesDAO: ${e}` @@ -29,17 +32,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 +51,33 @@ export default class RecipesDAO { let query; let cursor; let user; - query = { "userName": data.userName } - console.log(query) + query = { userName: data.userName }; if (data) { cursor = await users.findOne(query); - console.log(cursor) - if (cursor!==null) { - return {success: false} + 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 }; 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 +87,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 @@ -121,14 +123,10 @@ export default class RecipesDAO { const str1 = filters["CleanedIngredients"][i]; str += "(?=.*" + str1 + ")"; } - console.log(str); query = { "Cleaned-Ingredients": { $regex: str } }; query["Cuisine"] = filters["Cuisine"]; - console.log(query); var email = filters["Email"]; var flagger = filters["Flag"]; - console.log(email); - console.log(flagger); } } @@ -208,15 +206,15 @@ export default class RecipesDAO { // Function to add a recipe static async addRecipe(recipe) { - console.log("Inside addRecipe"); - console.log(recipe); let inputRecipe = {}; inputRecipe["TranslatedRecipeName"] = recipe["recipeName"]; 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["ImageFile"]=recipe["imageFile"] inputRecipe["URL"] = recipe["recipeURL"]; inputRecipe["TranslatedInstructions"] = recipe["instructions"]; var ingredients = ""; @@ -232,42 +230,180 @@ export default class RecipesDAO { } inputRecipe["Restaurant"] = restaurants; inputRecipe["Restaurant-Location"] = locations; - 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; } } - //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}`) + 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 } } + ); + } + + //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 + ? 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" }; + } + + // 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 { + const updateResponse = await users.updateOne( + { userName: userName }, + { $pull: { bookmarks: { _id: recipeId } } } + ); + + if (updateResponse.modifiedCount === 1) { + return { success: true, message: "Bookmark removed successfully" }; + } else if (updateResponse.matchedCount === 0) { + return { success: false, message: "User not found" }; + } else { + return { + success: false, + message: "Bookmark not found or already removed", + }; + } + } catch (e) { + console.error(`DAO: Unable to remove bookmark:`, e); + throw e; + } + } + + static async addRecipeToMealPlan(userName, recipeID, weekDay) { + let response; + try { + if(!recipeID) { + throw new Error("recipe id not defined") + } + 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 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'] : {} + 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} + } + } + return mealPlanResponse + } else { + throw new Error(`Cannot find user with name ${userName}`); } + } catch (e) { + console.log(`error: ${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; } - } + } + + static async initDB() { + if(recipes) { + return {success: true} + } + try { + await mongodb.MongoClient.connect(process.env.RECIPES_DB_URI, { + maxPoolSize: 50, + wtimeoutMS: 2500, + useNewUrlParser: true, + }).then(async (client) => { + await this.injectDB(client) + return {success: true} + }) + + } catch (e) { + console.log(e); + return {success: false} + } + } } diff --git a/Code/backend/index.js b/Code/backend/index.js index 0ca44ac2..03a1517a 100644 --- a/Code/backend/index.js +++ b/Code/backend/index.js @@ -1,5 +1,5 @@ import app from "./server.js"; -import mongodb from "mongodb"; +import * as mongodb from "mongodb"; import dotenv from "dotenv"; import recipesDAO from "./dao/recipesDAO.js"; dotenv.config(); diff --git a/Code/backend/package.json b/Code/backend/package.json index 20c53293..c178b394 100644 --- a/Code/backend/package.json +++ b/Code/backend/package.json @@ -5,7 +5,8 @@ "main": "index.js", "type": "module", "scripts": { - "test": "set CI=true && jest", + "test": "set CI=true && jest --setupFiles dotenv/config", + "test-cov": "set CI=true && jest --setupFiles dotenv/config --coverage", "lint": "eslint 'dao/**.js'", "format": "prettier --check ./dao", "format:fix": "prettier --write ./dao" @@ -23,7 +24,11 @@ "nodemon": "^2.0.13" }, "devDependencies": { + "@babel/core": "^7.26.0", + "@babel/preset-env": "^7.26.0", + "@shelf/jest-mongodb": "^4.3.2", "babel-eslint": "^10.1.0", + "babel-jest": "^29.7.0", "eslint": "^7.32.0", "eslint-plugin-react": "^7.26.1", "jest": "^27.3.1", @@ -32,12 +37,14 @@ }, "jest": { "collectCoverageFrom": [ - "/__tests__/*.{js,jsx}" + "api/*.js", + "dao/*.js" ], "coveragePathIgnorePatterns": [ "/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..2192e41d --- /dev/null +++ b/Code/backend/test_resources/setup.js @@ -0,0 +1,93 @@ +import { TextEncoder, TextDecoder } from 'util'; +import mongodb from "mongodb" +const MongoClient = mongodb.MongoClient; +import dotenv from 'dotenv' +dotenv.config() + +export default function (globalConfig, projectConfig) { + + console.log('Setting up for tests') + const users = {"users": [ + { + "userName": "Test", + "password": "admin", + }, + { + "userName": "TestB", + "password": "adminB", + "bookmarks": [ + { + "TranslatedRecipeName": "Test_Recipe_B", + "_id": 85 + } + ] + } + ]} + const recipes = { + "recipes": [ + { + "TranslatedRecipeName": "BLT", + "TotalTimeInMins": "15", + "Diet-type": "", + "Recipe-rating": 5, + "Times-rated": 1, + "Cuisine": "American", + "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": "" + }, + { + "TranslatedRecipeName": "AndhraTwo", + "TotalTimeInMins": "20", + "Diet-type": "Vegetarian", + "Recipe-rating": 5, + "Times-rated": 1, + "Cuisine": "Indian", + "image-url": "", + "URL": "", + "TranslatedInstructions": "Cook the food", + "Cleaned-Ingredients": "Bread%", + "Restaurant": "", + "Restaurant-Location": "" + } + ] + } + const ingredients = {"ingredients": [ + {"item_name": "Tomato"}, {"item_name": "Bread"}, {"item_name": "Lettuce"}, {"item_name": "Bacon"}, + ]} + Object.assign(global, { TextDecoder, TextEncoder }); + const uri = process.env.RECIPES_DB_URI; + var mongoClient = MongoClient.connect(uri, { + useNewUrlParser: true, + maxPoolSize: 50, + wtimeoutMS: 2500, + }).then(async (client) => { + const recipeCollection = client.db(process.env.RECIPES_NS).collection("recipe")//.then(async (recipeCollection) => { + await recipeCollection.deleteMany({}) + await recipeCollection.insertMany(recipes.recipes) + const userCollection = client.db(process.env.RECIPES_NS).collection("user")//.then(async (recipeCollection) => { + await userCollection.deleteMany({}) + await userCollection.insertMany(users.users) + const ingredientCollection = client.db(process.env.RECIPES_NS).collection("ingredient_list")//.then(async (recipeCollection) => { + await ingredientCollection.deleteMany({}) + await ingredientCollection.insertMany(ingredients.ingredients) + 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 diff --git a/Code/frontend/src/App.js b/Code/frontend/src/App.js index 08ea45f8..b21bca65 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,12 +11,17 @@ 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"; +import BookMarksRecipeList from "./components/BookMarksRecipeList"; // Import BookMarksRecipeList +import UserMealPlan from "./components/UserMealPlan.js"; // 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: "", @@ -24,81 +29,94 @@ class App extends Component { ingredients: new Set(), recipeList: [], recipeByNameList: [], + searchName: "", email: "", flag: false, isLoading: false, isLoggedIn: false, isProfileView: false, - userData: {} + isMealPlanView: false, + userData: { + bookmarks: [], + }, }; } - handleBookMarks = ()=> { + handleBookMarks = () => { this.setState({ - isProfileView: true - }) - } + isProfileView: true, + isMealPlanView: false + }); + }; - handleProfileView = ()=> { + handleMealPlan = () => { this.setState({ - isProfileView: false + isProfileView: false, + isMealPlanView: true }) } - handleSignup = async (userName, password)=> { + handleProfileView = () => { + this.setState({ + isProfileView: false, + isMealPlanView: false, + }); + }; + + 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 +136,23 @@ 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, + searchName: recipeName, + }); + 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 +166,101 @@ 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: {}, + }); + }; + + 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) { + this.setState((prevState) => ({ + userData: { + ...prevState.userData, + bookmarks: prevState.userData.bookmarks.filter( + (recipe) => (recipe.id || 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); + } + }; + + handleProfileView = () => { + this.setState({ + isProfileView: false, + isMealPlanView: false + }); + }; render() { return (
-
); } diff --git a/Code/frontend/src/components/AddRecipe.js b/Code/frontend/src/components/AddRecipe.js index 5bccb843..cf926715 100644 --- a/Code/frontend/src/components/AddRecipe.js +++ b/Code/frontend/src/components/AddRecipe.js @@ -12,6 +12,7 @@ const AddRecipe = () => { cuisine: "", recipeURL: "", imageURL: "", + imageFile:"", instructions: "", ingredientCount: 0, ingredients: [], @@ -20,6 +21,7 @@ const AddRecipe = () => { }); const [ingredientCount, setIngredientCount] = React.useState(0); + const [imageFile, setImageFile] = React.useState(null); const addIngredient = () => { const ingredient = document.getElementById("ingredients").value; @@ -66,6 +68,9 @@ const AddRecipe = () => { } const addRecipe = () => { + const formData = new FormData(); + formData.append("recipeData", JSON.stringify(recipe)); + if (imageFile) formData.append("imageFile", imageFile); recipeDB.post("/recipes/addRecipe", recipe) .then(res => { console.log(res.data); @@ -78,6 +83,7 @@ const AddRecipe = () => { cuisine: "", recipeURL: "", imageURL: "", + imageFile:"", instructions: "", ingredientCount: 0, ingredients: [], @@ -92,6 +98,7 @@ const AddRecipe = () => { document.getElementById("recipeURL").value = ""; document.getElementById("imageURL").value = ""; document.getElementById("instructions").value = ""; + document.getElementById("imageFile").value=""; // Alert user that recipe was added @@ -101,7 +108,9 @@ const AddRecipe = () => { }) .catch(err => console.log(err)); } - + const handleFileChange = (event) => { + setImageFile(event.target.files[0]); + }; const ingredientPrintHandler = () => { const ingredientList = recipe.ingredients; @@ -198,6 +207,7 @@ const AddRecipe = () => { + @@ -228,4 +238,4 @@ const AddRecipe = () => { ) }; -export default AddRecipe; \ No newline at end of file +export default AddRecipe; diff --git a/Code/frontend/src/components/AddToPlanModal.js b/Code/frontend/src/components/AddToPlanModal.js new file mode 100644 index 00000000..d7b4a83a --- /dev/null +++ b/Code/frontend/src/components/AddToPlanModal.js @@ -0,0 +1,152 @@ +import React, { useState } from "react"; +import { Button, Modal,ModalBody, + ModalCloseButton, + ModalOverlay, + ModalHeader, + ModalFooter, + ModalContent, + Heading, + Box, + SimpleGrid, + Text, + Card, + CardHeader, + CardBody, + Image, + Select, + Spacer +} from "@chakra-ui/react"; +import Rating from "./Rating"; +import recipeDB from "../apis/recipeDB"; + +const MiniRecipeCard = (props) => { + const color = props.recipe['_id'] === props.selectedRecipe ? "lightgray" : null + const handleClick = () => { + props.setRecipe(props.recipe['_id']) + console.log(props.recipe['_id']) + console.log(color) + } + return handleClick()} + > + + + {props.recipe.TranslatedRecipeName} + + + + + Cooking Time: {props.recipe.TotalTimeInMins} mins + + + + Rating: + {/* {props.recipe["Recipe-rating"]} */} + + + + + Diet Type: {props.recipe["Diet-type"]} + + + +} + +const AddToPlanModal = (props) => { + const [isOpen, setIsOpen] = useState(false) + const [day, setDay] = useState(props.day) + const [recipeToAdd, setRecipeToAdd] = useState(null) + const onClose = () => { + setRecipeToAdd(null) + setDay(props.day) + setIsOpen(false) + } + + const addToMealPlan = () => { + const requestBody = { + userName: localStorage.getItem("userName"), + recipeID: recipeToAdd, + weekDay: day + } + recipeDB.put("/recipes/mealPlan", requestBody).then((res) => { + setIsOpen(false) + props.updateMealPlan() + }) + } + + const TextButton = () => { + return + } + const PlusButton = () => { + return setIsOpen(true)} variant="ghost" size="lg" bgColor="transparent"> + + + + } + + return ( + <> + {props.text ? : } + + + + Add to Meal Plan from Bookmarks + + + + {props.bookmarks.length !== 0 ? ( + props.bookmarks.map((recipe) => ( + + )) + ) : ( + + No bookmarks available. + + )} + + + + Add this meal to which day? + + + + + + + + + ) +} + +export default AddToPlanModal \ No newline at end of file diff --git a/Code/frontend/src/components/BookMarksRecipeCard.js b/Code/frontend/src/components/BookMarksRecipeCard.js index 54f8b348..4cb28340 100644 --- a/Code/frontend/src/components/BookMarksRecipeCard.js +++ b/Code/frontend/src/components/BookMarksRecipeCard.js @@ -1,40 +1,125 @@ -/* 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, + }); + } 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..b2152650 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 [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/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/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/MealPlanRecipeCard.js b/Code/frontend/src/components/MealPlanRecipeCard.js new file mode 100644 index 00000000..0fb98c08 --- /dev/null +++ b/Code/frontend/src/components/MealPlanRecipeCard.js @@ -0,0 +1,94 @@ +import React from "react"; +import { + Card, + CardHeader, + Heading, + Text, + CardBody, + Image, + Tag, + Box +} from "@chakra-ui/react"; +import Rating from "./Rating"; +import recipeDB from "../apis/recipeDB"; + + +const MealPlanRecipeCard = (props) => { + const removeFromMealPlan = (day) => { + const requestBody = { + userName: localStorage.getItem("userName"), + recipeID: "", + weekDay: day + } + recipeDB.put("/recipes/mealPlan", requestBody).then((res) => { + props.updateMealPlan() + }) + } + + return ( + + + props.handler(props.recipe)} + cursor='pointer' // Add pointer cursor to indicate it's clickable + > + {props.recipe.TranslatedRecipeName} + + + + + + Cooking Time: {props.recipe.TotalTimeInMins} mins + + + + Rating: + {/* {props.recipe["Recipe-rating"]} */} + + + + + Diet Type: {props.recipe["Diet-type"]} + + + removeFromMealPlan(props.day)} + > + Remove + + + + + ); +}; + +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..9ddad62a --- /dev/null +++ b/Code/frontend/src/components/MealPlanRecipeList.js @@ -0,0 +1,135 @@ +import React, { useState } from "react"; +import { + Avatar, + Flex, + Modal, + ModalBody, + ModalCloseButton, + ModalOverlay, + ModalHeader, + ModalFooter, + ModalContent, + Box, + SimpleGrid, + Text, + Button, + Heading +} from "@chakra-ui/react"; +import Rating from "./Rating"; +import MealPlanRecipeCard from "./MealPlanRecipeCard"; +import AddToPlanModal from "./AddToPlanModal"; + +const MealPlanRecipeList = (props) => { + const [isOpen, setIsOpen] = useState(false); + const [currentRecipe, setCurrentRecipe] = useState({}); + const youtubeVideosURL = `https://www.youtube.com/results?search_query=${currentRecipe["TranslatedRecipeName"]}`; + + const handleViewRecipe = (data) => { + setIsOpen(true); + setCurrentRecipe(data); + }; + + const onClose = () => { + setIsOpen(false); + }; + + const plan = [] + const today = new Date() + var weekDay = 0 + for(const day in props.mealPlan) { + const color = weekDay === today.getDay() ? "lightgray" : "" + plan.push( + {day.toUpperCase()} + { props.mealPlan[day] ? + () : + ( + + )} + ) + weekDay++ + } + + return ( + <> + + + {plan} + + + + + + {currentRecipe.TranslatedRecipeName} + + + + + + + Cooking Time: + {currentRecipe.TotalTimeInMins} mins + + + Rating: {" "} + {/* {currentRecipe["Recipe-rating"]} */} + + + + Diet Type: {currentRecipe["Diet-type"]} + + + + + Instructions: {" "} + {currentRecipe["TranslatedInstructions"]} + + + + Video URL:{" "} + + + Youtube + + + + + + + + + + ); +}; + +export default MealPlanRecipeList \ No newline at end of file diff --git a/Code/frontend/src/components/Navbar.js b/Code/frontend/src/components/Navbar.js index e4b582e6..24d372e1 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,100 @@ 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 handleMealPlan = () => { + props.handleMealPlan(); } - 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 + Meal Plan + Logout +
+ + ) : ( + + )}
- ) -} \ No newline at end of file + ); +} 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/Rating.js b/Code/frontend/src/components/Rating.js new file mode 100644 index 00000000..8b0b444b --- /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..190a8a3c 100644 --- a/Code/frontend/src/components/RecipeCard.js +++ b/Code/frontend/src/components/RecipeCard.js @@ -1,50 +1,131 @@ 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); + 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); + // Show an error message to the user + toast({ + title: "Error", + description: error.message || "Failed to save 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']} - 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 f4c2142d..821e6c87 100644 --- a/Code/frontend/src/components/RecipeList.js +++ b/Code/frontend/src/components/RecipeList.js @@ -1,9 +1,25 @@ 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"; // 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 ; @@ -11,49 +27,144 @@ const RecipeList = ({ recipes }) => { // ) // }); - 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) - } + 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: {currentRecipe['Recipe-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 - + @@ -61,6 +172,6 @@ const RecipeList = ({ recipes }) => { - ) + ); }; 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; diff --git a/Code/frontend/src/components/UserMealPlan.js b/Code/frontend/src/components/UserMealPlan.js new file mode 100644 index 00000000..b7270e5d --- /dev/null +++ b/Code/frontend/src/components/UserMealPlan.js @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; +import { Heading, Flex, Button, Spacer } from "@chakra-ui/react" +import recipeDB from "../apis/recipeDB"; +import MealPlanRecipeList from "./MealPlanRecipeList"; +import AddToPlanModal from "./AddToPlanModal"; + +const UserMealPlan = (props) => { + const [mealPlan, setMealPlan] = useState({}) + const [bookmarks, setBookmarks] = useState([]) + const [refresh, setRefresh] = useState(false) + useEffect(() => { + const plan = recipeDB.get("/recipes/mealPlan", { + params: { + userName: localStorage.getItem("userName") + } + }) + plan.then(res => { + console.log(res) + if (res.data) { + setMealPlan(res.data) + } + }) + const bks = recipeDB.get("/recipes/getBookmarks", { + params: { + userName: localStorage.getItem("userName") + } + }) + bks.then(res => { + if (res.data) { + console.log(res.data) + setBookmarks(res.data.bookmarks) + } + }) + }, [refresh]) + const handleClick = () => { + props.handleProfileView() + } + const updateMealPlan = () => { + setRefresh(!refresh) + } + return ( + <> + + Meal Plan for {props.user.userName} + + + + + + + ) +} + +export default UserMealPlan; \ No newline at end of file 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 00000000..d8989a8b Binary files /dev/null and b/Code/frontend/src/components/componentImages/Empty_star.png differ 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 00000000..98649b62 Binary files /dev/null and b/Code/frontend/src/components/componentImages/Filled_star.png differ 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 00000000..b2161cda Binary files /dev/null and b/Code/frontend/src/components/componentImages/Filled_star_to_rate.png differ 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 00000000..d9209232 Binary files /dev/null and b/Code/frontend/src/components/componentImages/Half_star.png differ