├── .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 |
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 |
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 |
--------------------------------------------------------------------------------