├── .gitignore ├── client ├── images │ └── akimichi-logo.png ├── styles │ ├── _variables.scss │ └── app.scss ├── components │ ├── Recipe.jsx │ ├── FoodLocation.jsx │ ├── FindRecipe.jsx │ ├── ShoppingList.jsx │ ├── Navbar.jsx │ ├── Login.jsx │ └── FoodItem.jsx ├── index.js ├── index.html └── App.jsx ├── server ├── middleware │ ├── notFoundError.js │ ├── authenticateUser.js │ └── globalErrorHandler.js ├── errors │ ├── notFound.js │ ├── allErrors.js │ ├── badRequest.js │ └── unauthenticated.js ├── db │ └── connect.js ├── routes │ ├── authRouter.js │ ├── recipeRouter.js │ ├── foodItemsRouter.js │ ├── foodLocRouter.js │ └── shopListRouter.js ├── models │ ├── FoodLocation.js │ ├── RecipeItem.js │ ├── ShoppingListItem.js │ ├── FoodItem.js │ └── User.js ├── controllers │ ├── authController.js │ ├── recipeController.js │ ├── foodLocController.js │ ├── shopListController.js │ └── foodItemsController.js └── server.js ├── webpack.config.js ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env -------------------------------------------------------------------------------- /client/images/akimichi-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkymn10/EATME/HEAD/client/images/akimichi-logo.png -------------------------------------------------------------------------------- /client/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $lightOrange: #FFCA08; 2 | $darkOrange: #F57921; 3 | $lightBlue: #b4ecac; 4 | $darkBlue: #398BD4; -------------------------------------------------------------------------------- /server/middleware/notFoundError.js: -------------------------------------------------------------------------------- 1 | const notFound = (req, res) => res.status(404).send('Route does not exist'); 2 | 3 | module.exports = notFound; 4 | -------------------------------------------------------------------------------- /client/components/Recipe.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Navbar from './Navbar.jsx'; 3 | 4 | 5 | 6 | const Recipe = () => { 7 | return ( 8 |
9 | 10 |

RECIPE

11 |
12 | ) 13 | } 14 | 15 | export default Recipe; -------------------------------------------------------------------------------- /server/errors/notFound.js: -------------------------------------------------------------------------------- 1 | const { StatusCodes } = require('http-status-codes'); 2 | 3 | class NotFoundError extends Error { 4 | constructor(message) { 5 | super(message); 6 | this.statusCode = StatusCodes.NOT_FOUND; 7 | } 8 | } 9 | 10 | module.exports = NotFoundError; 11 | -------------------------------------------------------------------------------- /client/components/FoodLocation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Navbar from './Navbar.jsx'; 3 | 4 | 5 | 6 | const FoodLocation = () => { 7 | return ( 8 |
9 | 10 |

LOCATION

11 |
12 | ) 13 | } 14 | 15 | export default FoodLocation; -------------------------------------------------------------------------------- /server/errors/allErrors.js: -------------------------------------------------------------------------------- 1 | const UnauthenticatedError = require('./unauthenticated'); 2 | const NotFoundError = require('./notFound'); 3 | const BadRequestError = require('./badRequest'); 4 | 5 | module.exports = { 6 | UnauthenticatedError, 7 | NotFoundError, 8 | BadRequestError, 9 | }; 10 | -------------------------------------------------------------------------------- /server/errors/badRequest.js: -------------------------------------------------------------------------------- 1 | const { StatusCodes } = require('http-status-codes'); 2 | 3 | class BadRequestError extends Error { 4 | constructor(message) { 5 | super(message); 6 | this.statusCode = StatusCodes.BAD_REQUEST; 7 | } 8 | } 9 | 10 | module.exports = BadRequestError; 11 | -------------------------------------------------------------------------------- /client/components/FindRecipe.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Navbar from './Navbar.jsx'; 3 | 4 | 5 | 6 | 7 | const FindRecipe = () => { 8 | return ( 9 |
10 | 11 |

FIND RECIPE

12 |
13 | ) 14 | } 15 | 16 | export default FindRecipe; -------------------------------------------------------------------------------- /server/db/connect.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | mongoose.set('strictQuery', false); 4 | 5 | const connectDB = (url) => { 6 | return mongoose.connect(url, { 7 | useNewUrlParser: true, 8 | useUnifiedTopology: true, 9 | }); 10 | }; 11 | 12 | module.exports = connectDB; 13 | -------------------------------------------------------------------------------- /client/components/ShoppingList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Navbar from './Navbar.jsx'; 3 | 4 | 5 | 6 | 7 | const ShoppingList = () => { 8 | return ( 9 |
10 | 11 |

SHOPPING LIST

12 |
13 | ) 14 | } 15 | 16 | export default ShoppingList; -------------------------------------------------------------------------------- /server/errors/unauthenticated.js: -------------------------------------------------------------------------------- 1 | const { StatusCodes } = require('http-status-codes'); 2 | 3 | class UnauthenticatedError extends Error { 4 | constructor(message) { 5 | super(message); 6 | this.statusCode = StatusCodes.UNAUTHORIZED; 7 | } 8 | } 9 | 10 | module.exports = UnauthenticatedError; 11 | -------------------------------------------------------------------------------- /server/routes/authRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const { register, login } = require('../controllers/authController'); 5 | 6 | router.post('/register', register); 7 | router.post('/login', login); 8 | 9 | // TODO: logout router 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './App.jsx'; 5 | 6 | import styles from './styles/app.scss'; 7 | 8 | render ( 9 | 10 | 11 | , 12 | document.getElementById('root'), 13 | ); -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | EATME 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /server/routes/recipeRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const { 5 | getAllRecipes, 6 | getRecipe, 7 | createRecipe, 8 | updateRecipe, 9 | deleteRecipe, 10 | } = require('../controllers/recipeController'); 11 | 12 | router.route('/').post(createRecipe).get(getAllRecipes); 13 | router.route('/:id').get(getRecipe).patch(updateRecipe).delete(deleteRecipe); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /server/routes/foodItemsRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const { 5 | getAllFoodItems, 6 | getFoodItem, 7 | createFoodItem, 8 | updateFoodItem, 9 | deleteFoodItem, 10 | } = require('../controllers/foodItemsController'); 11 | 12 | router.route('/').post(createFoodItem).get(getAllFoodItems); 13 | router 14 | .route('/:id') 15 | .get(getFoodItem) 16 | .patch(updateFoodItem) 17 | .delete(deleteFoodItem); 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /server/models/FoodLocation.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const foodLocationSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: [true, 'Please provide location of food item.'], 8 | trim: true, 9 | }, 10 | createdBy: { 11 | type: mongoose.Types.ObjectId, 12 | ref: 'User', 13 | required: [true, 'Please provide user.'], 14 | }, 15 | }, 16 | { timestamps: true } 17 | ); 18 | 19 | module.exports = mongoose.model('FoodLocation', foodLocationSchema); 20 | -------------------------------------------------------------------------------- /server/routes/foodLocRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const { 5 | getAllFoodLocations, 6 | getFoodLocation, 7 | createFoodLocation, 8 | updateFoodLocation, 9 | deleteFoodLocation, 10 | } = require('../controllers/foodLocController'); 11 | 12 | router.route('/').post(createFoodLocation).get(getAllFoodLocations); 13 | router 14 | .route('/:id') 15 | .get(getFoodLocation) 16 | .patch(updateFoodLocation) 17 | .delete(deleteFoodLocation); 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /server/routes/shopListRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const { 5 | getAllShopListItems, 6 | getShopListItem, 7 | createShopListItem, 8 | updateShopListItem, 9 | deleteShopListItem, 10 | } = require('../controllers/shopListController'); 11 | 12 | router.route('/').post(createShopListItem).get(getAllShopListItems); 13 | router 14 | .route('/:id') 15 | .get(getShopListItem) 16 | .patch(updateShopListItem) 17 | .delete(deleteShopListItem); 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /server/models/RecipeItem.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const recipeItemSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: [true, 'Please provide the name of the recipe.'], 8 | trim: true, 9 | }, 10 | link: { 11 | type: String, 12 | required: [true, 'Please provide the link to the recipe.'], 13 | trim: true, 14 | }, 15 | favorite: { 16 | type: Boolean, 17 | default: false, 18 | }, 19 | createdBy: { 20 | type: mongoose.Types.ObjectId, 21 | ref: 'User', 22 | required: [true, 'Please provide user.'], 23 | }, 24 | }, 25 | { timestamps: true } 26 | ); 27 | 28 | module.exports = mongoose.model('RecipeItem', recipeItemSchema); 29 | -------------------------------------------------------------------------------- /server/models/ShoppingListItem.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const shoppingListItemSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: [ 8 | true, 9 | 'Please provide name of ingredient to add to your shopping list.', 10 | ], 11 | trim: true, 12 | }, 13 | description: { 14 | type: String, 15 | trim: true 16 | }, 17 | quantity: { 18 | type: Number, 19 | default: 1, 20 | }, 21 | boughtAt: { 22 | type: String, 23 | trim: true 24 | }, 25 | bought: { 26 | type: Boolean, 27 | default: false, 28 | }, 29 | createdBy: { 30 | type: mongoose.Types.ObjectId, 31 | ref: 'User', 32 | required: [true, 'Please provide user.'], 33 | }, 34 | }, 35 | { timestamps: true } 36 | ); 37 | 38 | module.exports = mongoose.model('ShoppingListItem', shoppingListItemSchema); 39 | -------------------------------------------------------------------------------- /server/models/FoodItem.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const foodItemSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: [true, 'Please provide name of food item.'], 8 | trim: true, 9 | }, 10 | description: { 11 | type: String, 12 | trim: true, 13 | }, 14 | expirationDate: { 15 | type: Date, 16 | required: [true, 'Please provide an expiration date.'], 17 | }, 18 | quantity: { 19 | type: Number, 20 | default: 1, 21 | }, 22 | location: { 23 | type: String, 24 | required: [true, 'Please provide where you store this food item.'], 25 | trim: true, 26 | }, 27 | expired: { 28 | type: Boolean, 29 | default: false, 30 | }, 31 | createdBy: { 32 | type: mongoose.Types.ObjectId, 33 | ref: 'User', 34 | required: [true, 'Please provide user.'], 35 | }, 36 | }, 37 | { timestamps: true } 38 | ); 39 | 40 | module.exports = mongoose.model('FoodItem', foodItemSchema); 41 | -------------------------------------------------------------------------------- /server/middleware/authenticateUser.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/User'); 2 | const jwt = require('jsonwebtoken'); 3 | const { UnauthenticatedError } = require('../errors/allErrors'); 4 | 5 | const authorize = async (req, res, next) => { 6 | // console.log('IN AUTHORIZE'); 7 | // check the authorization header 8 | const authHeader = req.headers.authorization; 9 | // console.log('authheader:' , authHeader); 10 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 11 | throw new UnauthenticatedError('Invalid authentication.'); 12 | } 13 | // split 'Bearer [jwt token here]' -> [Bearer, token] 14 | const token = authHeader.split(' ')[1]; 15 | try { 16 | // verify jwt and return payload 17 | // if signature is not valid, throw error 18 | const payload = jwt.verify(token, process.env.JWT_SECRET); 19 | // attach the user to the job routes (or req object) 20 | req.user = { userId: payload.userId, name: payload.name }; 21 | return next(); 22 | } catch (error) { 23 | throw new UnauthenticatedError('Invalid authentication.'); 24 | } 25 | }; 26 | 27 | module.exports = authorize; 28 | -------------------------------------------------------------------------------- /client/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import axios from 'axios'; 3 | // const result = axios.get('/api/fooditems') 4 | import { Link } from 'react-router-dom'; 5 | 6 | const Navbar = () => { 7 | return ( 8 | 40 | ); 41 | }; 42 | 43 | export default Navbar; 44 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HTMLWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './client/index.js', 6 | 7 | output: { 8 | path: path.join(__dirname, '/build'), 9 | filename: 'bundle.js', 10 | }, 11 | 12 | plugins: [ 13 | new HTMLWebpackPlugin({ 14 | template: './client/index.html', 15 | }), 16 | ], 17 | 18 | devServer: { 19 | static: { 20 | directory: path.join(__dirname, 'build'), 21 | publicPath: '/build', 22 | }, 23 | proxy: { 24 | '/api': 'http://localhost:3000', 25 | }, 26 | }, 27 | 28 | module: { 29 | rules: [ 30 | { 31 | test: /.(js|jsx)$/, 32 | exclude: /node_modules/, 33 | use: { 34 | loader: 'babel-loader', 35 | options: { 36 | presets: ['@babel/preset-env', '@babel/preset-react'], 37 | }, 38 | }, 39 | }, 40 | { 41 | test: /\.s?css/, 42 | exclude: /node_modules/, 43 | use: ['style-loader', 'css-loader', 'sass-loader'], 44 | }, 45 | { 46 | test: /\.(png|jpe?g|gif)$/i, 47 | use: [ 48 | { 49 | loader: 'file-loader', 50 | }, 51 | ], 52 | } 53 | ], 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Routes, Route } from 'react-router-dom'; 3 | import Navbar from './components/Navbar.jsx'; 4 | import Login from './components/Login.jsx'; 5 | import FoodItem from './components/FoodItem.jsx'; 6 | import FoodLocation from './components/FoodLocation.jsx'; 7 | import Recipe from './components/Recipe.jsx'; 8 | import ShoppingList from './components/ShoppingList.jsx'; 9 | import FindRecipe from './components/FindRecipe.jsx'; 10 | 11 | const App = (props) => { 12 | return ( 13 |
14 | {/* */} 15 | 16 | } /> 17 | } /> 18 | } /> 19 | } /> 20 | } /> 21 | } /> 22 | 23 | {/* 24 | 25 | 26 | 27 | }> 28 | */} 29 |
30 | ); 31 | }; 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /server/middleware/globalErrorHandler.js: -------------------------------------------------------------------------------- 1 | const { StatusCodes } = require('http-status-codes'); 2 | const globalErrorHandlerMiddleware = (err, req, res, next) => { 3 | let customError = { 4 | // set default status code 5 | statusCode: err.statusCode || StatusCodes.INTERNAL_SERVER_ERROR, 6 | message: err.message || 'In global error handler, something went wrong', 7 | }; 8 | 9 | // error during register: if name or email or password isn't filled out 10 | if (err.name === 'ValidationError') { 11 | console.log(Object.values(err.errors)); 12 | customError.message = Object.values(err.errors) 13 | .map((item) => item.message) 14 | .join(','); 15 | customError.statusCode = 400; 16 | } 17 | 18 | // duplicate email error 19 | if (err.code && err.code === 11000) { 20 | customError.message = `Duplicate value entered for ${Object.keys( 21 | err.keyValue 22 | )} field. Please choose another value.`; 23 | customError.statusCode = 400; 24 | } 25 | 26 | // error if you try to get/ find something that doesn't exist 27 | if (err.name === 'CastError') { 28 | customError.message = `No item found with id: ${err.value}.`; 29 | customError.statusCode = 404; 30 | } 31 | 32 | // return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ err }) 33 | return res.status(customError.statusCode).json({ message: customError.message }); 34 | }; 35 | 36 | module.exports = globalErrorHandlerMiddleware; 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EatMe 2 | Personal Food Manager Application 3 | (_Currently REST API only, UI/UX still in progress_) 4 | 5 | ### Background and Motivation: 6 | 7 | With limited kitchen space to store all of your groceries, it becomes very easy to forget about certain food items, especially if they are placed in the very back of a storage area (for example, a refrigerator or pantry) and not in plain sight. 8 | 9 | EATME is designed to help you manage your groceries so that all of your food items are consumed before expiration. 10 | 11 | ### Current Features: 12 | 13 | - JWT-based authentication/ authorization with password hashing using bcrypt 14 | - CRUD functionalities for food items, storage areas, shopping lists, and recipes with sorting, searching/ filtering, and pagination implemented 15 | - Single page application routing set up with minimalist UI for food items 16 | 17 | ### Future iterations recommendations: 18 | 19 | - Develop UI/UX further to utilize authentication/ authorization and CRUD functionalities in the API 20 | - Send reminders to user's account email when a food item is close to expiration (or update user schema to include phone numbers, and send text messages as reminders instead) 21 | - Integration of third party APIs to suggest nearby supermarkets that carry specific food items in the user's shopping list or generate recipes that include ingredients based on the user's remaining food items 22 | 23 | ### Current tech stack 24 | 25 | - Node/ Express 26 | - MongoDB 27 | - React, React router 28 | - Webpack 29 | -------------------------------------------------------------------------------- /server/controllers/authController.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/User'); 2 | const { StatusCodes } = require('http-status-codes'); 3 | const { 4 | BadRequestError, 5 | UnauthenticatedError, 6 | } = require('../errors/allErrors'); 7 | 8 | // register user 9 | const register = async (req, res) => { 10 | // req.body will follow User Schema 11 | // see User Model (in models directory) for createJWT and getName functions 12 | const user = await User.create(req.body); 13 | const token = user.createJWT(); 14 | // return jwt token and user's name 15 | return res 16 | .status(StatusCodes.CREATED) 17 | .json({ token, user: { name: user.getName(), email: user.getEmail() } }); 18 | }; 19 | 20 | // login user 21 | // need email and password to login (no need for name in register) 22 | const login = async (req, res) => { 23 | // if email, pw don't exist -> bad request error 24 | // if user doesn't exist -> unauthenticated error 25 | const { email, password } = req.body; 26 | if (!email || !password) { 27 | throw new BadRequestError('Please provide email and password.'); 28 | } 29 | const user = await User.findOne({ email }); 30 | if (!user) { 31 | throw new UnauthenticatedError('Invalid credentials.'); 32 | } 33 | const isPasswordCorrect = await user.comparePassword(password); 34 | if (!isPasswordCorrect) { 35 | throw new UnauthenticatedError('Invalid credentials.'); 36 | } 37 | 38 | // return jwt token and username 39 | const token = user.createJWT(); 40 | return res.status(StatusCodes.OK).json({ token, user: { name: user.name } }); 41 | }; 42 | 43 | // TODO: create logout method 44 | 45 | module.exports = { 46 | register, 47 | login, 48 | }; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eatme", 3 | "version": "1.0.0", 4 | "description": "App that reminds you to eat your food before expiration", 5 | "main": "server/server.js", 6 | "scripts": { 7 | "build": "webpack --mode production", 8 | "dev": "webpack-dev-server --mode development --open --hot", 9 | "start": "nodemon server/server.js", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/mkymn10/EATME.git" 15 | }, 16 | "keywords": [], 17 | "author": "mkymn10", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/mkymn10/EATME/issues" 21 | }, 22 | "homepage": "https://github.com/mkymn10/EATME#readme", 23 | "dependencies": { 24 | "axios": "^1.3.4", 25 | "bcryptjs": "^2.4.3", 26 | "cookie-parser": "^1.4.6", 27 | "cors": "^2.8.5", 28 | "css-loader": "^6.7.3", 29 | "dotenv": "^16.0.3", 30 | "express": "^4.18.2", 31 | "express-async-errors": "^3.1.1", 32 | "helmet": "^6.0.1", 33 | "http-status-codes": "^2.2.0", 34 | "jsonwebtoken": "^9.0.0", 35 | "mongoose": "^6.10.0", 36 | "node-sass": "^8.0.0", 37 | "react": "^18.2.0", 38 | "react-dom": "^18.2.0", 39 | "react-router-dom": "^6.8.2", 40 | "sass-loader": "^13.2.0", 41 | "style-loader": "^3.3.1", 42 | "xss-clean": "^0.1.1" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.21.0", 46 | "@babel/preset-env": "^7.20.2", 47 | "@babel/preset-react": "^7.18.6", 48 | "babel-loader": "^9.1.2", 49 | "file-loader": "^6.2.0", 50 | "html-webpack-plugin": "^5.5.0", 51 | "nodemon": "^2.0.20", 52 | "webpack": "^5.75.0", 53 | "webpack-cli": "^5.0.1", 54 | "webpack-dev-server": "^4.11.1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /server/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const bcrypt = require('bcryptjs'); 3 | const jwt = require('jsonwebtoken'); 4 | 5 | const UserSchema = new mongoose.Schema({ 6 | name: { 7 | type: String, 8 | trim: true, 9 | required: [true, 'Please provide name.'], 10 | maxlength: [50, 'Name cannot be more than 50 characters.'], 11 | }, 12 | email: { 13 | type: String, 14 | required: [true, 'Please provide email.'], 15 | match: [ 16 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 17 | 'Please provide a valid email.', 18 | ], 19 | unique: [ 20 | true, 21 | 'An account with this email already exists. Please provide a different email.', 22 | ], 23 | trim: true, 24 | }, 25 | password: { 26 | type: String, 27 | required: [true, 'Please provide password.'], 28 | minlength: [8, 'Password must be at least 8 characters long.'], 29 | }, 30 | }); 31 | 32 | UserSchema.pre('save', async function (next) { 33 | // generate salt and hash password 34 | const salt = await bcrypt.genSalt(10); 35 | this.password = await bcrypt.hash(this.password, salt); 36 | return next(); 37 | }); 38 | 39 | UserSchema.methods.getName = function () { 40 | return this.name; 41 | }; 42 | 43 | UserSchema.methods.getEmail = function () { 44 | return this.email; 45 | }; 46 | 47 | UserSchema.methods.createJWT = function () { 48 | // sign token 49 | // payload will include _id and name from User Model 50 | console.log(process.env.JWT_DURATION); 51 | return jwt.sign( 52 | { userId: this._id, name: this.name }, 53 | process.env.JWT_SECRET, 54 | { 55 | expiresIn: process.env.JWT_DURATION, 56 | } 57 | ); 58 | }; 59 | 60 | UserSchema.methods.comparePassword = async function (inputPassword) { 61 | const isSamePassword = await bcrypt.compare(inputPassword, this.password); 62 | return isSamePassword; 63 | }; 64 | 65 | module.exports = mongoose.model('User', UserSchema); 66 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | // set up .env and async/await errors in express 2 | require('dotenv').config(); 3 | require('express-async-errors'); 4 | 5 | // extra security packages 6 | // helmet: middleware that helps secure express apps by setting http headers 7 | const helmet = require('helmet'); 8 | // cors (cross origin resource sharing): middleware that can be used to enable cors 9 | const cors = require('cors'); 10 | // xss: helps prevent cross site scripting attacks/ malicious code injections 11 | const xss = require('xss-clean'); 12 | 13 | // enable express: 14 | const express = require('express'); 15 | const app = express(); 16 | 17 | // connect DB (see function in db folder): 18 | const connectDB = require('./db/connect'); 19 | 20 | // authentication middleware 21 | const authenticateUser = require('./middleware/authenticateUser'); 22 | 23 | // error handler middleware 24 | const notFoundError = require('./middleware/notFoundError'); 25 | const globalErrorHandler = require('./middleware/globalErrorHandler'); 26 | 27 | // routers 28 | const authRouter = require('./routes/authRouter'); 29 | const foodItemsRouter = require('./routes/foodItemsRouter'); 30 | const foodLocRouter = require('./routes/foodLocRouter'); 31 | const shopListRouter = require('./routes/shopListRouter'); 32 | const recipeRouter = require('./routes/recipeRouter'); 33 | 34 | // all general purpose middleware 35 | app.use(express.json()); 36 | app.use(helmet()); 37 | app.use(cors()); 38 | app.use(xss()); 39 | 40 | // routes 41 | app.use('/api/authuser', authRouter); 42 | app.use('/api/fooditems', authenticateUser, foodItemsRouter); 43 | // app.use('/api/fooditems', foodItemsRouter); 44 | app.use('/api/foodlocations', authenticateUser, foodLocRouter); 45 | app.use('/api/shoppinglist', authenticateUser, shopListRouter); 46 | app.use('/api/recipes', authenticateUser, recipeRouter); 47 | 48 | // use error handler middleware 49 | // 404 50 | app.use(notFoundError); 51 | // global 52 | app.use(globalErrorHandler); 53 | 54 | const PORT = process.env.PORT || 3000; 55 | 56 | const start = async () => { 57 | try { 58 | await connectDB(process.env.MONGO_URI); 59 | app.listen(PORT, () => { 60 | console.log(`Server is listening on port ${PORT}...`); 61 | }); 62 | } catch (error) { 63 | console.log('Error in starting app:', error); 64 | } 65 | }; 66 | 67 | start(); 68 | -------------------------------------------------------------------------------- /client/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Navbar from './Navbar.jsx'; 3 | import { Navigate } from 'react-router-dom'; 4 | 5 | const Login = () => { 6 | const [loginData, setLoginData] = React.useState({ 7 | email: '', 8 | password: '', 9 | errorMsg: '', 10 | }); 11 | 12 | function handleChange(event) { 13 | console.log('STATE UPDATED'); 14 | const { name, value } = event.target; 15 | setLoginData((prevLoginData) => { 16 | return { 17 | ...prevLoginData, 18 | [name]: value, 19 | }; 20 | }); 21 | } 22 | 23 | function handleSubmit(event) { 24 | event.preventDefault(); 25 | // console.log(loginData); 26 | 27 | fetch('/api/authuser/login', { 28 | method: 'POST', 29 | body: JSON.stringify(loginData), 30 | headers: { 31 | 'Content-Type': 'application/json', 32 | }, 33 | }) 34 | .then((response) => response.json()) 35 | .then((data) => { 36 | console.log('data', data); 37 | // TODO: front end error handling 38 | if (data.token !== undefined) { 39 | setLoginData((prevLoginData) => { 40 | return { 41 | ...prevLoginData, 42 | errorMsg: 'none', 43 | }; 44 | }) 45 | localStorage.setItem('JWT', data.token); 46 | } else { 47 | setLoginData((prevLoginData) => { 48 | return { 49 | ...prevLoginData, 50 | errorMsg: data.message, 51 | }; 52 | }) 53 | } 54 | }) 55 | .catch((error) => { 56 | console.log('Error in Login:', error); 57 | }); 58 | } 59 | 60 | return ( 61 |
62 | {/* */} 63 |
64 |

Login

65 |
66 | 69 | 77 |
78 |
79 | 82 | 90 |
91 | 92 |
93 | {loginData.errorMsg === 'none' && () } 94 |
95 | ); 96 | }; 97 | 98 | export default Login; 99 | -------------------------------------------------------------------------------- /client/components/FoodItem.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import Navbar from './Navbar.jsx'; 3 | import axios from 'axios'; 4 | 5 | 6 | const FoodItem = () => { 7 | 8 | const [result, setResult] = useState([]); 9 | useEffect(() => { 10 | const jwt = localStorage.getItem('JWT'); 11 | console.log('HELLO!!!') 12 | fetch('/api/fooditems', 13 | { 14 | method: 'GET', 15 | // body: JSON.stringify({ result }), 16 | headers: { 17 | 'Content-Type': 'application/json', 18 | 'Authorization': `Bearer ${jwt}` 19 | }, 20 | } 21 | ) 22 | .then(res => res.json()) 23 | .then((data) => { 24 | setResult(data); 25 | }) 26 | .catch(err => console.log('getDetails: ERROR: ', err)); 27 | // axios.get('api/fooditems').then(response => { 28 | // console.log('RESPONSE:', response); 29 | // setResult(response); 30 | // }) 31 | }, []) 32 | console.log('RESULT:', result); 33 | function handleSubmit(e) { 34 | 35 | } 36 | 37 | 38 | 39 | return ( 40 |
41 | 42 |
43 |
44 | 45 | 48 | Search By: 49 |
50 | 58 |
59 |
60 |
61 | Add New Food Item: 62 | 65 |
66 |
67 |
68 |
Item
69 |
Expiration Date
70 |
Quantity
71 |
Location
72 |
Expired
73 |
74 |
75 |
76 |

Name

77 |

Description

78 |
79 |
80 | 81 | 82 |
83 |
84 |
85 | 1/23/2022 86 |
87 |
88 |
89 |
90 | {JSON.stringify(result)} 91 |
92 |
93 | ) 94 | } 95 | 96 | export default FoodItem; -------------------------------------------------------------------------------- /client/styles/app.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | @import '_variables'; 8 | 9 | body { 10 | background: $darkBlue; 11 | } 12 | 13 | /**************** 14 | LOGIN 15 | *****************/ 16 | .login-form { 17 | background-color: $darkBlue; 18 | height: 90vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | 25 | /**************** 26 | NAVBAR 27 | *****************/ 28 | 29 | nav { 30 | background-color: white; 31 | display: flex; 32 | align-items: center; 33 | justify-content: space-between; 34 | height: 90px; 35 | span { 36 | font-size: 3rem; 37 | margin: 1.5rem; 38 | } 39 | } 40 | 41 | .logout-button { 42 | padding: 0.5rem; 43 | margin-right: 1.5rem; 44 | } 45 | 46 | .nav-categories { 47 | list-style-type: none; 48 | display: flex; 49 | align-items: center; 50 | li { 51 | font-size: 1.2rem; 52 | margin: 1.5rem; 53 | } 54 | } 55 | 56 | /**************** 57 | FOOD ITEM 58 | *****************/ 59 | 60 | .search-container { 61 | background-color: pink; 62 | margin: 3rem auto; 63 | height: 50px; 64 | width: 80%; 65 | border-radius: 8px; 66 | display: flex; 67 | align-items: center; 68 | justify-content: space-between; 69 | } 70 | 71 | .search-container-left { 72 | display: flex; 73 | align-items: center; 74 | width: 40%; 75 | 76 | .searchbar { 77 | text-indent: 5px; 78 | height: 60%; 79 | margin: auto 1rem; 80 | margin-right: 0; 81 | width: 100%; 82 | } 83 | 84 | .search-button { 85 | height: 60%; 86 | width: 2.5em; 87 | margin-left: 0; 88 | } 89 | 90 | .searchBy { 91 | margin: 1rem; 92 | margin-right: -15px; 93 | width: 40%; 94 | } 95 | 96 | .searchBy-dropdown { 97 | height: 60%; 98 | width: 75px; 99 | // margin-left: 0; 100 | } 101 | 102 | } 103 | 104 | .search-container-right { 105 | display: flex; 106 | align-items: center; 107 | margin-right: 1rem; 108 | 109 | .add-new { 110 | margin-right: 3px; 111 | } 112 | .add-button { 113 | height: 60%; 114 | width: 1.3rem; 115 | } 116 | } 117 | 118 | .display-items { 119 | background-color: green; 120 | height: 100px; 121 | width: 80%; 122 | margin: auto; 123 | display: grid; 124 | grid-template-columns: 2fr repeat(3, 1fr) 0.35fr; 125 | // text-align: center; 126 | 127 | .display-items-category { 128 | background-color: gray; 129 | border: 1px solid black; 130 | height: 100%; 131 | display: flex; 132 | align-items: center; 133 | justify-content: center; 134 | grid-template-columns: 2fr repeat(3, 1fr) 0.35fr; 135 | grid-template-rows: auto 1fr; 136 | } 137 | 138 | // .grid-alignment { 139 | // // grid-template-columns: 2fr repeat(3, 1fr) 0.35fr; 140 | // } 141 | 142 | .item--FoodItem { 143 | background-color: yellow; 144 | height: 100%; 145 | grid-template-columns: 1fr; 146 | 147 | .FoodItem-Name-Description { 148 | display: flex; 149 | align-items: center; 150 | justify-content: space-between; 151 | 152 | .name-description-container { 153 | height: 100%; 154 | width: 100%; 155 | background-color: greenyellow; 156 | display: flex; 157 | flex-direction: column; 158 | } 159 | 160 | .update-delete-container { 161 | // height: 100%; 162 | // width: 100%; 163 | background-color: darkgreen; 164 | display: flex; 165 | } 166 | } 167 | } 168 | } 169 | 170 | .show-results { 171 | width: 90%; 172 | height: 1000px; 173 | background-color: azure; 174 | margin: auto; 175 | } 176 | 177 | 178 | -------------------------------------------------------------------------------- /server/controllers/recipeController.js: -------------------------------------------------------------------------------- 1 | const Recipe = require('../models/RecipeItem'); 2 | const { StatusCodes } = require('http-status-codes'); 3 | const { BadRequestError, NotFoundError } = require('../errors/allErrors'); 4 | 5 | const getAllRecipes = async (req, res) => { 6 | // search by name, link, favorite? 7 | const { name, link, favorite, sortBy } = req.query; 8 | const queryObj = {}; 9 | if (name) queryObj.name = { $regex: name, $options: 'i' }; 10 | if (link) queryObj.link = { $regex: link, $options: 'i' }; 11 | if (favorite) queryObj.favorite = favorite === 'true' ? true : false; 12 | // find user by id 13 | queryObj.createdBy = req.user.userId; 14 | 15 | let result = Recipe.find(queryObj); 16 | 17 | // sort by name 18 | if (sortBy) { 19 | // example: sortBy = '-name' -> '-name' 20 | // sort by name desc first, then expirationDate 21 | // const sortList = sortBy.split(',').join(' '); 22 | result = result.sort(sortBy); 23 | } else { 24 | result = result.sort('createAt'); 25 | } 26 | 27 | // choose how many locations to display per page 28 | const page = Number(req.query.page) || 1; 29 | const limit = Number(req.query.limit) || 10; 30 | const skip = (page - 1) * limit; 31 | result = result.skip(skip).limit(limit); 32 | const recipes = await result; 33 | return res.status(StatusCodes.OK).json({ recipes, numLoc: recipes.length }); 34 | }; 35 | 36 | const getRecipe = async (req, res) => { 37 | // extract userId from req.user 38 | // extract id from req.params, rename as recipeId 39 | const { 40 | user: { userId }, 41 | params: { id: recipeId }, 42 | } = req; 43 | const recipe = await Recipe.findOne({ 44 | _id: recipeId, 45 | createdBy: userId, 46 | }); 47 | if (!recipe) { 48 | throw new NotFoundError(`No Food Item found with id ${recipeId}.`); 49 | } 50 | return res.status(200).json({ recipe }); 51 | }; 52 | 53 | const createRecipe = async (req, res) => { 54 | // assign createdBy property on req.body to reference user's id 55 | req.body.createdBy = req.user.userId; 56 | const newRecipe = await Recipe.create(req.body); 57 | return res.status(StatusCodes.CREATED).json({ newRecipe }); 58 | }; 59 | 60 | const updateRecipe = async (req, res) => { 61 | // things to update: name, link, favorite 62 | // we don't extract favorite because no error handling required 63 | // extract fields to update from req.body 64 | // extract userId from req.user 65 | // extract id from req.params, rename as recipeId 66 | const { 67 | body: { name, link }, 68 | user: { userId }, 69 | params: { id: recipeId }, 70 | } = req; 71 | 72 | if (name === '') throw new BadRequestError('Name field cannot be empty.'); 73 | if (link === '') throw new BadRequestError('Link field cannot be empty.'); 74 | 75 | const updatedRecipe = await Recipe.findByIdAndUpdate( 76 | { _id: recipeId, createdBy: userId }, 77 | req.body, 78 | { new: true, runValidators: true } 79 | ); 80 | 81 | if (!updatedRecipe) { 82 | throw new NotFoundError(`No Food Item to update with id ${recipeId}.`); 83 | } 84 | 85 | return res.status(StatusCodes.OK).json({ updatedRecipe }); 86 | }; 87 | 88 | const deleteRecipe = async (req, res) => { 89 | // extract userId from req.user 90 | // extract id from req.params, rename as recipeId 91 | const { 92 | user: { userId }, 93 | params: { id: recipeId }, 94 | } = req; 95 | const deletedRecipe = await Recipe.findByIdAndDelete({ 96 | _id: recipeId, 97 | createdBy: userId, 98 | }); 99 | if (!deletedRecipe) { 100 | throw new NotFoundError(`No Food Item to delete with id ${recipeId}.`); 101 | } 102 | return res.status(StatusCodes.OK).json({ deletedRecipe }); 103 | }; 104 | 105 | module.exports = { 106 | getAllRecipes, 107 | getRecipe, 108 | createRecipe, 109 | updateRecipe, 110 | deleteRecipe, 111 | }; 112 | -------------------------------------------------------------------------------- /server/controllers/foodLocController.js: -------------------------------------------------------------------------------- 1 | const FoodLocation = require('../models/FoodLocation'); 2 | const { StatusCodes } = require('http-status-codes'); 3 | const { BadRequestError, NotFoundError } = require('../errors/allErrors'); 4 | 5 | const getAllFoodLocations = async (req, res) => { 6 | // search by name of location? 7 | const { name, sortBy } = req.query; 8 | const queryObj = {}; 9 | // find by user's food locations 10 | if (name) queryObj.name = { $regex: name, $options: 'i' }; 11 | // find user by id 12 | queryObj.createdBy = req.user.userId; 13 | 14 | let result = FoodLocation.find(queryObj); 15 | 16 | // sort by name 17 | if (sortBy) { 18 | // example: sortBy = '-name' -> '-name' 19 | // sort by name desc first, then expirationDate 20 | // const sortList = sortBy.split(',').join(' '); 21 | result = result.sort(sortBy); 22 | } else { 23 | result = result.sort('createAt'); 24 | } 25 | 26 | // choose how many locations to display per page 27 | const page = Number(req.query.page) || 1; 28 | const limit = Number(req.query.limit) || 10; 29 | const skip = (page - 1) * limit; 30 | result = result.skip(skip).limit(limit); 31 | const foodLocations = await result; 32 | return res 33 | .status(StatusCodes.OK) 34 | .json({ foodLocations, numLoc: foodLocations.length }); 35 | }; 36 | 37 | const getFoodLocation = async (req, res) => { 38 | // extract userId from req.user 39 | // extract id from req.params, rename as locationId 40 | const { 41 | user: { userId }, 42 | params: { id: locationId }, 43 | } = req; 44 | const foodLocation = await FoodLocation.findOne({ 45 | _id: locationId, 46 | createdBy: userId, 47 | }); 48 | 49 | if (!foodLocation) { 50 | throw new NotFoundError(`No Food Location found with id ${locationId}.`); 51 | } 52 | 53 | return res.status(200).json({ foodLocation }); 54 | }; 55 | 56 | const createFoodLocation = async (req, res) => { 57 | const { quantity } = req.body; 58 | if (quantity <= 0) 59 | throw new BadRequestError('Quantity must be larger than 0.'); 60 | // assign createdBy property on req.body to reference user's id 61 | req.body.createdBy = req.user.userId; 62 | const newFoodLocation = await FoodLocation.create(req.body); 63 | return res.status(StatusCodes.CREATED).json({ newFoodLocation }); 64 | }; 65 | 66 | const updateFoodLocation = async (req, res) => { 67 | // only thing to update is the name 68 | // extract name from req.body 69 | // extract userId from req.user 70 | // extract id from req.params, rename as locationId 71 | const { 72 | body: { name }, 73 | user: { userId }, 74 | params: { id: locationId }, 75 | } = req; 76 | // if the name field is empty 77 | if (name === '') { 78 | throw new BadRequestError('Name field cannot be empty.'); 79 | } 80 | 81 | const updatedFoodLocation = await FoodLocation.findByIdAndUpdate( 82 | { _id: locationId, createdBy: userId }, 83 | req.body, 84 | { new: true, runValidators: true } 85 | ); 86 | 87 | if (!updatedFoodLocation) { 88 | throw new NotFoundError( 89 | `No Food Location to update with id ${locationId}.` 90 | ); 91 | } 92 | 93 | return res.status(StatusCodes.OK).json({ updatedFoodLocation }); 94 | }; 95 | 96 | const deleteFoodLocation = async (req, res) => { 97 | // extract userId from req.user 98 | // extract id from req.params, rename as locationId 99 | const { 100 | user: { userId }, 101 | params: { id: locationId }, 102 | } = req; 103 | const deletedFoodLocation = await FoodLocation.findByIdAndDelete({ 104 | _id: locationId, 105 | createdBy: userId, 106 | }); 107 | if (!deletedFoodLocation) { 108 | throw new NotFoundError( 109 | `No Food Location to delete with id ${locationId}.` 110 | ); 111 | } 112 | return res.status(StatusCodes.OK).json({ deletedFoodLocation }); 113 | }; 114 | 115 | module.exports = { 116 | getAllFoodLocations, 117 | getFoodLocation, 118 | createFoodLocation, 119 | updateFoodLocation, 120 | deleteFoodLocation, 121 | }; 122 | -------------------------------------------------------------------------------- /server/controllers/shopListController.js: -------------------------------------------------------------------------------- 1 | const ShopListItem = require('../models/ShoppingListItem'); 2 | const { StatusCodes } = require('http-status-codes'); 3 | const { BadRequestError, NotFoundError } = require('../errors/allErrors'); 4 | 5 | const getAllShopListItems = async (req, res) => { 6 | // search by name/ description/ location/ boughtAt, bought(?) of food item? 7 | const { name, description, boughtAt, bought, sortBy, selectedFields } = 8 | req.query; 9 | const queryObj = {}; 10 | if (name) queryObj.name = { $regex: name, $options: 'i' }; 11 | if (description) 12 | queryObj.description = { $regex: description, $options: 'i' }; 13 | if (boughtAt) queryObj.boughtAt = { $regex: boughtAt, $options: 'i' }; 14 | if (bought) queryObj.bought = bought === 'true' ? true : false; 15 | 16 | // find user by id 17 | queryObj.createdBy = req.user.userId; 18 | // choose how many food items to display per page 19 | let result = ShopListItem.find(queryObj); 20 | 21 | // sort by expiration date, name 22 | if (sortBy) { 23 | // example: sortBy = '-name,expirationDate' -> '-name expirationDate' 24 | // sort by name desc first, then expirationDate 25 | const sortList = sortBy.split(',').join(' '); 26 | result = result.sort(sortList); 27 | } else { 28 | result = result.sort('createAt'); 29 | } 30 | 31 | // select only particular fields 32 | if (selectedFields) { 33 | // example: selectedFields = 'name,expirationDate' -> 'name expirationDate' 34 | const fieldsList = selectedFields.split(',').join(' '); 35 | result = result.select(fieldsList); 36 | } 37 | 38 | const page = Number(req.query.page) || 1; 39 | const limit = Number(req.query.limit) || 10; 40 | const skip = (page - 1) * limit; 41 | result = result.skip(skip).limit(limit); 42 | const shopListItem = await result; 43 | return res 44 | .status(StatusCodes.OK) 45 | .json({ shopListItem, numItems: shopListItem.length }); 46 | }; 47 | 48 | const getShopListItem = async (req, res) => { 49 | // extract userId from req.user 50 | // extract id from req.params, rename as shopListId 51 | const { 52 | user: { userId }, 53 | params: { id: shopListId }, 54 | } = req; 55 | const shopListItem = await ShopListItem.findOne({ 56 | _id: shopListId, 57 | createdBy: userId, 58 | }); 59 | if (!shopListItem) { 60 | throw new NotFoundError(`No Shop List Item found with id ${shopListId}.`); 61 | } 62 | return res.status(200).json({ shopListItem }); 63 | }; 64 | 65 | const createShopListItem = async (req, res) => { 66 | // assign createdBy property on req.body to reference user's id 67 | req.body.createdBy = req.user.userId; 68 | const newShopListItem = await ShopListItem.create(req.body); 69 | return res.status(StatusCodes.CREATED).json({ newShopListItem }); 70 | }; 71 | 72 | const updateShopListItem = async (req, res) => { 73 | // things to update: name, description, quantity, boughtAt, bought 74 | // extract fields to update from req.body 75 | // we don't extract bought because no error handling required 76 | // extract userId from req.user 77 | // extract id from req.params, rename as shopListId 78 | const { 79 | body: { name, description, quantity, boughtAt }, 80 | user: { userId }, 81 | params: { id: shopListId }, 82 | } = req; 83 | 84 | if (name === '') throw new BadRequestError('Name field cannot be empty.'); 85 | if (description === '') 86 | throw new BadRequestError('Description field cannot be empty.'); 87 | if (quantity <= 0) 88 | throw new BadRequestError('Quantity must be larger than 0.'); 89 | if (boughtAt === '') 90 | throw new BadRequestError('Buy at field cannot be empty.'); 91 | 92 | const updatedShopListItem = await ShopListItem.findByIdAndUpdate( 93 | { _id: shopListId, createdBy: userId }, 94 | req.body, 95 | { new: true, runValidators: true } 96 | ); 97 | 98 | if (!updatedShopListItem) { 99 | throw new NotFoundError( 100 | `No Shop List Item to update with id ${shopListId}.` 101 | ); 102 | } 103 | 104 | return res.status(StatusCodes.OK).json({ updatedShopListItem }); 105 | }; 106 | 107 | const deleteShopListItem = async (req, res) => { 108 | // extract userId from req.user 109 | // extract id from req.params, rename as shopListId 110 | const { 111 | user: { userId }, 112 | params: { id: shopListId }, 113 | } = req; 114 | const deletedShopListItem = await ShopListItem.findByIdAndDelete({ 115 | _id: shopListId, 116 | createdBy: userId, 117 | }); 118 | if (!deletedShopListItem) { 119 | throw new NotFoundError( 120 | `No Shop List Item to delete with id ${shopListId}.` 121 | ); 122 | } 123 | return res.status(StatusCodes.OK).json({ deletedShopListItem }); 124 | }; 125 | 126 | module.exports = { 127 | getAllShopListItems, 128 | getShopListItem, 129 | createShopListItem, 130 | updateShopListItem, 131 | deleteShopListItem, 132 | }; 133 | -------------------------------------------------------------------------------- /server/controllers/foodItemsController.js: -------------------------------------------------------------------------------- 1 | const FoodItem = require('../models/FoodItem'); 2 | const { StatusCodes } = require('http-status-codes'); 3 | const { BadRequestError, NotFoundError } = require('../errors/allErrors'); 4 | 5 | const getAllFoodItems = async (req, res) => { 6 | // search by name/ description/ location, expired(?) of food item? 7 | const { name, description, location, expired, sortBy, selectedFields } = 8 | req.query; 9 | const queryObj = {}; 10 | if (name) queryObj.name = { $regex: name, $options: 'i' }; 11 | if (description) 12 | queryObj.description = { $regex: description, $options: 'i' }; 13 | if (location) queryObj.location = { $regex: location, $options: 'i' }; 14 | if (expired) queryObj.expired = expired === 'true' ? true : false; 15 | 16 | // find user by id 17 | queryObj.createdBy = req.user.userId; 18 | // choose how many food items to display per page 19 | let result = FoodItem.find(queryObj); 20 | 21 | // sort by expiration date, name 22 | if (sortBy) { 23 | // example: sortBy = '-name,expirationDate' -> '-name expirationDate' 24 | // sort by name desc first, then expirationDate 25 | const sortList = sortBy.split(',').join(' '); 26 | result = result.sort(sortList); 27 | } else { 28 | result = result.sort('createAt'); 29 | } 30 | 31 | // select only particular fields 32 | if (selectedFields) { 33 | // example: selectedFields = 'name,expirationDate' -> 'name expirationDate' 34 | const fieldsList = selectedFields.split(',').join(' '); 35 | result = result.select(fieldsList); 36 | } 37 | 38 | const page = Number(req.query.page) || 1; 39 | const limit = Number(req.query.limit) || 10; 40 | const skip = (page - 1) * limit; 41 | result = result.skip(skip).limit(limit); 42 | const foodItems = await result; 43 | 44 | return res 45 | .status(StatusCodes.OK) 46 | .json({ foodItems, numItems: foodItems.length }); 47 | }; 48 | 49 | const getFoodItem = async (req, res) => { 50 | // extract userId from req.user 51 | // extract id from req.params, rename as foodItemId 52 | const { 53 | user: { userId }, 54 | params: { id: foodItemId }, 55 | } = req; 56 | const foodItem = await FoodItem.findOne({ 57 | _id: foodItemId, 58 | createdBy: userId, 59 | }); 60 | if (!foodItem) { 61 | throw new NotFoundError(`No Food Item found with id ${foodItemId}.`); 62 | } 63 | return res.status(200).json({ foodItem }); 64 | }; 65 | 66 | const createFoodItem = async (req, res) => { 67 | const { quantity } = req.body; 68 | if (quantity <= 0) 69 | throw new BadRequestError('Quantity must be larger than 0.'); 70 | // assign createdBy property on req.body to reference user's id 71 | req.body.createdBy = req.user.userId; 72 | const newFoodItem = await FoodItem.create(req.body); 73 | return res.status(StatusCodes.CREATED).json({ newFoodItem }); 74 | }; 75 | 76 | const updateFoodItem = async (req, res) => { 77 | // things to update: name, description, expirationDate, quantity, location, expired 78 | // we don't extract expired because no error handling required 79 | // extract fields to update from req.body 80 | // extract userId from req.user 81 | // extract id from req.params, rename as foodItemId 82 | const { 83 | body: { name, description, expirationDate, quantity, location }, 84 | user: { userId }, 85 | params: { id: foodItemId }, 86 | } = req; 87 | 88 | if (name === '') throw new BadRequestError('Name field cannot be empty.'); 89 | if (description === '') 90 | throw new BadRequestError('Description field cannot be empty.'); 91 | if (expirationDate === '') 92 | throw new BadRequestError('Expiration date field cannot be empty.'); 93 | if (quantity <= 0) 94 | throw new BadRequestError('Quantity must be larger than 0.'); 95 | if (location === '') 96 | throw new BadRequestError('Location field cannot be empty.'); 97 | 98 | const updatedFoodItem = await FoodItem.findByIdAndUpdate( 99 | { _id: foodItemId, createdBy: userId }, 100 | req.body, 101 | { new: true, runValidators: true } 102 | ); 103 | 104 | if (!updatedFoodItem) { 105 | throw new NotFoundError(`No Food Item to update with id ${foodItemId}.`); 106 | } 107 | 108 | return res.status(StatusCodes.OK).json({ updatedFoodItem }); 109 | }; 110 | 111 | const deleteFoodItem = async (req, res) => { 112 | // extract userId from req.user 113 | // extract id from req.params, rename as foodItemId 114 | const { 115 | user: { userId }, 116 | params: { id: foodItemId }, 117 | } = req; 118 | const deletedFoodItem = await FoodItem.findByIdAndDelete({ 119 | _id: foodItemId, 120 | createdBy: userId, 121 | }); 122 | if (!deletedFoodItem) { 123 | throw new NotFoundError(`No Food Item to delete with id ${foodItemId}.`); 124 | } 125 | return res.status(StatusCodes.OK).json({ deletedFoodItem }); 126 | }; 127 | 128 | module.exports = { 129 | getAllFoodItems, 130 | getFoodItem, 131 | createFoodItem, 132 | updateFoodItem, 133 | deleteFoodItem, 134 | }; 135 | --------------------------------------------------------------------------------