├── .gitignore ├── client ├── public │ ├── robots.txt │ ├── images │ │ ├── banner.jpg │ │ ├── img-signin-bkg.jpg │ │ └── img-signup-bkg.jpg │ └── index.html ├── src │ ├── redux │ │ ├── constants │ │ │ ├── filterConstants.js │ │ │ ├── loadingConstants.js │ │ │ ├── categoryConstants.js │ │ │ ├── cartConstants.js │ │ │ ├── orderConstants.js │ │ │ ├── messageConstants.js │ │ │ └── productConstants.js │ │ ├── actions │ │ │ ├── messageActions.js │ │ │ ├── orderActions.js │ │ │ ├── cartActions.js │ │ │ ├── filterActions.js │ │ │ ├── categoryActions.js │ │ │ └── productActions.js │ │ ├── reducers │ │ │ ├── filterReducers.js │ │ │ ├── loadingReducers.js │ │ │ ├── categoryReducers.js │ │ │ ├── messageReducers.js │ │ │ ├── cartReducers.js │ │ │ ├── productReducers.js │ │ │ └── orderReducers.js │ │ └── store.js │ ├── components │ │ ├── NotFound.js │ │ ├── UserDashboard.js │ │ ├── UserRoute.js │ │ ├── AdminRoute.js │ │ ├── AdminHeader.js │ │ ├── App.css │ │ ├── AdminBody.js │ │ ├── AdminDashboard.js │ │ ├── AdminActionBtns.js │ │ ├── CheckoutForm.js │ │ ├── PlaceOrder.js │ │ ├── ProgressBar.js │ │ ├── App.js │ │ ├── Product.js │ │ ├── Payment.js │ │ ├── Card.js │ │ ├── Home.js │ │ ├── AdminCategoryModal.js │ │ ├── Shop.js │ │ ├── Shipping.js │ │ ├── Header.js │ │ ├── Signin.js │ │ ├── Cart.js │ │ ├── Signup.js │ │ ├── AdminProductModal.js │ │ └── AdminEditProduct.js │ ├── api │ │ ├── product.js │ │ ├── category.js │ │ └── auth.js │ ├── helpers │ │ ├── cookies.js │ │ ├── message.js │ │ ├── localStorage.js │ │ ├── auth.js │ │ └── loading.js │ ├── index.js │ ├── data │ │ └── usaStates.js │ └── serviceWorker.js ├── .gitignore ├── package.json ├── README.md └── .eslintcache ├── uploads ├── 1608677405799.jpg ├── 1616620605480.jpg ├── 1629832518493.jpg ├── 1629832554601.jpg ├── 1629832588006.jpg ├── 1629832620039.jpg ├── 1633705481294.jpg └── 1633706637575.jpg ├── config ├── keys.js └── prod.js ├── routes ├── filter.js ├── category.js ├── payment.js ├── auth.js └── product.js ├── middleware ├── multer.js ├── authenticator.js └── validator.js ├── models ├── Category.js ├── User.js └── Product.js ├── controllers ├── payment.js ├── category.js ├── filter.js ├── auth.js └── product.js ├── database └── db.js ├── server.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | dev.js 3 | 4 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/src/redux/constants/filterConstants.js: -------------------------------------------------------------------------------- 1 | export const GET_NEW_ARRIVALS = 'GET_NEW_ARRIVALS'; 2 | -------------------------------------------------------------------------------- /uploads/1608677405799.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalvaradoas39/restaurant-tutorial/HEAD/uploads/1608677405799.jpg -------------------------------------------------------------------------------- /uploads/1616620605480.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalvaradoas39/restaurant-tutorial/HEAD/uploads/1616620605480.jpg -------------------------------------------------------------------------------- /uploads/1629832518493.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalvaradoas39/restaurant-tutorial/HEAD/uploads/1629832518493.jpg -------------------------------------------------------------------------------- /uploads/1629832554601.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalvaradoas39/restaurant-tutorial/HEAD/uploads/1629832554601.jpg -------------------------------------------------------------------------------- /uploads/1629832588006.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalvaradoas39/restaurant-tutorial/HEAD/uploads/1629832588006.jpg -------------------------------------------------------------------------------- /uploads/1629832620039.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalvaradoas39/restaurant-tutorial/HEAD/uploads/1629832620039.jpg -------------------------------------------------------------------------------- /uploads/1633705481294.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalvaradoas39/restaurant-tutorial/HEAD/uploads/1633705481294.jpg -------------------------------------------------------------------------------- /uploads/1633706637575.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalvaradoas39/restaurant-tutorial/HEAD/uploads/1633706637575.jpg -------------------------------------------------------------------------------- /client/public/images/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalvaradoas39/restaurant-tutorial/HEAD/client/public/images/banner.jpg -------------------------------------------------------------------------------- /client/public/images/img-signin-bkg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalvaradoas39/restaurant-tutorial/HEAD/client/public/images/img-signin-bkg.jpg -------------------------------------------------------------------------------- /client/public/images/img-signup-bkg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalvaradoas39/restaurant-tutorial/HEAD/client/public/images/img-signup-bkg.jpg -------------------------------------------------------------------------------- /client/src/redux/constants/loadingConstants.js: -------------------------------------------------------------------------------- 1 | export const START_LOADING = 'START_LOADING'; 2 | export const STOP_LOADING = 'STOP_LOADING'; 3 | -------------------------------------------------------------------------------- /client/src/redux/constants/categoryConstants.js: -------------------------------------------------------------------------------- 1 | export const GET_CATEGORIES = 'GET_CATEGORIES'; 2 | export const CREATE_CATEGORY = 'CREATE_CATEGORY'; 3 | -------------------------------------------------------------------------------- /config/keys.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./prod.js'); 3 | } else { 4 | module.exports = require('./dev.js'); 5 | } 6 | -------------------------------------------------------------------------------- /client/src/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NotFound = () => { 4 | return

Inside NotFound component

; 5 | }; 6 | 7 | export default NotFound; 8 | -------------------------------------------------------------------------------- /client/src/redux/constants/cartConstants.js: -------------------------------------------------------------------------------- 1 | export const ADD_TO_CART = 'ADD_TO_CART'; 2 | export const DELETE_FROM_CART = 'DELETE_FROM_CART'; 3 | export const CLEAR_CART = 'CLEAR_CART'; 4 | -------------------------------------------------------------------------------- /client/src/components/UserDashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const UserDashboard = () => { 4 | return
Inside UserDashboard
; 5 | }; 6 | 7 | export default UserDashboard; 8 | -------------------------------------------------------------------------------- /client/src/redux/constants/orderConstants.js: -------------------------------------------------------------------------------- 1 | export const SAVE_SHIPPING_ADDRESS = 'SAVE_SHIPPING_ADDRESS'; 2 | export const SAVE_PAYMENT_METHOD = 'SAVE_PAYMENT_METHOD'; 3 | export const CLEAR_ORDER = 'CLEAR_ORDER'; 4 | -------------------------------------------------------------------------------- /config/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | jwtSecret: process.env.JWT_SECRET, 3 | jwtExpire: process.env.JWT_EXPIRE, 4 | stripeSecretKey: process.env.STRIPE_SECRET_KEY, 5 | atlasURI: process.env.ATLAS_URI, 6 | }; 7 | -------------------------------------------------------------------------------- /client/src/redux/constants/messageConstants.js: -------------------------------------------------------------------------------- 1 | export const SHOW_SUCCESS_MESSAGE = 'SHOW_SUCCESS_MESSAGE'; 2 | export const SHOW_ERROR_MESSAGE = 'SHOW_ERROR_MESSAGE'; 3 | export const CLEAR_MESSAGES = 'CLEAR_MESSAGES'; 4 | -------------------------------------------------------------------------------- /client/src/redux/actions/messageActions.js: -------------------------------------------------------------------------------- 1 | import { CLEAR_MESSAGES } from '../constants/messageConstants'; 2 | 3 | export const clearMessages = () => dispatch => { 4 | dispatch({ 5 | type: CLEAR_MESSAGES, 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /client/src/redux/constants/productConstants.js: -------------------------------------------------------------------------------- 1 | export const CREATE_PRODUCT = 'CREATE_PRODUCT'; 2 | export const GET_PRODUCTS = 'GET_PRODUCTS'; 3 | export const GET_PRODUCT = 'GET_PRODUCT'; 4 | export const DELETE_PRODUCT = 'DELETE_PRODUCT'; 5 | -------------------------------------------------------------------------------- /client/src/api/product.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const createProduct = async data => { 4 | const response = await axios.post( 5 | `${process.env.REACT_APP_SERVER_URL}/api/product`, 6 | data 7 | ); 8 | 9 | return response; 10 | }; 11 | -------------------------------------------------------------------------------- /routes/filter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const filterController = require('../controllers/filter'); 4 | 5 | router.get('/', filterController.getNewArrivals); 6 | router.post('/search', filterController.searchByQueryType); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /client/src/helpers/cookies.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | 3 | export const setCookie = (key, value) => { 4 | Cookies.set(key, value, { expires: 1 }); 5 | }; 6 | 7 | export const getCookie = (key) => { 8 | return Cookies.get(key); 9 | }; 10 | 11 | export const deleteCookie = (key) => { 12 | Cookies.remove(key); 13 | }; 14 | -------------------------------------------------------------------------------- /client/src/helpers/message.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const showErrorMsg = (msg) => ( 4 |
5 | {msg} 6 |
7 | ); 8 | 9 | export const showSuccessMsg = (msg) => ( 10 |
11 | {msg} 12 |
13 | ); 14 | -------------------------------------------------------------------------------- /middleware/multer.js: -------------------------------------------------------------------------------- 1 | const multer = require('multer'); 2 | 3 | var storage = multer.diskStorage({ 4 | destination: function (req, file, cb) { 5 | cb(null, 'uploads'); 6 | }, 7 | filename: function (req, file, cb) { 8 | cb(null, `${Date.now()}.jpg`); 9 | }, 10 | }); 11 | 12 | var upload = multer({ storage }); 13 | 14 | module.exports = upload; 15 | -------------------------------------------------------------------------------- /routes/category.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const categoryController = require('../controllers/category'); 4 | const { authenticateJWT } = require('../middleware/authenticator'); 5 | 6 | router.post('/', authenticateJWT, categoryController.create); 7 | router.get('/', categoryController.readAll); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /client/src/components/UserRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navigate, Outlet } from 'react-router-dom'; 3 | import { isAuthenticated } from '../helpers/auth'; 4 | 5 | const UserRoute = () => { 6 | return isAuthenticated() && isAuthenticated().role === 0 ? ( 7 | 8 | ) : ( 9 | 10 | ); 11 | }; 12 | 13 | export default UserRoute; 14 | -------------------------------------------------------------------------------- /routes/payment.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const paymentController = require('../controllers/payment'); 4 | //const { authenticateJWT } = require('../middleware/authenticator'); 5 | 6 | router.post( 7 | '/payment-intent', 8 | //authenticateJWT, 9 | paymentController.create_payment_intent 10 | ); 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /client/src/components/AdminRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navigate, Outlet } from 'react-router-dom'; 3 | import { isAuthenticated } from '../helpers/auth'; 4 | 5 | const AdminRoute = () => { 6 | return isAuthenticated() && isAuthenticated().role === 1 ? ( 7 | 8 | ) : ( 9 | 10 | ); 11 | }; 12 | 13 | export default AdminRoute; 14 | -------------------------------------------------------------------------------- /client/src/components/AdminHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const showHeader = () => ( 4 |
5 |
6 |
7 |
8 |

9 | Dashboard 10 |

11 |
12 |
13 |
14 |
15 | ); 16 | 17 | export default showHeader; 18 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env -------------------------------------------------------------------------------- /models/Category.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const categorySchema = new mongoose.Schema( 4 | { 5 | category: { 6 | type: String, 7 | required: true, 8 | trim: true, 9 | maxlength: 50, 10 | }, 11 | }, 12 | { timestamps: true } 13 | ); 14 | 15 | const Category = mongoose.model('Category', categorySchema); 16 | 17 | module.exports = Category; 18 | -------------------------------------------------------------------------------- /controllers/payment.js: -------------------------------------------------------------------------------- 1 | const { stripeSecretKey } = require('../config/keys'); 2 | const stripe = require('stripe')(stripeSecretKey); 3 | 4 | exports.create_payment_intent = async (req, res) => { 5 | const { total } = req.body; 6 | 7 | const paymentIntent = await stripe.paymentIntents.create({ 8 | amount: total, 9 | currency: 'usd', 10 | }); 11 | 12 | res.status(200).json({ 13 | clientSecret: paymentIntent.client_secret, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/redux/reducers/filterReducers.js: -------------------------------------------------------------------------------- 1 | import { GET_NEW_ARRIVALS } from '../constants/filterConstants'; 2 | 3 | const INITIAL_STATE = { 4 | newArrivals: [], 5 | }; 6 | 7 | const filterReducer = (state = INITIAL_STATE, action) => { 8 | switch (action.type) { 9 | case GET_NEW_ARRIVALS: 10 | return { 11 | newArrivals: [...action.payload], 12 | }; 13 | 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default filterReducer; 20 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './components/App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | import { Provider } from 'react-redux'; 6 | import store from './redux/store'; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById('root')); 9 | 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | serviceWorker.unregister(); 17 | -------------------------------------------------------------------------------- /routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { 4 | signupValidator, 5 | signinValidator, 6 | validatorResult, 7 | } = require('../middleware/validator'); 8 | const { signupController, signinController } = require('../controllers/auth'); 9 | 10 | router.post('/signup', signupValidator, validatorResult, signupController); 11 | router.post('/signin', signinValidator, validatorResult, signinController); 12 | 13 | module.exports = router; 14 | -------------------------------------------------------------------------------- /database/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { atlasURI } = require('../config/keys'); 3 | 4 | const connectDB = async () => { 5 | await mongoose 6 | .connect(atlasURI, { 7 | useNewUrlParser: true, 8 | // useCreateIndex: true, 9 | useUnifiedTopology: true, 10 | }) 11 | .then(() => console.log('Database connection success!!')) 12 | .catch(err => console.log('Database connection failed: ', err)); 13 | }; 14 | 15 | mongoose.set('strictQuery', true); 16 | 17 | module.exports = connectDB; 18 | -------------------------------------------------------------------------------- /client/src/redux/reducers/loadingReducers.js: -------------------------------------------------------------------------------- 1 | import { START_LOADING, STOP_LOADING } from '../constants/loadingConstants'; 2 | 3 | const INITIAL_STATE = { 4 | loading: false, 5 | }; 6 | 7 | const loadingReducer = (state = INITIAL_STATE, action) => { 8 | switch (action.type) { 9 | case START_LOADING: 10 | return { 11 | loading: true, 12 | }; 13 | case STOP_LOADING: 14 | return { 15 | loading: false, 16 | }; 17 | default: 18 | return state; 19 | } 20 | }; 21 | 22 | export default loadingReducer; 23 | -------------------------------------------------------------------------------- /client/src/components/App.css: -------------------------------------------------------------------------------- 1 | .signup-container { 2 | background-size: cover; 3 | background-repeat: no-repeat; 4 | background-position: center; 5 | opacity: 0.8; 6 | } 7 | 8 | .signin-container { 9 | background-size: cover; 10 | background-repeat: no-repeat; 11 | background-position: center; 12 | opacity: 0.8; 13 | } 14 | 15 | .home-page { 16 | height: 100vh; 17 | } 18 | 19 | .banner-image { 20 | background-size: cover; 21 | background-repeat: no-repeat; 22 | background-position: center; 23 | opacity: 1; 24 | height: 60%; 25 | } 26 | -------------------------------------------------------------------------------- /client/src/helpers/localStorage.js: -------------------------------------------------------------------------------- 1 | export const setLocalStorage = (key, value) => { 2 | localStorage.setItem(key, JSON.stringify(value)); 3 | }; 4 | 5 | export const getLocalStorage = key => { 6 | return JSON.parse(localStorage.getItem(key)); 7 | }; 8 | 9 | export const deleteLocalStorage = key => { 10 | localStorage.removeItem(key); 11 | }; 12 | 13 | export const clearCartLocalStorage = next => { 14 | deleteLocalStorage('cart'); 15 | deleteLocalStorage('shippingAddress'); 16 | deleteLocalStorage('paymentMethod'); 17 | 18 | next(); 19 | }; 20 | -------------------------------------------------------------------------------- /client/src/api/category.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const createCategory = async formData => { 4 | const config = { 5 | headers: { 6 | 'Content-Type': 'application/json', 7 | }, 8 | }; 9 | 10 | const response = await axios.post( 11 | `${process.env.REACT_APP_SERVER_URL}/api/category`, 12 | formData, 13 | config 14 | ); 15 | 16 | return response; 17 | }; 18 | 19 | export const getCategories = async () => { 20 | const response = await axios.get( 21 | `${process.env.REACT_APP_SERVER_URL}/api/category` 22 | ); 23 | 24 | return response; 25 | }; 26 | -------------------------------------------------------------------------------- /middleware/authenticator.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const { jwtSecret } = require('../config/keys'); 3 | 4 | exports.authenticateJWT = (req, res, next) => { 5 | const token = req.cookies.token; 6 | 7 | if (!token) { 8 | return res.status(401).json({ 9 | errorMessage: 'No token. Authorization denied', 10 | }); 11 | } 12 | 13 | try { 14 | const decoded = jwt.verify(token, jwtSecret); 15 | 16 | req.user = decoded.user; 17 | 18 | next(); 19 | } catch (err) { 20 | console.log('jwt error: ', err); 21 | res.status(401).json({ 22 | errorMessage: 'Invalid token', 23 | }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /client/src/redux/reducers/categoryReducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_CATEGORIES, 3 | CREATE_CATEGORY, 4 | } from '../constants/categoryConstants'; 5 | 6 | const INITIAL_STATE = { 7 | categories: [], 8 | }; 9 | 10 | const categoryReducers = (state = INITIAL_STATE, action) => { 11 | switch (action.type) { 12 | case GET_CATEGORIES: 13 | return { 14 | ...state, 15 | categories: action.payload, 16 | }; 17 | case CREATE_CATEGORY: 18 | return { 19 | ...state, 20 | categories: [...state.categories, action.payload], 21 | }; 22 | default: 23 | return state; 24 | } 25 | }; 26 | 27 | export default categoryReducers; 28 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema( 4 | { 5 | username: { 6 | type: String, 7 | required: true, 8 | }, 9 | email: { 10 | type: String, 11 | required: true, 12 | }, 13 | password: { 14 | type: String, 15 | required: true, 16 | }, 17 | role: { 18 | type: Number, 19 | default: 0, 20 | }, 21 | }, 22 | { timestamps: true } 23 | ); 24 | 25 | const User = mongoose.model('User', UserSchema); 26 | 27 | module.exports = User; 28 | -------------------------------------------------------------------------------- /client/src/components/AdminBody.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Card from './Card'; 3 | // redux 4 | import { useSelector } from 'react-redux'; 5 | 6 | const AdminBody = () => { 7 | const { products } = useSelector(state => state.products); 8 | 9 | return ( 10 |
11 |
12 |
13 | {products && 14 | products.map(product => ( 15 | 20 | ))} 21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default AdminBody; 28 | -------------------------------------------------------------------------------- /client/src/api/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const signup = async data => { 4 | const config = { 5 | headers: { 6 | 'Content-Type': 'application/json', 7 | }, 8 | }; 9 | 10 | const response = await axios.post( 11 | `${process.env.REACT_APP_SERVER_URL}/api/auth/signup`, 12 | data, 13 | config 14 | ); 15 | 16 | return response; 17 | }; 18 | 19 | export const signin = async data => { 20 | const config = { 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | }, 24 | }; 25 | 26 | const response = await axios.post( 27 | `${process.env.REACT_APP_SERVER_URL}/api/auth/signin`, 28 | data, 29 | config 30 | ); 31 | 32 | return response; 33 | }; 34 | -------------------------------------------------------------------------------- /client/src/helpers/auth.js: -------------------------------------------------------------------------------- 1 | import { setCookie, getCookie, deleteCookie } from './cookies'; 2 | import { 3 | setLocalStorage, 4 | getLocalStorage, 5 | deleteLocalStorage, 6 | } from './localStorage'; 7 | 8 | export const setAuthentication = (token, user) => { 9 | setCookie('token', token); 10 | setLocalStorage('user', user); 11 | }; 12 | 13 | export const isAuthenticated = () => { 14 | if (getCookie('token') && getLocalStorage('user')) { 15 | return getLocalStorage('user'); 16 | } else { 17 | return false; 18 | } 19 | }; 20 | 21 | export const logout = (next) => { 22 | deleteCookie('token'); 23 | deleteLocalStorage('user'); 24 | 25 | next(); 26 | }; 27 | -------------------------------------------------------------------------------- /client/src/redux/reducers/messageReducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | SHOW_SUCCESS_MESSAGE, 3 | SHOW_ERROR_MESSAGE, 4 | CLEAR_MESSAGES, 5 | } from '../constants/messageConstants'; 6 | 7 | const INITIAL_STATE = { 8 | successMsg: '', 9 | errorMsg: '', 10 | }; 11 | 12 | const messsageReducer = (state = INITIAL_STATE, action) => { 13 | switch (action.type) { 14 | case SHOW_SUCCESS_MESSAGE: 15 | return { 16 | ...state, 17 | successMsg: action.payload, 18 | }; 19 | case SHOW_ERROR_MESSAGE: 20 | return { 21 | ...state, 22 | errorMsg: action.payload, 23 | }; 24 | case CLEAR_MESSAGES: 25 | return { 26 | successMsg: '', 27 | errorMsg: '', 28 | }; 29 | default: 30 | return state; 31 | } 32 | }; 33 | 34 | export default messsageReducer; 35 | -------------------------------------------------------------------------------- /client/src/redux/reducers/cartReducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_TO_CART, 3 | DELETE_FROM_CART, 4 | CLEAR_CART, 5 | } from '../constants/cartConstants'; 6 | 7 | let INITIAL_STATE = { 8 | cart: [], 9 | }; 10 | 11 | if (localStorage.getItem('cart')) { 12 | INITIAL_STATE.cart = JSON.parse(localStorage.getItem('cart')); 13 | } else { 14 | INITIAL_STATE.cart = []; 15 | } 16 | 17 | const cartReducer = (state = INITIAL_STATE, action) => { 18 | switch (action.type) { 19 | case ADD_TO_CART: 20 | return { 21 | cart: [...action.payload], 22 | }; 23 | case DELETE_FROM_CART: 24 | return { 25 | cart: [...action.payload], 26 | }; 27 | case CLEAR_CART: 28 | return { 29 | cart: [], 30 | }; 31 | default: 32 | return state; 33 | } 34 | }; 35 | 36 | export default cartReducer; 37 | -------------------------------------------------------------------------------- /routes/product.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { authenticateJWT } = require('../middleware/authenticator'); 4 | const upload = require('../middleware/multer'); 5 | const productController = require('../controllers/product'); 6 | 7 | router.post( 8 | '/', 9 | authenticateJWT, 10 | upload.single('productImage'), 11 | productController.create 12 | ); 13 | 14 | router.get('/', productController.readAll); 15 | router.get('/count', productController.readByCount); 16 | router.get('/:productId', productController.read); 17 | router.put( 18 | '/:productId', 19 | authenticateJWT, 20 | upload.single('productImage'), 21 | productController.update 22 | ); 23 | router.delete('/:productId', authenticateJWT, productController.delete); 24 | 25 | module.exports = router; 26 | -------------------------------------------------------------------------------- /client/src/redux/actions/orderActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | SAVE_SHIPPING_ADDRESS, 3 | SAVE_PAYMENT_METHOD, 4 | CLEAR_ORDER, 5 | } from '../constants/orderConstants'; 6 | 7 | export const saveShippingAddress = data => async dispatch => { 8 | dispatch({ 9 | type: SAVE_SHIPPING_ADDRESS, 10 | payload: data, 11 | }); 12 | 13 | localStorage.setItem('shippingAddress', JSON.stringify(data)); 14 | }; 15 | 16 | export const savePaymentMethod = data => async dispatch => { 17 | // stores payment method into redux 18 | dispatch({ 19 | type: SAVE_PAYMENT_METHOD, 20 | payload: data, 21 | }); 22 | 23 | // stores payment method into localStorage 24 | localStorage.setItem('paymentMethod', JSON.stringify(data)); 25 | }; 26 | 27 | export const clearOrder = () => async dispatch => { 28 | dispatch({ 29 | type: CLEAR_ORDER, 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /client/src/redux/reducers/productReducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | CREATE_PRODUCT, 3 | GET_PRODUCTS, 4 | GET_PRODUCT, 5 | DELETE_PRODUCT, 6 | } from '../constants/productConstants'; 7 | 8 | const INITIAL_STATE = { 9 | products: [], 10 | }; 11 | 12 | const productReducer = (state = INITIAL_STATE, action) => { 13 | switch (action.type) { 14 | case CREATE_PRODUCT: 15 | return { 16 | products: [...state.products, action.payload], 17 | }; 18 | case GET_PRODUCTS: 19 | return { 20 | products: [...action.payload], 21 | }; 22 | case GET_PRODUCT: 23 | return { 24 | product: action.payload, 25 | }; 26 | case DELETE_PRODUCT: 27 | return { 28 | products: state.products.filter( 29 | p => p._id !== action.payload._id 30 | ), 31 | }; 32 | 33 | default: 34 | return state; 35 | } 36 | }; 37 | 38 | export default productReducer; 39 | -------------------------------------------------------------------------------- /models/Product.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { ObjectId } = mongoose.Schema; 3 | 4 | const ProductSchema = new mongoose.Schema( 5 | { 6 | fileName: { 7 | type: 'String', 8 | required: true, 9 | }, 10 | productName: { 11 | type: 'String', 12 | required: true, 13 | trim: true, 14 | maxlength: 60, 15 | }, 16 | productDesc: { 17 | type: 'String', 18 | trim: true, 19 | }, 20 | productPrice: { 21 | type: Number, 22 | required: true, 23 | }, 24 | productCategory: { 25 | type: ObjectId, 26 | ref: 'Category', 27 | required: true, 28 | }, 29 | productQty: { 30 | type: Number, 31 | required: true, 32 | }, 33 | }, 34 | { timestamps: true } 35 | ); 36 | 37 | ProductSchema.index({ productName: 'text' }); 38 | const Product = mongoose.model('Product', ProductSchema); 39 | 40 | module.exports = Product; 41 | -------------------------------------------------------------------------------- /client/src/redux/reducers/orderReducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | SAVE_SHIPPING_ADDRESS, 3 | SAVE_PAYMENT_METHOD, 4 | CLEAR_ORDER, 5 | } from '../constants/orderConstants'; 6 | 7 | const INITIAL_STATE = { 8 | shippingAddress: {}, 9 | paymentMethod: '', 10 | }; 11 | 12 | if (localStorage.getItem('shippingAddress')) { 13 | INITIAL_STATE.shippingAddress = JSON.parse( 14 | localStorage.getItem('shippingAddress') 15 | ); 16 | } else { 17 | INITIAL_STATE.shippingAddress = {}; 18 | } 19 | 20 | const orderReducer = (state = INITIAL_STATE, action) => { 21 | switch (action.type) { 22 | case SAVE_SHIPPING_ADDRESS: 23 | return { 24 | ...state, 25 | shippingAddress: action.payload, 26 | }; 27 | case SAVE_PAYMENT_METHOD: 28 | return { 29 | ...state, 30 | paymentMethod: action.payload, 31 | }; 32 | case CLEAR_ORDER: 33 | return { 34 | shippingAddress: {}, 35 | paymentMethod: '', 36 | }; 37 | default: 38 | return state; 39 | } 40 | }; 41 | 42 | export default orderReducer; 43 | -------------------------------------------------------------------------------- /client/src/components/AdminDashboard.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | // components 3 | import AdminHeader from './AdminHeader'; 4 | import AdminActionBtns from './AdminActionBtns'; 5 | import AdminCategoryModal from './AdminCategoryModal'; 6 | import AdminProductModal from './AdminProductModal'; 7 | import AdminBody from './AdminBody'; 8 | // redux 9 | import { useDispatch } from 'react-redux'; 10 | import { getCategories } from '../redux/actions/categoryActions'; 11 | import { getProducts } from '../redux/actions/productActions'; 12 | 13 | const AdminDashboard = () => { 14 | const dispatch = useDispatch(); 15 | useEffect(() => { 16 | dispatch(getCategories()); 17 | }, [dispatch]); 18 | useEffect(() => { 19 | dispatch(getProducts()); 20 | }, [dispatch]); 21 | 22 | return ( 23 |
24 | 25 | 26 | 27 | 28 | 29 |
30 | ); 31 | }; 32 | 33 | export default AdminDashboard; 34 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const cors = require('cors'); 4 | const morgan = require('morgan'); 5 | const cookieParser = require('cookie-parser'); 6 | const connectDB = require('./database/db'); 7 | const authRoutes = require('./routes/auth'); 8 | const categoryRoutes = require('./routes/category'); 9 | const productRoutes = require('./routes/product'); 10 | const filterRoutes = require('./routes/filter'); 11 | const paymentRoutes = require('./routes/payment'); 12 | 13 | // middleware 14 | app.use(cors()); 15 | app.use(morgan('dev')); 16 | app.use(express.json()); 17 | app.use(cookieParser()); 18 | app.use('/api/auth', authRoutes); 19 | app.use('/api/category', categoryRoutes); 20 | app.use('/api/product', productRoutes); 21 | app.use('/uploads', express.static('uploads')); 22 | app.use('/api/filter', filterRoutes); 23 | app.use('/api/payment', paymentRoutes); 24 | 25 | connectDB(); 26 | 27 | const port = process.env.PORT || 5000; 28 | 29 | app.listen(port, () => console.log(`Listening on port ${port}`)); 30 | -------------------------------------------------------------------------------- /client/src/components/AdminActionBtns.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const AdminActionBtns = () => ( 4 |
5 |
6 |
7 |
8 | 15 |
16 | 17 |
18 | 25 |
26 | 27 |
28 | 31 |
32 |
33 |
34 |
35 | ); 36 | 37 | export default AdminActionBtns; 38 | -------------------------------------------------------------------------------- /client/src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, applyMiddleware, createStore } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { composeWithDevTools } from 'redux-devtools-extension'; 4 | import loadingReducer from './reducers/loadingReducers'; 5 | import messageReducer from './reducers/messageReducers'; 6 | import categoryReducer from './reducers/categoryReducers'; 7 | import productReducer from './reducers/productReducers'; 8 | import filterReducer from './reducers/filterReducers'; 9 | import cartReducer from './reducers/cartReducers'; 10 | import orderReducer from './reducers/orderReducers'; 11 | 12 | const reducer = combineReducers({ 13 | loading: loadingReducer, 14 | messages: messageReducer, 15 | categories: categoryReducer, 16 | products: productReducer, 17 | filters: filterReducer, 18 | cart: cartReducer, 19 | order: orderReducer, 20 | }); 21 | 22 | const initialState = {}; 23 | 24 | const middleware = [thunk]; 25 | 26 | const store = createStore( 27 | reducer, 28 | initialState, 29 | composeWithDevTools(applyMiddleware(...middleware)) 30 | ); 31 | 32 | export default store; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restaurant-tutorial", 3 | "version": "1.0.0", 4 | "description": "Tutorial for restaurant site using MERN stack", 5 | "main": "server.js", 6 | "engines": { 7 | "node": "14.17.1", 8 | "npm": "9.6.5" 9 | }, 10 | "scripts": { 11 | "start": "node server.js", 12 | "start-server": "nodemon server.js --ignore './client/'", 13 | "start-client": "npm start --prefix client", 14 | "dev": "concurrently \"npm run start-server\" \"npm run start-client\"" 15 | }, 16 | "author": "Jorge Alvarado", 17 | "license": "MIT", 18 | "dependencies": { 19 | "bcryptjs": "^2.4.3", 20 | "cookie-parser": "^1.4.6", 21 | "cors": "^2.8.5", 22 | "express": "^4.18.2", 23 | "express-validator": "^7.0.1", 24 | "jsonwebtoken": "^9.0.0", 25 | "mongoose": "^7.1.0", 26 | "multer": "^1.4.4", 27 | "nodemon": "^2.0.22", 28 | "react-redux": "^8.0.5", 29 | "redux": "^4.2.1", 30 | "redux-devtools-extension": "^2.13.9", 31 | "redux-thunk": "^2.4.2", 32 | "stripe": "^12.3.0" 33 | }, 34 | "devDependencies": { 35 | "concurrently": "^8.0.1", 36 | "minimist": "^1.2.8", 37 | "morgan": "^1.10.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /middleware/validator.js: -------------------------------------------------------------------------------- 1 | const { check, validationResult } = require('express-validator'); 2 | 3 | exports.signupValidator = [ 4 | check('username').not().isEmpty().trim().withMessage('All fields required'), 5 | check('email').isEmail().normalizeEmail().withMessage('Invalid email'), 6 | check('password') 7 | .isLength({ min: 6 }) 8 | .withMessage('Password must be at least 6 characters long'), 9 | ]; 10 | 11 | exports.signinValidator = [ 12 | check('email').isEmail().normalizeEmail().withMessage('Invalid email'), 13 | check('password') 14 | .isLength({ min: 6 }) 15 | .withMessage('Password must be at least 6 characters long'), 16 | ]; 17 | 18 | exports.validatorResult = (req, res, next) => { 19 | const result = validationResult(req); 20 | const hasErrors = !result.isEmpty(); 21 | 22 | if (hasErrors) { 23 | const firstError = result.array()[0].msg; 24 | return res.status(400).json({ 25 | errorMessage: firstError, 26 | }); 27 | 28 | // console.log('hasErrors: ', hasErrors); 29 | // console.log('result: ', result); 30 | } 31 | 32 | next(); 33 | }; 34 | -------------------------------------------------------------------------------- /controllers/category.js: -------------------------------------------------------------------------------- 1 | const Category = require('../models/Category'); 2 | 3 | exports.create = async (req, res) => { 4 | const { category } = req.body; 5 | 6 | try { 7 | const categoryExist = await Category.findOne({ category }); 8 | if (categoryExist) { 9 | return res.status(400).json({ 10 | errorMessage: `${category} already exists`, 11 | }); 12 | } 13 | 14 | let newCategory = new Category(); 15 | newCategory.category = category; 16 | 17 | newCategory = await newCategory.save(); 18 | 19 | res.status(200).json({ 20 | category: newCategory, 21 | successMessage: `${newCategory.category} was created!`, 22 | }); 23 | } catch (err) { 24 | console.log('category create error: ', err); 25 | res.status(500).json({ 26 | errorMessage: 'Please try again later', 27 | }); 28 | } 29 | }; 30 | 31 | exports.readAll = async (req, res) => { 32 | try { 33 | const categories = await Category.find({}); 34 | 35 | res.status(200).json({ 36 | categories, 37 | }); 38 | } catch (err) { 39 | console.log('category readAll error: ', err); 40 | res.status(500).json({ 41 | errorMessage: 'Please try again later', 42 | }); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /client/src/helpers/loading.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | 3 | export const showLoading = () => ( 4 | 5 |
6 | Loading... 7 |
8 |
9 | Loading... 10 |
11 |
12 | Loading... 13 |
14 |
15 | Loading... 16 |
17 |
18 | Loading... 19 |
20 |
21 | Loading... 22 |
23 |
24 | Loading... 25 |
26 |
27 | ); 28 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@stripe/react-stripe-js": "^2.1.0", 7 | "@stripe/stripe-js": "^1.52.1", 8 | "@testing-library/jest-dom": "^5.16.5", 9 | "@testing-library/user-event": "^14.4.3", 10 | "axios": "^1.4.0", 11 | "js-cookie": "^3.0.5", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-redux": "^8.0.5", 15 | "react-router-dom": "^6.11.0", 16 | "react-scripts": "^5.0.1", 17 | "redux": "^4.2.1", 18 | "redux-thunk": "^2.4.2", 19 | "validator": "^13.9.0" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.3%", 33 | "not ie 11", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | ">0.3%", 39 | "not ie 11", 40 | "not dead", 41 | "not op_mini all" 42 | ] 43 | }, 44 | "devDependencies": { 45 | "@testing-library/react": "^14.0.0", 46 | "redux-devtools-extension": "^2.13.9" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /controllers/filter.js: -------------------------------------------------------------------------------- 1 | const Product = require('../models/Product'); 2 | 3 | exports.getNewArrivals = async (req, res) => { 4 | const sortBy = req.query.sortBy ? req.query.sortBy : 'desc'; 5 | const limit = req.query.limit ? parseInt(req.query.limit) : parseInt(3); 6 | 7 | try { 8 | const newArrivals = await Product.find({}) 9 | .sort({ createdAt: sortBy }) 10 | .limit(limit); 11 | 12 | res.json({ 13 | newArrivals, 14 | }); 15 | } catch (err) { 16 | console.log(err, 'filter Controller.getNewArrivals error'); 17 | res.status(500).json({ 18 | errorMessage: 'Please try again later', 19 | }); 20 | } 21 | }; 22 | 23 | exports.searchByQueryType = async (req, res) => { 24 | const { type, query } = req.body; 25 | 26 | try { 27 | let products; 28 | 29 | switch (type) { 30 | case 'text': 31 | products = await Product.find({ $text: { $search: query } }); 32 | break; 33 | case 'category': 34 | products = await Product.find({ productCategory: query }); 35 | break; 36 | } 37 | 38 | if (!products.length > 0) { 39 | products = await Product.find({}); 40 | } 41 | 42 | res.json({ products }); 43 | } catch (err) { 44 | console.log(err, 'filter Controller.searchByQueryType error'); 45 | res.status(500).json({ 46 | errorMessage: 'Please try again later', 47 | }); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /client/src/redux/actions/cartActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_TO_CART, 3 | DELETE_FROM_CART, 4 | CLEAR_CART, 5 | } from '../constants/cartConstants'; 6 | 7 | export const addToCart = product => async dispatch => { 8 | // if cart already exists in local storage, use it, otherwise set to empty array 9 | const cart = localStorage.getItem('cart') 10 | ? JSON.parse(localStorage.getItem('cart')) 11 | : []; 12 | 13 | // check if duplicates 14 | const duplicates = cart.filter(cartItem => cartItem._id === product._id); 15 | 16 | // if no duplicates, proceed 17 | if (duplicates.length === 0) { 18 | // prep product data 19 | const productToAdd = { 20 | ...product, 21 | count: 1, 22 | }; 23 | 24 | // add product data to cart 25 | cart.push(productToAdd); 26 | 27 | // add cart to local storage 28 | localStorage.setItem('cart', JSON.stringify(cart)); 29 | 30 | // add cart to redux 31 | dispatch({ 32 | type: ADD_TO_CART, 33 | payload: cart, 34 | }); 35 | } 36 | }; 37 | 38 | export const deleteFromCart = product => async dispatch => { 39 | const cart = localStorage.getItem('cart') 40 | ? JSON.parse(localStorage.getItem('cart')) 41 | : []; 42 | 43 | const updatedCart = cart.filter(cartItem => cartItem._id !== product._id); 44 | 45 | localStorage.setItem('cart', JSON.stringify(updatedCart)); 46 | 47 | dispatch({ 48 | type: DELETE_FROM_CART, 49 | payload: updatedCart, 50 | }); 51 | }; 52 | 53 | export const clearCart = () => async dispatch => { 54 | dispatch({ 55 | type: CLEAR_CART, 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /client/src/components/CheckoutForm.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | // FOR PRODUCTION 3 | // import { useNavigate } from 'react-router-dom'; 4 | import { 5 | useStripe, 6 | useElements, 7 | PaymentElement, 8 | } from '@stripe/react-stripe-js'; 9 | 10 | const CheckoutForm = () => { 11 | // FOR PRODUCTION 12 | // const navigate = useNavigate(); 13 | 14 | const stripe = useStripe(); 15 | const elements = useElements(); 16 | 17 | const [loading, setLoading] = useState(false); 18 | 19 | const handleSubmit = async event => { 20 | event.preventDefault(); 21 | 22 | // FOR DEVELOPMENT 23 | setLoading(true); 24 | setTimeout(() => { 25 | setLoading(false); 26 | }, 4000); 27 | 28 | /* FOR PRODUCTION 29 | if (!stripe || !elements) { 30 | return; 31 | } 32 | 33 | setLoading(true); 34 | const result = await stripe.confirmPayment({ 35 | elements, 36 | redirect: 'if_required', 37 | }); 38 | setLoading(false); 39 | 40 | if (result.error) { 41 | console.log(result.error.message); 42 | } else { 43 | navigate('/', { 44 | state: { 45 | result, 46 | }, 47 | }); 48 | } 49 | */ 50 | }; 51 | 52 | return ( 53 |
54 | 55 | 65 | 66 | ); 67 | }; 68 | 69 | export default CheckoutForm; 70 | -------------------------------------------------------------------------------- /client/src/redux/actions/filterActions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { START_LOADING, STOP_LOADING } from '../constants/loadingConstants'; 3 | import { SHOW_ERROR_MESSAGE } from '../constants/messageConstants'; 4 | import { GET_NEW_ARRIVALS } from '../constants/filterConstants'; 5 | import { GET_PRODUCTS } from '../constants/productConstants'; 6 | 7 | export const getNewArrivals = 8 | (sortBy = 'desc', limit = 3) => 9 | async dispatch => { 10 | try { 11 | dispatch({ type: START_LOADING }); 12 | const response = await axios.get( 13 | `${process.env.REACT_APP_SERVER_URL}/api/filter?sortBy=${sortBy}&limit=${limit}` 14 | ); 15 | dispatch({ type: STOP_LOADING }); 16 | dispatch({ 17 | type: GET_NEW_ARRIVALS, 18 | payload: response.data.newArrivals, 19 | }); 20 | } catch (err) { 21 | console.log('getNewProducts api error: ', err); 22 | dispatch({ type: STOP_LOADING }); 23 | dispatch({ 24 | type: SHOW_ERROR_MESSAGE, 25 | payload: err.response.data.errorMessage, 26 | }); 27 | } 28 | }; 29 | 30 | export const getProductsByFilter = arg => async dispatch => { 31 | try { 32 | const response = await axios.post( 33 | `${process.env.REACT_APP_SERVER_URL}/api/filter/search`, 34 | arg 35 | ); 36 | 37 | dispatch({ 38 | type: GET_PRODUCTS, 39 | payload: response.data.products, 40 | }); 41 | } catch (err) { 42 | console.log('getProductsByFilter api error: ', err); 43 | dispatch({ type: STOP_LOADING }); 44 | dispatch({ 45 | type: SHOW_ERROR_MESSAGE, 46 | payload: err.response.data.errorMessage, 47 | }); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 17 | Ecommerce - MERN STACK 18 | 19 | 20 |
21 | 22 | 27 | 32 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /client/src/redux/actions/categoryActions.js: -------------------------------------------------------------------------------- 1 | import { START_LOADING, STOP_LOADING } from '../constants/loadingConstants'; 2 | import { 3 | SHOW_ERROR_MESSAGE, 4 | SHOW_SUCCESS_MESSAGE, 5 | } from '../constants/messageConstants'; 6 | import { 7 | GET_CATEGORIES, 8 | CREATE_CATEGORY, 9 | } from '../constants/categoryConstants'; 10 | import axios from 'axios'; 11 | 12 | export const getCategories = () => async dispatch => { 13 | try { 14 | dispatch({ type: START_LOADING }); 15 | const response = await axios.get( 16 | `${process.env.REACT_APP_SERVER_URL}/api/category` 17 | ); 18 | dispatch({ type: STOP_LOADING }); 19 | dispatch({ type: GET_CATEGORIES, payload: response.data.categories }); 20 | } catch (err) { 21 | console.log('getCategories api error: ', err); 22 | dispatch({ type: STOP_LOADING }); 23 | dispatch({ 24 | type: SHOW_ERROR_MESSAGE, 25 | payload: err.response.data.errorMessage, 26 | }); 27 | } 28 | }; 29 | 30 | export const createCategory = formData => async dispatch => { 31 | try { 32 | const config = { 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | }, 36 | }; 37 | dispatch({ type: START_LOADING }); 38 | const response = await axios.post( 39 | `${process.env.REACT_APP_SERVER_URL}/api/category`, 40 | formData, 41 | config 42 | ); 43 | dispatch({ type: STOP_LOADING }); 44 | dispatch({ 45 | type: SHOW_SUCCESS_MESSAGE, 46 | payload: response.data.successMessage, 47 | }); 48 | dispatch({ type: CREATE_CATEGORY, payload: response.data.category }); 49 | } catch (err) { 50 | console.log('createCategory api error: ', err); 51 | dispatch({ type: STOP_LOADING }); 52 | dispatch({ 53 | type: SHOW_ERROR_MESSAGE, 54 | payload: err.response.data.errorMessage, 55 | }); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /client/src/components/PlaceOrder.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import ProgressBar from './ProgressBar'; 3 | import axios from 'axios'; 4 | import { Elements } from '@stripe/react-stripe-js'; 5 | import { loadStripe } from '@stripe/stripe-js'; 6 | import CheckoutForm from './CheckoutForm'; 7 | 8 | const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY); 9 | 10 | const PlaceOrder = () => { 11 | const [clientSecret, setClientSecret] = useState(''); 12 | 13 | const calculateCartTotal = () => { 14 | const cart = JSON.parse(localStorage.getItem('cart')); 15 | 16 | let cartTotal = cart.reduce((accumulator, currentValue) => { 17 | return accumulator + currentValue.count * currentValue.productPrice; 18 | }, 0); 19 | 20 | cartTotal = cartTotal.toFixed(2) * 100; 21 | 22 | return cartTotal; 23 | }; 24 | 25 | const getPaymentIntent = async () => { 26 | const cartTotal = calculateCartTotal(); 27 | 28 | const response = await axios.post( 29 | `${process.env.REACT_APP_SERVER_URL}/api/payment/payment-intent`, 30 | { 31 | total: cartTotal, 32 | } 33 | ); 34 | 35 | setClientSecret(response.data.clientSecret); 36 | }; 37 | 38 | useEffect(() => { 39 | getPaymentIntent(); 40 | // eslint-disable-next-line 41 | }, []); 42 | 43 | const options = { 44 | clientSecret, 45 | appearance: { 46 | theme: 'stripe', 47 | }, 48 | }; 49 | 50 | return ( 51 |
52 |
53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 |
61 |
Place Order
62 | {clientSecret && ( 63 | 64 | 65 | 66 | )} 67 |
68 |
69 |
70 |
71 | ); 72 | }; 73 | 74 | export default PlaceOrder; 75 | -------------------------------------------------------------------------------- /client/src/components/ProgressBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const ProgressBar = ({ step1, step2, step3 }) => { 5 | return ( 6 | <> 7 | 79 | 80 | ); 81 | }; 82 | 83 | export default ProgressBar; 84 | -------------------------------------------------------------------------------- /client/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 3 | import './App.css'; 4 | import Header from './Header'; 5 | import Home from './Home'; 6 | import Shop from './Shop'; 7 | import Cart from './Cart'; 8 | import Shipping from './Shipping'; 9 | import PlaceOrder from './PlaceOrder'; 10 | import Payment from './Payment'; 11 | import Product from './Product'; 12 | import Signup from './Signup'; 13 | import Signin from './Signin'; 14 | import UserDashboard from './UserDashboard'; 15 | import AdminDashboard from './AdminDashboard'; 16 | import AdminEditProduct from './AdminEditProduct'; 17 | import AdminRoute from './AdminRoute'; 18 | import UserRoute from './UserRoute'; 19 | import NotFound from './NotFound'; 20 | 21 | const App = () => { 22 | return ( 23 | 24 |
25 |
26 | 27 | } /> 28 | } /> 29 | } /> 30 | } 34 | /> 35 | } /> 36 | } /> 37 | } /> 38 | } /> 39 | } /> 40 | 41 | {/* protected user routes */} 42 | }> 43 | } 47 | /> 48 | 49 | 50 | {/* protected admin routes */} 51 | }> 52 | } 56 | /> 57 | } 61 | /> 62 | 63 | 64 | } /> 65 | 66 |
67 | 68 | ); 69 | }; 70 | 71 | export default App; 72 | -------------------------------------------------------------------------------- /client/src/components/Product.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useParams, useNavigate } from 'react-router-dom'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { getProduct } from '../redux/actions/productActions'; 5 | import { addToCart } from '../redux/actions/cartActions'; 6 | 7 | const Product = () => { 8 | const navigate = useNavigate(); 9 | const { productId } = useParams(); 10 | 11 | const dispatch = useDispatch(); 12 | 13 | useEffect(() => { 14 | dispatch(getProduct(productId)); 15 | }, [dispatch, productId]); 16 | 17 | const { product } = useSelector(state => state.products); 18 | 19 | const handleAddToCart = () => { 20 | dispatch(addToCart(product)); 21 | }; 22 | 23 | const handleGoBackBtn = () => { 24 | navigate(-1); 25 | }; 26 | 27 | return ( 28 |
29 | 36 | {product && ( 37 |
38 |
39 | product 44 |
45 |
46 |

{product.productName}

47 |

48 | Price:{' '} 49 | {product.productPrice.toLocaleString('en-US', { 50 | style: 'currency', 51 | currency: 'USD', 52 | })} 53 |

54 |

55 | Status:{' '} 56 | {product.productQty <= 0 57 | ? 'Out of Stock' 58 | : 'In Stock'} 59 |

60 |

61 | Description: {product.productDesc} 62 |

63 | 70 |
71 |
72 | )} 73 |
74 | ); 75 | }; 76 | 77 | export default Product; 78 | -------------------------------------------------------------------------------- /controllers/auth.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/User'); 2 | const bcrypt = require('bcryptjs'); 3 | const jwt = require('jsonwebtoken'); 4 | const { jwtSecret, jwtExpire } = require('../config/keys'); 5 | 6 | exports.signupController = async (req, res) => { 7 | const { username, email, password } = req.body; 8 | 9 | try { 10 | const user = await User.findOne({ email }); 11 | if (user) { 12 | return res.status(400).json({ 13 | errorMessage: 'Email already exists', 14 | }); 15 | } 16 | 17 | const newUser = new User(); 18 | newUser.username = username; 19 | newUser.email = email; 20 | 21 | const salt = await bcrypt.genSalt(10); 22 | newUser.password = await bcrypt.hash(password, salt); 23 | 24 | await newUser.save(); 25 | 26 | res.json({ 27 | successMessage: 'Registration success. Please signin.', 28 | }); 29 | } catch (err) { 30 | console.log('signupController error: ', err); 31 | res.status(500).json({ 32 | errorMessage: 'Server error', 33 | }); 34 | } 35 | }; 36 | 37 | exports.signinController = async (req, res) => { 38 | const { email, password } = req.body; 39 | 40 | try { 41 | const user = await User.findOne({ email }); 42 | if (!user) { 43 | return res.status(400).json({ 44 | errorMessage: 'Invalid credentials', 45 | }); 46 | } 47 | 48 | const isMatch = await bcrypt.compare(password, user.password); 49 | if (!isMatch) { 50 | return res.status(400).json({ 51 | errorMessage: 'Invalid credentials', 52 | }); 53 | } 54 | 55 | const payload = { 56 | user: { 57 | _id: user._id, 58 | }, 59 | }; 60 | 61 | jwt.sign(payload, jwtSecret, { expiresIn: jwtExpire }, (err, token) => { 62 | if (err) console.log('jwt error: ', err); 63 | const { _id, username, email, role } = user; 64 | 65 | res.json({ 66 | token, 67 | user: { _id, username, email, role }, 68 | }); 69 | }); 70 | } catch (err) { 71 | console.log('signinController error: ', err); 72 | res.status(500).json({ 73 | errorMessage: 'Server error', 74 | }); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /client/src/components/Payment.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import ProgressBar from './ProgressBar'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { savePaymentMethod } from '../redux/actions/orderActions'; 5 | import { useNavigate } from 'react-router-dom'; 6 | 7 | const Payment = () => { 8 | const navigate = useNavigate(); 9 | 10 | const dispatch = useDispatch(); 11 | 12 | const { paymentMethod } = useSelector(state => state.order); 13 | const [paymentType, setPaymentType] = useState('stripe'); 14 | 15 | useEffect(() => { 16 | if (paymentMethod) { 17 | setPaymentType(paymentMethod); 18 | } 19 | }, [setPaymentType, paymentMethod]); 20 | 21 | const handleChange = e => { 22 | setPaymentType(e.target.value); 23 | dispatch(savePaymentMethod(e.target.value)); 24 | }; 25 | 26 | const handleSubmit = e => { 27 | e.preventDefault(); 28 | dispatch(savePaymentMethod(paymentType)); 29 | 30 | navigate('/placeorder'); 31 | }; 32 | 33 | return ( 34 |
35 |
36 |
37 | 38 |
39 |
40 | 41 |
42 |
43 |
44 |
Payment
45 | 46 |
47 |
48 | 56 | 59 |
60 |
61 | 69 | 72 |
73 | 76 |
77 |
78 |
79 |
80 |
81 | ); 82 | }; 83 | 84 | export default Payment; 85 | -------------------------------------------------------------------------------- /client/src/components/Card.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | // redux 4 | import { useDispatch } from 'react-redux'; 5 | import { deleteProduct } from '../redux/actions/productActions'; 6 | import { addToCart } from '../redux/actions/cartActions'; 7 | 8 | const Card = ({ product, adminPage = false, homePage = false }) => { 9 | const dispatch = useDispatch(); 10 | 11 | const handleAddToCart = () => { 12 | dispatch(addToCart(product)); 13 | }; 14 | 15 | return ( 16 |
17 |
18 | 19 | product 24 | 25 | 26 |
27 |
{product.productName}
28 |
29 |
30 | 31 | {product.productPrice.toLocaleString('en-US', { 32 | style: 'currency', 33 | currency: 'USD', 34 | })} 35 | 36 |
37 |

38 | {product.productQty <= 0 ? 'Out of Stock' : 'In Stock'} 39 |

40 |

41 | {product.productDesc.length > 60 42 | ? product.productDesc.substring(0, 60) + '...' 43 | : product.productDesc.substring(0, 60)} 44 |

45 | {adminPage && ( 46 | <> 47 | 52 | 53 | Edit 54 | 55 | 65 | 66 | )} 67 | 68 | {homePage && ( 69 | <> 70 | 75 | View Product 76 | 77 | 85 | 86 | )} 87 |
88 |
89 |
90 | ); 91 | }; 92 | 93 | export default Card; 94 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /client/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { showLoading } from '../helpers/loading'; 3 | import Card from './Card'; 4 | import { getNewArrivals } from '../redux/actions/filterActions'; 5 | import { getProductsByCount } from '../redux/actions/productActions'; 6 | import { useDispatch, useSelector } from 'react-redux'; 7 | import { useLocation } from 'react-router-dom'; 8 | import { clearCart } from '../redux/actions/cartActions'; 9 | import { clearOrder } from '../redux/actions/orderActions'; 10 | import { clearCartLocalStorage } from '../helpers/localStorage'; 11 | 12 | const Home = () => { 13 | const location = useLocation(); 14 | const dispatch = useDispatch(); 15 | 16 | useEffect(() => { 17 | if ( 18 | location.state && 19 | location.state.result.paymentIntent.status === 'succeeded' 20 | ) { 21 | dispatch(clearCart()); 22 | dispatch(clearOrder()); 23 | clearCartLocalStorage(() => { 24 | setSuccessMsg('Your payment was successful!'); 25 | setTimeout(() => { 26 | setSuccessMsg(''); 27 | }, 5000); 28 | }); 29 | } 30 | // eslint-disable-next-line 31 | }, []); 32 | 33 | useEffect(() => { 34 | dispatch(getNewArrivals()); 35 | }, [dispatch]); 36 | 37 | useEffect(() => { 38 | dispatch(getProductsByCount()); 39 | }, [dispatch]); 40 | 41 | const [successMsg, setSuccessMsg] = useState(''); 42 | const { newArrivals } = useSelector(state => state.filters); 43 | const { products } = useSelector(state => state.products); 44 | const { loading } = useSelector(state => state.loading); 45 | 46 | return ( 47 |
48 |
56 | {loading ? ( 57 |
{showLoading()}
58 | ) : ( 59 | <> 60 |
61 |
62 | {location.state && 63 | location.state.result.paymentIntent.status === 64 | 'succeeded' && 65 | successMsg && ( 66 |
70 | {successMsg} 71 |
72 | )} 73 |

New Arrivals

74 |
75 | {newArrivals && 76 | newArrivals.map(newArrival => ( 77 | 82 | ))} 83 |
84 | 85 |
86 |

Menu

87 |
88 | {products && 89 | products.map(product => ( 90 | 95 | ))} 96 |
97 |
98 | 99 | )} 100 |
101 | ); 102 | }; 103 | 104 | export default Home; 105 | -------------------------------------------------------------------------------- /controllers/product.js: -------------------------------------------------------------------------------- 1 | const Product = require('../models/Product'); 2 | const fs = require('fs'); 3 | 4 | exports.create = async (req, res) => { 5 | const { filename } = req.file; 6 | const { 7 | productName, 8 | productDesc, 9 | productPrice, 10 | productCategory, 11 | productQty, 12 | } = req.body; 13 | 14 | try { 15 | let product = new Product(); 16 | product.fileName = filename; 17 | product.productName = productName; 18 | product.productDesc = productDesc; 19 | product.productPrice = productPrice; 20 | product.productCategory = productCategory; 21 | product.productQty = productQty; 22 | 23 | await product.save(); 24 | 25 | res.json({ 26 | successMessage: `${productName} was created`, 27 | product, 28 | }); 29 | } catch (err) { 30 | console.log(err, 'productController.create error'); 31 | res.status(500).json({ 32 | errorMessage: 'Please try again later', 33 | }); 34 | } 35 | }; 36 | 37 | exports.readAll = async (req, res) => { 38 | try { 39 | const products = await Product.find({}).populate( 40 | 'productCategory', 41 | 'category' 42 | ); 43 | 44 | res.json({ products }); 45 | } catch (err) { 46 | console.log(err, 'productController.readAll error'); 47 | res.status(500).json({ 48 | errorMessage: 'Please try again later', 49 | }); 50 | } 51 | }; 52 | 53 | exports.readByCount = async (req, res) => { 54 | try { 55 | const products = await Product.find({}) 56 | .populate('productCategory', 'category') 57 | .limit(6); 58 | 59 | res.json({ products }); 60 | } catch (err) { 61 | console.log(err, 'productController.readAll error'); 62 | res.status(500).json({ 63 | errorMessage: 'Please try again later', 64 | }); 65 | } 66 | }; 67 | 68 | exports.read = async (req, res) => { 69 | try { 70 | const productId = req.params.productId; 71 | const product = await Product.findById(productId); 72 | 73 | res.json(product); 74 | } catch (err) { 75 | console.log(err, 'productController.read error'); 76 | res.status(500).json({ 77 | errorMessage: 'Please try again later', 78 | }); 79 | } 80 | }; 81 | 82 | exports.update = async (req, res) => { 83 | const productId = req.params.productId; 84 | 85 | if (req.file !== undefined) { 86 | req.body.fileName = req.file.filename; 87 | } 88 | 89 | const oldProduct = await Product.findByIdAndUpdate(productId, req.body); 90 | 91 | if (req.file !== undefined && req.file.filename !== oldProduct.fileName) { 92 | fs.unlink(`uploads/${oldProduct.fileName}`, err => { 93 | if (err) throw err; 94 | console.log('Image deleted from the filesystem'); 95 | }); 96 | } 97 | 98 | res.json({ 99 | successMessage: 'Product successfully updated', 100 | }); 101 | }; 102 | 103 | exports.delete = async (req, res) => { 104 | try { 105 | const productId = req.params.productId; 106 | const deletedProduct = await Product.findByIdAndDelete(productId); 107 | 108 | fs.unlink(`uploads/${deletedProduct.fileName}`, err => { 109 | if (err) throw err; 110 | console.log( 111 | 'Image successfully deleted from filesystem: ', 112 | deletedProduct.fileName 113 | ); 114 | }); 115 | 116 | res.json(deletedProduct); 117 | } catch (err) { 118 | console.log(err, 'productController.delete error'); 119 | res.status(500).json({ 120 | errorMessage: 'Please try again later', 121 | }); 122 | } 123 | }; 124 | -------------------------------------------------------------------------------- /client/src/components/AdminCategoryModal.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from 'react'; 2 | import isEmpty from 'validator/lib/isEmpty'; 3 | import { showErrorMsg, showSuccessMsg } from '../helpers/message'; 4 | import { showLoading } from '../helpers/loading'; 5 | // redux 6 | import { useSelector, useDispatch } from 'react-redux'; 7 | import { clearMessages } from '../redux/actions/messageActions'; 8 | import { createCategory } from '../redux/actions/categoryActions'; 9 | 10 | const AdminCategoryModal = () => { 11 | /**************************** 12 | * REDUX GLOBAL STATE PROPERTIES 13 | ***************************/ 14 | const { successMsg, errorMsg } = useSelector(state => state.messages); 15 | const { loading } = useSelector(state => state.loading); 16 | 17 | const dispatch = useDispatch(); 18 | /**************************** 19 | * COMPONENT STATE PROPERTIES 20 | ***************************/ 21 | const [category, setCategory] = useState(''); 22 | const [clientSideErrorMsg, setClientSideErrorMsg] = useState(''); 23 | 24 | /**************************** 25 | * EVENT HANDLERS 26 | ***************************/ 27 | const handleMessages = evt => { 28 | dispatch(clearMessages()); 29 | }; 30 | 31 | const handleCategoryChange = evt => { 32 | dispatch(clearMessages()); 33 | setCategory(evt.target.value); 34 | }; 35 | 36 | const handleCategorySubmit = evt => { 37 | evt.preventDefault(); 38 | 39 | if (isEmpty(category)) { 40 | setClientSideErrorMsg('Please enter a category'); 41 | } else { 42 | const data = { category }; 43 | dispatch(createCategory(data)); 44 | setCategory(''); 45 | } 46 | }; 47 | 48 | /**************************** 49 | * RENDERER 50 | ***************************/ 51 | return ( 52 |
53 |
54 |
55 |
56 |
57 |
Add Category
58 | 63 |
64 |
65 | {clientSideErrorMsg && 66 | showErrorMsg(clientSideErrorMsg)} 67 | {errorMsg && showErrorMsg(errorMsg)} 68 | {successMsg && showSuccessMsg(successMsg)} 69 | 70 | {loading ? ( 71 |
72 | {showLoading()} 73 |
74 | ) : ( 75 | 76 | 79 | 86 | 87 | )} 88 |
89 |
90 | 96 | 99 |
100 |
101 |
102 |
103 |
104 | ); 105 | }; 106 | 107 | export default AdminCategoryModal; 108 | -------------------------------------------------------------------------------- /client/src/redux/actions/productActions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { START_LOADING, STOP_LOADING } from '../constants/loadingConstants'; 3 | import { 4 | SHOW_ERROR_MESSAGE, 5 | SHOW_SUCCESS_MESSAGE, 6 | } from '../constants/messageConstants'; 7 | import { 8 | CREATE_PRODUCT, 9 | GET_PRODUCTS, 10 | GET_PRODUCT, 11 | DELETE_PRODUCT, 12 | } from '../constants/productConstants'; 13 | 14 | export const createProduct = formData => async dispatch => { 15 | try { 16 | dispatch({ type: START_LOADING }); 17 | const response = await axios.post( 18 | `${process.env.REACT_APP_SERVER_URL}/api/product`, 19 | formData 20 | ); 21 | dispatch({ type: STOP_LOADING }); 22 | dispatch({ 23 | type: SHOW_SUCCESS_MESSAGE, 24 | payload: response.data.successMessage, 25 | }); 26 | dispatch({ 27 | type: CREATE_PRODUCT, 28 | payload: response.data.product, 29 | }); 30 | } catch (err) { 31 | console.log('createProduct api error: ', err); 32 | dispatch({ type: STOP_LOADING }); 33 | dispatch({ 34 | type: SHOW_ERROR_MESSAGE, 35 | payload: err.response.data.errorMessage, 36 | }); 37 | } 38 | }; 39 | 40 | export const getProducts = () => async dispatch => { 41 | try { 42 | dispatch({ type: START_LOADING }); 43 | const response = await axios.get( 44 | `${process.env.REACT_APP_SERVER_URL}/api/product` 45 | ); 46 | dispatch({ type: STOP_LOADING }); 47 | dispatch({ 48 | type: GET_PRODUCTS, 49 | payload: response.data.products, 50 | }); 51 | } catch (err) { 52 | console.log('getProducts api error: ', err); 53 | dispatch({ type: STOP_LOADING }); 54 | dispatch({ 55 | type: SHOW_ERROR_MESSAGE, 56 | payload: err.response.data.errorMessage, 57 | }); 58 | } 59 | }; 60 | 61 | export const getProductsByCount = () => async dispatch => { 62 | try { 63 | dispatch({ type: START_LOADING }); 64 | const response = await axios.get( 65 | `${process.env.REACT_APP_SERVER_URL}/api/product/count` 66 | ); 67 | dispatch({ type: STOP_LOADING }); 68 | dispatch({ 69 | type: GET_PRODUCTS, 70 | payload: response.data.products, 71 | }); 72 | } catch (err) { 73 | console.log('getProducts api error: ', err); 74 | dispatch({ type: STOP_LOADING }); 75 | dispatch({ 76 | type: SHOW_ERROR_MESSAGE, 77 | payload: err.response.data.errorMessage, 78 | }); 79 | } 80 | }; 81 | 82 | export const getProduct = productId => async dispatch => { 83 | try { 84 | dispatch({ type: START_LOADING }); 85 | const response = await axios.get( 86 | `${process.env.REACT_APP_SERVER_URL}/api/product/${productId}` 87 | ); 88 | dispatch({ type: STOP_LOADING }); 89 | dispatch({ 90 | type: GET_PRODUCT, 91 | payload: response.data, 92 | }); 93 | } catch (err) { 94 | console.log('getProducts api error: ', err); 95 | dispatch({ type: STOP_LOADING }); 96 | dispatch({ 97 | type: SHOW_ERROR_MESSAGE, 98 | payload: err.response.data.errorMessage, 99 | }); 100 | } 101 | }; 102 | 103 | export const deleteProduct = productId => async dispatch => { 104 | try { 105 | dispatch({ type: START_LOADING }); 106 | const response = await axios.delete( 107 | `${process.env.REACT_APP_SERVER_URL}/api/product/${productId}` 108 | ); 109 | dispatch({ type: STOP_LOADING }); 110 | dispatch({ 111 | type: DELETE_PRODUCT, 112 | payload: response.data, 113 | }); 114 | } catch (err) { 115 | console.log('deleteProduct api error: ', err); 116 | dispatch({ type: STOP_LOADING }); 117 | dispatch({ 118 | type: SHOW_ERROR_MESSAGE, 119 | payload: err.response.data.errorMessage, 120 | }); 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Restaurant Tutorial 2 | 3 | [Restaurant Tutorial](https://restaurant-tutorial.herokuapp.com/) is a full-stack e-commerce project that showcases an online shopping platform. This project was created as part of a YouTube tutorial series aimed at teaching others how to build an e-commerce site from scratch. 4 | 5 | The focus of the tutorial series was on implementing essential e-commerce functionality rather than designing a visually polished UI, to keep the series concise. The application includes features such as search filters, shopping cart functionality, secure payment processing, and admin dashboard tools for managing products. 6 | 7 | You can watch the full tutorial series on [YouTube](https://www.youtube.com/@WebStoreMaker). 8 | 9 | ## Table of Contents 10 | - [About the Project](#about-the-project) 11 | - [Technologies Used](#technologies-used) 12 | - [Features](#features) 13 | - [What I Learned](#what-i-learned) 14 | - [Getting Started](#getting-started) 15 | 16 | --- 17 | 18 | ## About the Project 19 | 20 | This project was created as a result of my passion for teaching others what I've learned. I have learned so much from instructors on platforms like YouTube, so it feels rewarding to return the favor to the community in the best way possible. The tutorial series on [YouTube](https://www.youtube.com/@WebStoreMaker) guides viewers step-by-step through building an application with features like: 21 | 22 | - Search filters. 23 | - Shopping cart functionality. 24 | - Payment gateway integration using **Stripe**. 25 | - Admin dashboard functionality for managing products and inventory. 26 | 27 | The focus of the series was on **functionality** rather than UI design, as I wanted to ensure the tutorial series didn't overextend in time. The tutorials emphasize practical skills and real-world problem-solving to help aspiring developers learn and grow. 28 | 29 | --- 30 | 31 | ## Technologies Used 32 | 33 | This project was built using the following technologies and tools: 34 | 35 | - **Frontend**: React.js, HTML, CSS, JavaScript 36 | - **Backend**: Node.js, Express.js 37 | - **Database**: MongoDB 38 | - **APIs**: REST APIs 39 | - **Payment Gateway**: Stripe 40 | - **Other Tools**: Heroku for deployment 41 | 42 | --- 43 | 44 | ## Features 45 | 46 | - **Product Listings**: Display all food items with information. 47 | - **Search Filters**: Easily search and filter products. 48 | - **Shopping Cart**: Add, update, or remove items dynamically. 49 | - **Secure Payment Gateway**: Process payments using **Stripe**. 50 | - **Admin Dashboard**: Manage products. 51 | - **Responsive Design**: Ensure a seamless shopping experience on all devices. 52 | 53 | --- 54 | 55 | ## What I Learned 56 | 57 | While creating the YouTube tutorial series, I worked on this restaurant online ordering application using **React** on the frontend and **Express**, **Node.js**, and **MongoDB** on the backend. 58 | 59 | During development, I encountered a **proxy error** while writing the code to display food items on the homepage. After researching the issue on Google and StackOverflow, I discovered it was a common error among developers. I created a dedicated YouTube video explaining the steps to resolve the issue, which became one of my most viewed videos in the tutorial series. 60 | 61 | Through this project, I also learned to build an e-commerce site using the **MERN stack (MongoDB, Express, React, Node.js)**, implementing features such as: 62 | - Search filters. 63 | - Shopping cart functionality. 64 | - Payment gateway integration with **Stripe**. 65 | - Admin dashboard functionality for managing products. 66 | - Debugging and improving the development workflow. 67 | 68 | This project deepened my understanding of full-stack development and enhanced my ability to teach complex concepts effectively. 69 | 70 | --- 71 | 72 | ## Getting Started 73 | 74 | To run this project locally, follow these steps: 75 | 76 | 1. Clone the repository: 77 | ```bash 78 | git clone https://github.com/jalvaradoas39/restaurant-tutorial.git 79 | -------------------------------------------------------------------------------- /client/src/components/Shop.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { getProducts } from '../redux/actions/productActions'; 4 | import { getCategories } from '../redux/actions/categoryActions'; 5 | import { getProductsByFilter } from '../redux/actions/filterActions'; 6 | import Card from './Card'; 7 | 8 | const Shop = () => { 9 | const [text, setText] = useState(''); 10 | const [categoryIds, setCategoryIds] = useState([]); 11 | 12 | const dispatch = useDispatch(); 13 | 14 | useEffect(() => { 15 | dispatch(getProducts()); 16 | }, [dispatch]); 17 | 18 | useEffect(() => { 19 | dispatch(getCategories()); 20 | }, [dispatch]); 21 | 22 | const { products } = useSelector(state => state.products); 23 | const { categories } = useSelector(state => state.categories); 24 | 25 | const handleSearch = e => { 26 | resetState(); 27 | 28 | setText(e.target.value); 29 | 30 | dispatch(getProductsByFilter({ type: 'text', query: e.target.value })); 31 | }; 32 | 33 | const handleCategory = e => { 34 | resetState(); 35 | 36 | const currentCategoryChecked = e.target.value; 37 | const allCategoriesChecked = [...categoryIds]; 38 | const indexFound = allCategoriesChecked.indexOf(currentCategoryChecked); 39 | 40 | let updatedCategoryIds; 41 | if (indexFound === -1) { 42 | // add 43 | updatedCategoryIds = [...categoryIds, currentCategoryChecked]; 44 | setCategoryIds(updatedCategoryIds); 45 | } else { 46 | // remove 47 | updatedCategoryIds = [...categoryIds]; 48 | updatedCategoryIds.splice(indexFound, 1); 49 | setCategoryIds(updatedCategoryIds); 50 | } 51 | 52 | dispatch( 53 | getProductsByFilter({ type: 'category', query: updatedCategoryIds }) 54 | ); 55 | }; 56 | 57 | const resetState = () => { 58 | setText(''); 59 | setCategoryIds([]); 60 | }; 61 | 62 | return ( 63 |
64 |
65 |

Shop

66 |
67 |
68 |
69 |
70 | Filters 71 |
72 | 73 | 93 | 94 |
95 | {categories && 96 | categories.map(c => ( 97 |
98 | 107 | 113 |
114 | ))} 115 |
116 |
117 |
118 |
119 | {products && 120 | products.map(p => ( 121 | 122 | ))} 123 |
124 |
125 |
126 |
127 | ); 128 | }; 129 | 130 | export default Shop; 131 | -------------------------------------------------------------------------------- /client/src/data/usaStates.js: -------------------------------------------------------------------------------- 1 | const usaStates = [ 2 | { 3 | name: 'Alabama', 4 | abbreviation: 'AL', 5 | }, 6 | { 7 | name: 'Alaska', 8 | abbreviation: 'AK', 9 | }, 10 | { 11 | name: 'American Samoa', 12 | abbreviation: 'AS', 13 | }, 14 | { 15 | name: 'Arizona', 16 | abbreviation: 'AZ', 17 | }, 18 | { 19 | name: 'Arkansas', 20 | abbreviation: 'AR', 21 | }, 22 | { 23 | name: 'California', 24 | abbreviation: 'CA', 25 | }, 26 | { 27 | name: 'Colorado', 28 | abbreviation: 'CO', 29 | }, 30 | { 31 | name: 'Connecticut', 32 | abbreviation: 'CT', 33 | }, 34 | { 35 | name: 'Delaware', 36 | abbreviation: 'DE', 37 | }, 38 | { 39 | name: 'District Of Columbia', 40 | abbreviation: 'DC', 41 | }, 42 | { 43 | name: 'Federated States Of Micronesia', 44 | abbreviation: 'FM', 45 | }, 46 | { 47 | name: 'Florida', 48 | abbreviation: 'FL', 49 | }, 50 | { 51 | name: 'Georgia', 52 | abbreviation: 'GA', 53 | }, 54 | { 55 | name: 'Guam', 56 | abbreviation: 'GU', 57 | }, 58 | { 59 | name: 'Hawaii', 60 | abbreviation: 'HI', 61 | }, 62 | { 63 | name: 'Idaho', 64 | abbreviation: 'ID', 65 | }, 66 | { 67 | name: 'Illinois', 68 | abbreviation: 'IL', 69 | }, 70 | { 71 | name: 'Indiana', 72 | abbreviation: 'IN', 73 | }, 74 | { 75 | name: 'Iowa', 76 | abbreviation: 'IA', 77 | }, 78 | { 79 | name: 'Kansas', 80 | abbreviation: 'KS', 81 | }, 82 | { 83 | name: 'Kentucky', 84 | abbreviation: 'KY', 85 | }, 86 | { 87 | name: 'Louisiana', 88 | abbreviation: 'LA', 89 | }, 90 | { 91 | name: 'Maine', 92 | abbreviation: 'ME', 93 | }, 94 | { 95 | name: 'Marshall Islands', 96 | abbreviation: 'MH', 97 | }, 98 | { 99 | name: 'Maryland', 100 | abbreviation: 'MD', 101 | }, 102 | { 103 | name: 'Massachusetts', 104 | abbreviation: 'MA', 105 | }, 106 | { 107 | name: 'Michigan', 108 | abbreviation: 'MI', 109 | }, 110 | { 111 | name: 'Minnesota', 112 | abbreviation: 'MN', 113 | }, 114 | { 115 | name: 'Mississippi', 116 | abbreviation: 'MS', 117 | }, 118 | { 119 | name: 'Missouri', 120 | abbreviation: 'MO', 121 | }, 122 | { 123 | name: 'Montana', 124 | abbreviation: 'MT', 125 | }, 126 | { 127 | name: 'Nebraska', 128 | abbreviation: 'NE', 129 | }, 130 | { 131 | name: 'Nevada', 132 | abbreviation: 'NV', 133 | }, 134 | { 135 | name: 'New Hampshire', 136 | abbreviation: 'NH', 137 | }, 138 | { 139 | name: 'New Jersey', 140 | abbreviation: 'NJ', 141 | }, 142 | { 143 | name: 'New Mexico', 144 | abbreviation: 'NM', 145 | }, 146 | { 147 | name: 'New York', 148 | abbreviation: 'NY', 149 | }, 150 | { 151 | name: 'North Carolina', 152 | abbreviation: 'NC', 153 | }, 154 | { 155 | name: 'North Dakota', 156 | abbreviation: 'ND', 157 | }, 158 | { 159 | name: 'Northern Mariana Islands', 160 | abbreviation: 'MP', 161 | }, 162 | { 163 | name: 'Ohio', 164 | abbreviation: 'OH', 165 | }, 166 | { 167 | name: 'Oklahoma', 168 | abbreviation: 'OK', 169 | }, 170 | { 171 | name: 'Oregon', 172 | abbreviation: 'OR', 173 | }, 174 | { 175 | name: 'Palau', 176 | abbreviation: 'PW', 177 | }, 178 | { 179 | name: 'Pennsylvania', 180 | abbreviation: 'PA', 181 | }, 182 | { 183 | name: 'Puerto Rico', 184 | abbreviation: 'PR', 185 | }, 186 | { 187 | name: 'Rhode Island', 188 | abbreviation: 'RI', 189 | }, 190 | { 191 | name: 'South Carolina', 192 | abbreviation: 'SC', 193 | }, 194 | { 195 | name: 'South Dakota', 196 | abbreviation: 'SD', 197 | }, 198 | { 199 | name: 'Tennessee', 200 | abbreviation: 'TN', 201 | }, 202 | { 203 | name: 'Texas', 204 | abbreviation: 'TX', 205 | }, 206 | { 207 | name: 'Utah', 208 | abbreviation: 'UT', 209 | }, 210 | { 211 | name: 'Vermont', 212 | abbreviation: 'VT', 213 | }, 214 | { 215 | name: 'Virgin Islands', 216 | abbreviation: 'VI', 217 | }, 218 | { 219 | name: 'Virginia', 220 | abbreviation: 'VA', 221 | }, 222 | { 223 | name: 'Washington', 224 | abbreviation: 'WA', 225 | }, 226 | { 227 | name: 'West Virginia', 228 | abbreviation: 'WV', 229 | }, 230 | { 231 | name: 'Wisconsin', 232 | abbreviation: 'WI', 233 | }, 234 | { 235 | name: 'Wyoming', 236 | abbreviation: 'WY', 237 | }, 238 | ]; 239 | 240 | export default usaStates; 241 | -------------------------------------------------------------------------------- /client/src/components/Shipping.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import ProgressBar from './ProgressBar'; 3 | import usaStates from '../data/usaStates'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { useNavigate } from 'react-router-dom'; 6 | import { saveShippingAddress } from '../redux/actions/orderActions'; 7 | 8 | const Shipping = () => { 9 | const navigate = useNavigate(); 10 | const dispatch = useDispatch(); 11 | const { shippingAddress } = useSelector(state => state.order); 12 | 13 | const [address, setAddress] = useState(''); 14 | const [address2, setAddress2] = useState(''); 15 | const [city, setCity] = useState(''); 16 | const [state, setState] = useState(''); 17 | const [zip, setZip] = useState(''); 18 | 19 | useEffect(() => { 20 | shippingAddress.address 21 | ? setAddress(shippingAddress.address) 22 | : setAddress(''); 23 | shippingAddress.address2 24 | ? setAddress2(shippingAddress.address2) 25 | : setAddress2(''); 26 | shippingAddress.city ? setCity(shippingAddress.city) : setCity(''); 27 | shippingAddress.state ? setState(shippingAddress.state) : setState(''); 28 | shippingAddress.zip ? setZip(shippingAddress.zip) : setZip(''); 29 | }, [shippingAddress]); 30 | 31 | const handleSubmit = evt => { 32 | evt.preventDefault(); 33 | 34 | const shippingData = { 35 | address, 36 | address2, 37 | city, 38 | state, 39 | zip, 40 | }; 41 | 42 | dispatch(saveShippingAddress(shippingData)); 43 | navigate('/payment'); 44 | }; 45 | 46 | return ( 47 |
48 |
49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 |
57 |
58 | Shipping Details 59 |
60 | 61 |
62 |
63 | 64 | 69 | setAddress(evt.target.value) 70 | } 71 | /> 72 |
73 | 74 |
75 | 76 | 82 | setAddress2(evt.target.value) 83 | } 84 | /> 85 |
86 | 87 |
88 |
89 | 90 | 95 | setCity(evt.target.value) 96 | } 97 | /> 98 |
99 |
100 | 101 | 118 |
119 |
120 | 121 | 126 | setZip(evt.target.value) 127 | } 128 | /> 129 |
130 |
131 | 132 | 135 |
136 |
137 |
138 |
139 |
140 | ); 141 | }; 142 | 143 | export default Shipping; 144 | -------------------------------------------------------------------------------- /client/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { isAuthenticated, logout } from '../helpers/auth'; 4 | import { useSelector } from 'react-redux'; 5 | 6 | const Header = () => { 7 | let navigate = useNavigate(); 8 | const { cart } = useSelector(state => state.cart); 9 | 10 | const handleLogout = evt => { 11 | logout(() => { 12 | navigate('/signin'); 13 | }); 14 | }; 15 | 16 | // views 17 | const showNavigation = () => ( 18 | 149 | ); 150 | 151 | // render 152 | return ; 153 | }; 154 | 155 | export default Header; 156 | -------------------------------------------------------------------------------- /client/src/components/Signin.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link, useNavigate, useLocation } from 'react-router-dom'; 3 | import { showErrorMsg } from '../helpers/message'; 4 | import { showLoading } from '../helpers/loading'; 5 | import { setAuthentication, isAuthenticated } from '../helpers/auth'; 6 | import isEmpty from 'validator/lib/isEmpty'; 7 | import isEmail from 'validator/lib/isEmail'; 8 | import { signin } from '../api/auth'; 9 | 10 | const Signin = () => { 11 | let navigate = useNavigate(); 12 | let location = useLocation(); 13 | 14 | useEffect(() => { 15 | if (isAuthenticated() && isAuthenticated().role === 1) { 16 | navigate('/admin/dashboard'); 17 | } else if (isAuthenticated() && isAuthenticated().role === 0) { 18 | navigate('/'); 19 | } 20 | }, [navigate]); 21 | 22 | const [formData, setFormData] = useState({ 23 | email: '', 24 | password: '', 25 | errorMsg: false, 26 | loading: false, 27 | }); 28 | 29 | const { email, password, errorMsg, loading } = formData; 30 | 31 | /**************************** 32 | * EVENT HANDLERS 33 | ***************************/ 34 | const handleChange = evt => { 35 | setFormData({ 36 | ...formData, 37 | [evt.target.name]: evt.target.value, 38 | errorMsg: '', 39 | }); 40 | }; 41 | 42 | const handleSubmit = evt => { 43 | evt.preventDefault(); 44 | 45 | // client-side validation 46 | if (isEmpty(email) || isEmpty(password)) { 47 | setFormData({ 48 | ...formData, 49 | errorMsg: 'All fields are required', 50 | }); 51 | } else if (!isEmail(email)) { 52 | setFormData({ 53 | ...formData, 54 | errorMsg: 'Invalid email', 55 | }); 56 | } else { 57 | const { email, password } = formData; 58 | const data = { email, password }; 59 | 60 | setFormData({ ...formData, loading: true }); 61 | 62 | signin(data) 63 | .then(response => { 64 | setAuthentication(response.data.token, response.data.user); 65 | const redirect = location.search.split('=')[1]; 66 | 67 | if (isAuthenticated() && isAuthenticated().role === 1) { 68 | console.log('Redirecting to admin dashboard'); 69 | navigate('/admin/dashboard'); 70 | } else if ( 71 | isAuthenticated() && 72 | isAuthenticated().role === 0 && 73 | !redirect 74 | ) { 75 | console.log('Redirecting to user dashboard'); 76 | navigate('/'); 77 | } else { 78 | console.log('Redirecting to shipping'); 79 | navigate('/shipping'); 80 | } 81 | }) 82 | .catch(err => { 83 | console.log('signin api function error: ', err); 84 | setFormData({ 85 | ...formData, 86 | loading: false, 87 | errorMsg: err.response.data.errorMessage, 88 | }); 89 | }); 90 | } 91 | }; 92 | 93 | /**************************** 94 | * VIEWS 95 | ***************************/ 96 | const showSigninForm = () => ( 97 |
98 | {/* email */} 99 |
100 |
101 | 102 | 103 | 104 |
105 | 113 |
114 | {/* password */} 115 |
116 |
117 | 118 | 119 | 120 |
121 | 129 |
130 | {/* signin button */} 131 |
132 | 135 |
136 | {/* already have account */} 137 |

138 | Don't have an account? Register here 139 |

140 |
141 | ); 142 | 143 | /**************************** 144 | * RENDERER 145 | ***************************/ 146 | return ( 147 |
155 |
156 |
157 | {errorMsg && showErrorMsg(errorMsg)} 158 | {loading && ( 159 |
{showLoading()}
160 | )} 161 | {showSigninForm()} 162 |
163 |
164 |
165 | ); 166 | }; 167 | 168 | export default Signin; 169 | -------------------------------------------------------------------------------- /client/src/components/Cart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { Link, useNavigate } from 'react-router-dom'; 4 | import { ADD_TO_CART } from '../redux/constants/cartConstants'; 5 | import { deleteFromCart } from '../redux/actions/cartActions'; 6 | import { isAuthenticated } from '../helpers/auth'; 7 | 8 | const Cart = ({ history }) => { 9 | let navigate = useNavigate(); 10 | const { cart } = useSelector(state => state.cart); 11 | 12 | const dispatch = useDispatch(); 13 | 14 | const handleGoBackBtn = () => { 15 | navigate(-1); 16 | }; 17 | 18 | const handleQtyChange = (e, product) => { 19 | const cart = localStorage.getItem('cart') 20 | ? JSON.parse(localStorage.getItem('cart')) 21 | : []; 22 | 23 | cart.forEach(cartItem => { 24 | if (cartItem._id === product._id) { 25 | cartItem.count = e.target.value; 26 | } 27 | }); 28 | 29 | localStorage.setItem('cart', JSON.stringify(cart)); 30 | 31 | dispatch({ 32 | type: ADD_TO_CART, 33 | payload: cart, 34 | }); 35 | }; 36 | 37 | const handleCheckout = evt => { 38 | if (isAuthenticated()) { 39 | navigate('/shipping'); 40 | } else { 41 | navigate('/signin?redirect=shipping'); 42 | } 43 | }; 44 | 45 | return ( 46 |
47 | {cart.length <= 0 ? ( 48 |
49 |

50 | Your cart is empty{' '} 51 | 57 |

58 |
59 | ) : ( 60 | <> 61 |
62 |

Cart

63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | {cart.map(product => ( 78 | 79 | 90 | 98 | 108 | 122 | 138 | 139 | ))} 140 | 141 |
ProductPriceQuantityRemove
80 | {' '} 81 | product 89 | 91 | {' '} 92 | 95 | {product.productName} 96 | 97 | 99 | {' '} 100 | {product.productPrice.toLocaleString( 101 | 'en-US', 102 | { 103 | style: 'currency', 104 | currency: 'USD', 105 | } 106 | )} 107 | 109 | 115 | handleQtyChange( 116 | e, 117 | product 118 | ) 119 | } 120 | /> 121 | 123 | {' '} 124 | 137 |
142 |
143 |
144 |

Cart Summary

145 |

146 | {cart.length === 1 147 | ? '(1) Item' 148 | : `(${cart.length}) Items`} 149 |

150 |

151 | Total: $ 152 | {cart 153 | .reduce( 154 | (currentSum, currentCartItem) => 155 | currentSum + 156 | currentCartItem.count * 157 | currentCartItem.productPrice, 158 | 0 159 | ) 160 | .toFixed(2)} 161 |

162 | 168 |
169 |
170 | 171 | )} 172 |
173 | ); 174 | }; 175 | 176 | export default Cart; 177 | -------------------------------------------------------------------------------- /client/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /client/src/components/Signup.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import isEmpty from 'validator/lib/isEmpty'; 3 | import isEmail from 'validator/lib/isEmail'; 4 | import equals from 'validator/lib/equals'; 5 | import { showErrorMsg, showSuccessMsg } from '../helpers/message'; 6 | import { showLoading } from '../helpers/loading'; 7 | import { isAuthenticated } from '../helpers/auth'; 8 | import { Link, useNavigate } from 'react-router-dom'; 9 | import { signup } from '../api/auth'; 10 | 11 | const Signup = () => { 12 | let navigate = useNavigate(); 13 | 14 | useEffect(() => { 15 | if (isAuthenticated() && isAuthenticated().role === 1) { 16 | navigate('/admin/dashboard'); 17 | } else if (isAuthenticated() && isAuthenticated().role === 0) { 18 | navigate('/'); 19 | } 20 | }, [navigate]); 21 | 22 | const [formData, setFormData] = useState({ 23 | username: '', 24 | email: '', 25 | password: '', 26 | password2: '', 27 | successMsg: false, 28 | errorMsg: false, 29 | loading: false, 30 | }); 31 | const { 32 | username, 33 | email, 34 | password, 35 | password2, 36 | successMsg, 37 | errorMsg, 38 | loading, 39 | } = formData; 40 | /**************************** 41 | * EVENT HANDLERS 42 | ***************************/ 43 | const handleChange = evt => { 44 | //console.log(evt); 45 | setFormData({ 46 | ...formData, 47 | [evt.target.name]: evt.target.value, 48 | successMsg: '', 49 | errorMsg: '', 50 | }); 51 | }; 52 | 53 | const handleSubmit = evt => { 54 | evt.preventDefault(); 55 | 56 | // client-side validation 57 | if ( 58 | isEmpty(username) || 59 | isEmpty(email) || 60 | isEmpty(password) || 61 | isEmpty(password2) 62 | ) { 63 | setFormData({ 64 | ...formData, 65 | errorMsg: 'All fields are required', 66 | }); 67 | } else if (!isEmail(email)) { 68 | setFormData({ 69 | ...formData, 70 | errorMsg: 'Invalid email', 71 | }); 72 | } else if (!equals(password, password2)) { 73 | setFormData({ 74 | ...formData, 75 | errorMsg: 'Passwords do not match', 76 | }); 77 | } else { 78 | const { username, email, password } = formData; 79 | const data = { username, email, password }; 80 | 81 | setFormData({ ...formData, loading: true }); 82 | 83 | signup(data) 84 | .then(response => { 85 | console.log('Axios signup success: ', response); 86 | setFormData({ 87 | username: '', 88 | email: '', 89 | password: '', 90 | password2: '', 91 | loading: false, 92 | successMsg: response.data.successMessage, 93 | }); 94 | }) 95 | .catch(err => { 96 | console.log('Axios signup error: ', err); 97 | setFormData({ 98 | ...formData, 99 | loading: false, 100 | errorMsg: err.response.data.errorMessage, 101 | }); 102 | }); 103 | } 104 | }; 105 | 106 | /**************************** 107 | * VIEWS 108 | ***************************/ 109 | const showSignupForm = () => ( 110 |
111 | {/* username */} 112 |
113 |
114 | 115 | 116 | 117 |
118 | 126 |
127 | {/* email */} 128 |
129 |
130 | 131 | 132 | 133 |
134 | 142 |
143 | {/* password */} 144 |
145 |
146 | 147 | 148 | 149 |
150 | 158 |
159 | {/* password2 */} 160 |
161 |
162 | 163 | 164 | 165 |
166 | 174 |
175 | {/* signup button */} 176 |
177 | 180 |
181 | {/* already have account */} 182 |

183 | Have an account? Log In 184 |

185 |
186 | ); 187 | 188 | /**************************** 189 | * RENDERER 190 | ***************************/ 191 | return ( 192 |
200 |
201 |
202 | {successMsg && showSuccessMsg(successMsg)} 203 | {errorMsg && showErrorMsg(errorMsg)} 204 | {loading && ( 205 |
{showLoading()}
206 | )} 207 | {showSignupForm()} 208 | {/*

{JSON.stringify(formData)}

*/} 209 |
210 |
211 |
212 | ); 213 | }; 214 | 215 | export default Signup; 216 | -------------------------------------------------------------------------------- /client/src/components/AdminProductModal.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from 'react'; 2 | import isEmpty from 'validator/lib/isEmpty'; 3 | import { showErrorMsg, showSuccessMsg } from '../helpers/message'; 4 | import { showLoading } from '../helpers/loading'; 5 | // redux 6 | import { useSelector, useDispatch } from 'react-redux'; 7 | import { clearMessages } from '../redux/actions/messageActions'; 8 | import { createProduct } from '../redux/actions/productActions'; 9 | 10 | const AdminProductModal = () => { 11 | /**************************** 12 | * REDUX GLOBAL STATE PROPERTIES 13 | ***************************/ 14 | const { loading } = useSelector(state => state.loading); 15 | const { successMsg, errorMsg } = useSelector(state => state.messages); 16 | const { categories } = useSelector(state => state.categories); 17 | 18 | const dispatch = useDispatch(); 19 | /**************************** 20 | * COMPONENT STATE PROPERTIES 21 | ***************************/ 22 | const [clientSideError, setClientSideError] = useState(''); 23 | const [productData, setProductData] = useState({ 24 | productImage: null, 25 | productName: '', 26 | productDesc: '', 27 | productPrice: '', 28 | productCategory: '', 29 | productQty: '', 30 | }); 31 | 32 | const { 33 | productImage, 34 | productName, 35 | productDesc, 36 | productPrice, 37 | productCategory, 38 | productQty, 39 | } = productData; 40 | 41 | /**************************** 42 | * EVENT HANDLERS 43 | ***************************/ 44 | const handleMessages = evt => { 45 | dispatch(clearMessages()); 46 | setClientSideError(''); 47 | }; 48 | 49 | const handleProductChange = evt => { 50 | setProductData({ 51 | ...productData, 52 | [evt.target.name]: evt.target.value, 53 | }); 54 | }; 55 | 56 | const handleProductImage = evt => { 57 | console.log(evt.target.files[0]); 58 | setProductData({ 59 | ...productData, 60 | [evt.target.name]: evt.target.files[0], 61 | }); 62 | }; 63 | 64 | const handleProductSubmit = evt => { 65 | evt.preventDefault(); 66 | 67 | if (productImage === null) { 68 | setClientSideError('Please select an image'); 69 | } else if ( 70 | isEmpty(productName) || 71 | isEmpty(productDesc) || 72 | isEmpty(productPrice) 73 | ) { 74 | setClientSideError('Please enter all fields'); 75 | } else if (isEmpty(productCategory)) { 76 | setClientSideError('Please select a category'); 77 | } else if (isEmpty(productQty)) { 78 | setClientSideError('Please select a quantity'); 79 | } else { 80 | let formData = new FormData(); 81 | 82 | formData.append('productImage', productImage); 83 | formData.append('productName', productName); 84 | formData.append('productDesc', productDesc); 85 | formData.append('productPrice', productPrice); 86 | formData.append('productCategory', productCategory); 87 | formData.append('productQty', productQty); 88 | 89 | dispatch(createProduct(formData)); 90 | setProductData({ 91 | productImage: null, 92 | productName: '', 93 | productDesc: '', 94 | productPrice: '', 95 | productCategory: '', 96 | productQty: '', 97 | }); 98 | } 99 | }; 100 | 101 | /**************************** 102 | * RENDERER 103 | ***************************/ 104 | return ( 105 |
106 |
107 |
108 |
109 |
110 |
Add Food
111 | 116 |
117 |
118 | {clientSideError && showErrorMsg(clientSideError)} 119 | {errorMsg && showErrorMsg(errorMsg)} 120 | {successMsg && showSuccessMsg(successMsg)} 121 | 122 | {loading ? ( 123 |
124 | {showLoading()} 125 |
126 | ) : ( 127 | 128 |
129 | 135 | 138 |
139 | 140 |
141 | 144 | 151 |
152 | 153 |
154 | 157 | 164 |
165 | 166 |
167 | 170 | 177 |
178 | 179 |
180 |
181 | 184 | 202 |
203 | 204 |
205 | 208 | 217 |
218 |
219 |
220 | )} 221 |
222 |
223 | 229 | 235 |
236 |
237 |
238 |
239 |
240 | ); 241 | }; 242 | 243 | export default AdminProductModal; 244 | -------------------------------------------------------------------------------- /client/src/components/AdminEditProduct.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, Fragment } from 'react'; 2 | import axios from 'axios'; 3 | import AdminHeader from './AdminHeader'; 4 | import { Link, useParams, useNavigate } from 'react-router-dom'; 5 | import { useDispatch, useSelector } from 'react-redux'; 6 | import { getProduct } from '../redux/actions/productActions'; 7 | import { getCategories } from '../redux/actions/categoryActions'; 8 | 9 | const AdminEditProduct = () => { 10 | /**************************** 11 | * PARAMS 12 | ***************************/ 13 | const { productId } = useParams(); 14 | let navigate = useNavigate(); 15 | 16 | /**************************** 17 | * REDUX GLOBAL STATE PROPERTIES 18 | ***************************/ 19 | const dispatch = useDispatch(); 20 | const { product } = useSelector(state => state.products); 21 | const { categories } = useSelector(state => state.categories); 22 | 23 | /**************************** 24 | * COMPONENT STATE PROPERTIES 25 | ***************************/ 26 | const [productImage, setProductImage] = useState(null); 27 | const [productName, setProductName] = useState(''); 28 | const [productDesc, setProductDesc] = useState(''); 29 | const [productPrice, setProductPrice] = useState(''); 30 | const [productCategory, setProductCategory] = useState(''); 31 | const [productQty, setProductQty] = useState(''); 32 | 33 | /**************************** 34 | * LIFECYCLE METHODS 35 | ***************************/ 36 | useEffect(() => { 37 | if (!product) { 38 | dispatch(getProduct(productId)); 39 | dispatch(getCategories()); 40 | } else { 41 | setProductImage(product.fileName); 42 | setProductName(product.productName); 43 | setProductDesc(product.productDesc); 44 | setProductPrice(product.productPrice); 45 | setProductCategory(product.productCategory); 46 | setProductQty(product.productQty); 47 | } 48 | }, [dispatch, productId, product]); 49 | 50 | /**************************** 51 | * EVENT HANDLERS 52 | ***************************/ 53 | const handleImageUpload = e => { 54 | const image = e.target.files[0]; 55 | setProductImage(image); 56 | }; 57 | 58 | const handleProductSubmit = async e => { 59 | e.preventDefault(); 60 | 61 | const formData = new FormData(); 62 | formData.append('productImage', productImage); 63 | formData.append('productName', productName); 64 | formData.append('productDesc', productDesc); 65 | formData.append('productPrice', productPrice); 66 | formData.append('productCategory', productCategory); 67 | formData.append('productQty', productQty); 68 | 69 | const config = { 70 | headers: { 71 | 'Content-Type': 'multipart/form-data', 72 | }, 73 | }; 74 | 75 | await axios 76 | .put( 77 | `${process.env.REACT_APP_SERVER_URL}/api/product/${productId}`, 78 | formData, 79 | config 80 | ) 81 | .then(res => { 82 | navigate('/admin/dashboard'); 83 | }) 84 | .catch(err => { 85 | console.log(err); 86 | }); 87 | }; 88 | 89 | /**************************** 90 | * RENDERER 91 | ***************************/ 92 | return ( 93 | 94 | 95 |
96 |
97 |
98 | 99 | Go Back 100 | 101 |
102 |
103 |
104 |
105 |
106 |
107 | Update Food 108 |
109 |
110 |
111 | 112 | 122 | {productImage && 123 | productImage.name ? ( 124 | 125 | {productImage.name} 126 | 127 | ) : productImage ? ( 128 | product 137 | ) : null} 138 | 139 |
140 | 143 | 149 | setProductName( 150 | e.target.value 151 | ) 152 | } 153 | /> 154 |
155 |
156 | 159 | 170 |
171 |
172 | 175 | 181 | setProductPrice( 182 | e.target.value 183 | ) 184 | } 185 | /> 186 |
187 |
188 |
189 | 192 | 223 |
224 | 225 |
226 | 229 | 237 | setProductQty( 238 | e.target.value 239 | ) 240 | } 241 | /> 242 |
243 |
244 |
245 |
246 |
247 | 253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 | ); 262 | }; 263 | 264 | export default AdminEditProduct; 265 | -------------------------------------------------------------------------------- /client/.eslintcache: -------------------------------------------------------------------------------- 1 | [{"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/index.js":"1","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/serviceWorker.js":"2","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/App.js":"3","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/store.js":"4","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/Signup.js":"5","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminDashboard.js":"6","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/UserDashboard.js":"7","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminRoute.js":"8","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/Home.js":"9","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/Header.js":"10","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/Signin.js":"11","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/NotFound.js":"12","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/UserRoute.js":"13","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/reducers/loadingReducers.js":"14","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/reducers/categoryReducers.js":"15","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/reducers/productReducers.js":"16","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/reducers/messageReducers.js":"17","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminHeader.js":"18","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminProductModal.js":"19","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminActionBtns.js":"20","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminBody.js":"21","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminCategoryModal.js":"22","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/helpers/loading.js":"23","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/helpers/message.js":"24","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/helpers/auth.js":"25","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/api/auth.js":"26","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/actions/productActions.js":"27","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/actions/categoryActions.js":"28","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/constants/productConstants.js":"29","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/constants/loadingConstants.js":"30","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/constants/categoryConstants.js":"31","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/constants/messageConstants.js":"32","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/actions/messageActions.js":"33","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/Card.js":"34","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/helpers/cookies.js":"35","/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/helpers/localStorage.js":"36"},{"size":355,"mtime":1605745272241,"results":"37","hashOfConfig":"38"},{"size":5085,"mtime":1583864414880,"results":"39","hashOfConfig":"38"},{"size":983,"mtime":1609551447348,"results":"40","hashOfConfig":"38"},{"size":734,"mtime":1607475962794,"results":"41","hashOfConfig":"38"},{"size":6960,"mtime":1604972631055,"results":"42","hashOfConfig":"38"},{"size":850,"mtime":1607693312022,"results":"43","hashOfConfig":"38"},{"size":136,"mtime":1591837024150,"results":"44","hashOfConfig":"38"},{"size":536,"mtime":1593057430154,"results":"45","hashOfConfig":"38"},{"size":115,"mtime":1585069439788,"results":"46","hashOfConfig":"38"},{"size":3914,"mtime":1593643999100,"results":"47","hashOfConfig":"38"},{"size":5135,"mtime":1607643650496,"results":"48","hashOfConfig":"38"},{"size":127,"mtime":1585069435514,"results":"49","hashOfConfig":"38"},{"size":534,"mtime":1593057425864,"results":"50","hashOfConfig":"38"},{"size":391,"mtime":1606238208218,"results":"51","hashOfConfig":"38"},{"size":483,"mtime":1606436541027,"results":"52","hashOfConfig":"38"},{"size":583,"mtime":1609551447350,"results":"53","hashOfConfig":"38"},{"size":584,"mtime":1606339307549,"results":"54","hashOfConfig":"38"},{"size":322,"mtime":1605218153982,"results":"55","hashOfConfig":"38"},{"size":6420,"mtime":1607449859739,"results":"56","hashOfConfig":"38"},{"size":888,"mtime":1605218342164,"results":"57","hashOfConfig":"38"},{"size":447,"mtime":1609551487225,"results":"58","hashOfConfig":"38"},{"size":2973,"mtime":1606785811991,"results":"59","hashOfConfig":"38"},{"size":1065,"mtime":1595451709630,"results":"60","hashOfConfig":"38"},{"size":274,"mtime":1595452817239,"results":"61","hashOfConfig":"38"},{"size":577,"mtime":1592434280162,"results":"62","hashOfConfig":"38"},{"size":519,"mtime":1591138116967,"results":"63","hashOfConfig":"38"},{"size":1815,"mtime":1609551447349,"results":"64","hashOfConfig":"38"},{"size":1408,"mtime":1606756686355,"results":"65","hashOfConfig":"38"},{"size":140,"mtime":1609551447349,"results":"66","hashOfConfig":"38"},{"size":90,"mtime":1606237993614,"results":"67","hashOfConfig":"38"},{"size":98,"mtime":1606783741167,"results":"68","hashOfConfig":"38"},{"size":164,"mtime":1606339041147,"results":"69","hashOfConfig":"38"},{"size":158,"mtime":1606437677645,"results":"70","hashOfConfig":"38"},{"size":1332,"mtime":1609551529850,"results":"71","hashOfConfig":"38"},{"size":262,"mtime":1590005844860,"results":"72","hashOfConfig":"38"},{"size":285,"mtime":1590005846291,"results":"73","hashOfConfig":"38"},{"filePath":"74","messages":"75","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},"wgcgf6",{"filePath":"77","messages":"78","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"79","messages":"80","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"81","messages":"82","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"83","messages":"84","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"85","messages":"86","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"87","messages":"88","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"89","messages":"90","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"91","messages":"92","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"93","messages":"94","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"95","messages":"96","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"97","messages":"98","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"99","messages":"100","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"101","messages":"102","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"103","messages":"104","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"105","messages":"106","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"107","messages":"108","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"109","messages":"110","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"111","messages":"112","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"113","messages":"114","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"115","messages":"116","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"117","messages":"118","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"119","messages":"120","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"121","messages":"122","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"123","messages":"124","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"125","messages":"126","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"127","messages":"128","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"129","messages":"130","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"131","messages":"132","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"133","messages":"134","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"135","messages":"136","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"137","messages":"138","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"139","messages":"140","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"141","messages":"142","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"143","messages":"144","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"145"},{"filePath":"146","messages":"147","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/index.js",[],["148","149"],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/serviceWorker.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/App.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/store.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/Signup.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminDashboard.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/UserDashboard.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminRoute.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/Home.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/Header.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/Signin.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/NotFound.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/UserRoute.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/reducers/loadingReducers.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/reducers/categoryReducers.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/reducers/productReducers.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/reducers/messageReducers.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminHeader.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminProductModal.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminActionBtns.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminBody.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/AdminCategoryModal.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/helpers/loading.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/helpers/message.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/helpers/auth.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/api/auth.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/actions/productActions.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/actions/categoryActions.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/constants/productConstants.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/constants/loadingConstants.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/constants/categoryConstants.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/constants/messageConstants.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/redux/actions/messageActions.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/components/Card.js",[],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/helpers/cookies.js",[],["150","151"],"/Users/jorgealvarado/Sites/restaurant-tutorial/client/src/helpers/localStorage.js",[],{"ruleId":"152","replacedBy":"153"},{"ruleId":"154","replacedBy":"155"},{"ruleId":"152","replacedBy":"156"},{"ruleId":"154","replacedBy":"157"},"no-native-reassign",["158"],"no-negated-in-lhs",["159"],["158"],["159"],"no-global-assign","no-unsafe-negation"] --------------------------------------------------------------------------------