├── .gitignore ├── .gitattributes ├── frontend ├── public │ ├── favicon.ico │ ├── robots.txt │ ├── images │ │ ├── logo.png │ │ ├── success.png │ │ ├── products │ │ │ ├── 1.jpg │ │ │ ├── 2.jpg │ │ │ ├── 3.jpg │ │ │ ├── 4.jpg │ │ │ ├── 5.jpg │ │ │ ├── 6.jpg │ │ │ └── 7.jpg │ │ └── default_avatar.png │ ├── manifest.json │ └── index.html ├── src │ ├── components │ │ ├── layouts │ │ │ ├── Loader.js │ │ │ ├── MetaData.js │ │ │ ├── Footer.js │ │ │ ├── Search.js │ │ │ └── Header.js │ │ ├── cart │ │ │ ├── OrderSuccess.js │ │ │ ├── CheckoutStep.js │ │ │ ├── ConfirmOrder.js │ │ │ ├── Cart.js │ │ │ ├── Payment.js │ │ │ └── Shipping.js │ │ ├── route │ │ │ └── ProtectedRoute.js │ │ ├── product │ │ │ ├── ProductReview.js │ │ │ ├── Product.js │ │ │ └── ProductSearch.js │ │ ├── user │ │ │ ├── Profile.js │ │ │ ├── ForgotPassword.js │ │ │ ├── UpdatePassword.js │ │ │ ├── ResetPassword.js │ │ │ ├── Login.js │ │ │ ├── UpdateProfile.js │ │ │ └── Register.js │ │ ├── admin │ │ │ ├── Sidebar.js │ │ │ ├── UserList.js │ │ │ ├── ProductList.js │ │ │ ├── OrderList.js │ │ │ ├── UpdateUser.js │ │ │ ├── ReviewList.js │ │ │ ├── Dashboard.js │ │ │ └── UpdateOrder.js │ │ ├── Home.js │ │ └── order │ │ │ ├── UserOrders.js │ │ │ └── OrderDetail.js │ ├── index.js │ ├── actions │ │ ├── cartActions.js │ │ ├── orderActions.js │ │ ├── productActions.js │ │ └── userActions.js │ ├── store.js │ ├── slices │ │ ├── productsSlice.js │ │ ├── cartSlice.js │ │ ├── userSlice.js │ │ ├── orderSlice.js │ │ ├── authSlice.js │ │ └── productSlice.js │ └── App.js ├── .gitignore ├── package.json └── README.md ├── backend ├── uploads │ ├── user │ │ ├── user.png │ │ ├── Apple-PNG-Images.png │ │ └── alexander-hipp-iEEBWgY_6lA-unsplash.jpg │ └── product │ │ ├── pexels-math-90946.jpg │ │ └── pexels-pixabay-279906.jpg ├── middlewares │ ├── catchAsyncError.js │ ├── authenticate.js │ └── error.js ├── utils │ ├── errorHandler.js │ ├── jwt.js │ ├── seeder.js │ ├── email.js │ └── apiFeatures.js ├── config │ ├── database.js │ └── config.env ├── routes │ ├── payment.js │ ├── order.js │ ├── product.js │ └── auth.js ├── controllers │ ├── paymentController.js │ ├── orderController.js │ ├── productController.js │ └── authController.js ├── server.js ├── app.js ├── models │ ├── userModel.js │ ├── productModel.js │ └── orderModel.js └── data │ └── products.json ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /backend/uploads/user/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/backend/uploads/user/user.png -------------------------------------------------------------------------------- /frontend/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/frontend/public/images/logo.png -------------------------------------------------------------------------------- /frontend/public/images/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/frontend/public/images/success.png -------------------------------------------------------------------------------- /frontend/public/images/products/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/frontend/public/images/products/1.jpg -------------------------------------------------------------------------------- /frontend/public/images/products/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/frontend/public/images/products/2.jpg -------------------------------------------------------------------------------- /frontend/public/images/products/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/frontend/public/images/products/3.jpg -------------------------------------------------------------------------------- /frontend/public/images/products/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/frontend/public/images/products/4.jpg -------------------------------------------------------------------------------- /frontend/public/images/products/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/frontend/public/images/products/5.jpg -------------------------------------------------------------------------------- /frontend/public/images/products/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/frontend/public/images/products/6.jpg -------------------------------------------------------------------------------- /frontend/public/images/products/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/frontend/public/images/products/7.jpg -------------------------------------------------------------------------------- /backend/uploads/user/Apple-PNG-Images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/backend/uploads/user/Apple-PNG-Images.png -------------------------------------------------------------------------------- /frontend/public/images/default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/frontend/public/images/default_avatar.png -------------------------------------------------------------------------------- /backend/uploads/product/pexels-math-90946.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/backend/uploads/product/pexels-math-90946.jpg -------------------------------------------------------------------------------- /backend/middlewares/catchAsyncError.js: -------------------------------------------------------------------------------- 1 | module.exports = func => (req, res, next)=> 2 | Promise.resolve(func(req, res, next)).catch(next) 3 | -------------------------------------------------------------------------------- /backend/uploads/product/pexels-pixabay-279906.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/backend/uploads/product/pexels-pixabay-279906.jpg -------------------------------------------------------------------------------- /frontend/src/components/layouts/Loader.js: -------------------------------------------------------------------------------- 1 | export default function Loader() { 2 | return ( 3 |
4 | ) 5 | } -------------------------------------------------------------------------------- /backend/uploads/user/alexander-hipp-iEEBWgY_6lA-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlcode/jvlcart/HEAD/backend/uploads/user/alexander-hipp-iEEBWgY_6lA-unsplash.jpg -------------------------------------------------------------------------------- /frontend/src/components/layouts/MetaData.js: -------------------------------------------------------------------------------- 1 | import { Helmet } from "react-helmet-async" 2 | 3 | export default function MetaData({title}) { 4 | return ( 5 | 6 | {`${title} - JVLcart`} 7 | 8 | ) 9 | } -------------------------------------------------------------------------------- /backend/utils/errorHandler.js: -------------------------------------------------------------------------------- 1 | class ErrorHandler extends Error { 2 | constructor(message, statusCode){ 3 | super(message) 4 | this.statusCode = statusCode; 5 | Error.captureStackTrace(this, this.constructor) 6 | } 7 | } 8 | 9 | module.exports = ErrorHandler; -------------------------------------------------------------------------------- /frontend/src/components/layouts/Footer.js: -------------------------------------------------------------------------------- 1 | export default function Footer (){ 2 | return ( 3 | 8 | ) 9 | } -------------------------------------------------------------------------------- /backend/config/database.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const connectDatabase = ()=>{ 4 | mongoose.connect(process.env.DB_LOCAL_URI,{ 5 | useNewUrlParser:true, 6 | useUnifiedTopology:true 7 | }).then(con=>{ 8 | console.log(`MongoDB is connected to the host: ${con.connection.host} `) 9 | }) 10 | } 11 | 12 | module.exports = connectDatabase; -------------------------------------------------------------------------------- /frontend/.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 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import store from './store' 5 | import {Provider } from 'react-redux'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | // 10 | 11 | 12 | 13 | // 14 | ); 15 | 16 | -------------------------------------------------------------------------------- /backend/routes/payment.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { processPayment, sendStripeApi } = require('../controllers/paymentController'); 3 | const { isAuthenticatedUser } = require('../middlewares/authenticate'); 4 | const router = express.Router(); 5 | 6 | router.route('/payment/process').post( isAuthenticatedUser, processPayment); 7 | router.route('/stripeapi').get( isAuthenticatedUser, sendStripeApi); 8 | 9 | 10 | module.exports = router; -------------------------------------------------------------------------------- /backend/config/config.env: -------------------------------------------------------------------------------- 1 | PORT = 8000 2 | NODE_ENV = production 3 | DB_LOCAL_URI= %YOUR_MONGODB_CONNECTION_STRING% 4 | JWT_SECRET=F66DB4EC116F3 5 | JWT_EXPIRES_TIME=7d 6 | COOKIE_EXPIRES_TIME=7 7 | SMTP_HOST=smtp.mailtrap.io 8 | SMTP_PORT=2525 9 | SMTP_USER=%YOUR_MAILTRAP_USERNAME% 10 | SMTP_PASS=%YOUR_MAILTRAP_PASS% 11 | SMTP_FROM_NAME=JVLcart 12 | SMTP_FROM_EMAIL=noreply@jvlcart.com 13 | BACKEND_URL=http://127.0.0.1:8000 14 | FRONTEND_URL=http://127.0.0.1:3000 15 | STRIPE_API_KEY=%YOUR_STRIPE_PUBLIC_KEY% 16 | STRIPE_SECRET_KEY=%YOUR_STRIPE_SECRET_KEY% -------------------------------------------------------------------------------- /frontend/src/components/cart/OrderSuccess.js: -------------------------------------------------------------------------------- 1 | export default function OrderSuccess() { 2 | return ( 3 |
4 |
5 | Order Success 6 | 7 |

Your Order has been placed successfully.

8 | 9 | Go to Orders 10 |
11 | 12 |
13 | ) 14 | } -------------------------------------------------------------------------------- /backend/utils/jwt.js: -------------------------------------------------------------------------------- 1 | const sendToken = (user, statusCode, res) => { 2 | 3 | //Creating JWT Token 4 | const token = user.getJwtToken(); 5 | 6 | //setting cookies 7 | const options = { 8 | expires: new Date( 9 | Date.now() + process.env.COOKIE_EXPIRES_TIME * 24 * 60 * 60 * 1000 10 | ), 11 | httpOnly: true, 12 | } 13 | 14 | res.status(statusCode) 15 | .cookie('token', token, options) 16 | .json({ 17 | success: true, 18 | token, 19 | user 20 | }) 21 | 22 | 23 | } 24 | 25 | module.exports = sendToken; -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /backend/utils/seeder.js: -------------------------------------------------------------------------------- 1 | const products = require('../data/products.json'); 2 | const Product = require('../models/productModel'); 3 | const dotenv = require('dotenv'); 4 | const connectDatabase = require('../config/database') 5 | 6 | dotenv.config({path:'backend/config/config.env'}); 7 | connectDatabase(); 8 | 9 | const seedProducts = async ()=>{ 10 | try{ 11 | await Product.deleteMany(); 12 | console.log('Products deleted!') 13 | await Product.insertMany(products); 14 | console.log('All products added!'); 15 | }catch(error){ 16 | console.log(error.message); 17 | } 18 | process.exit(); 19 | } 20 | 21 | seedProducts(); -------------------------------------------------------------------------------- /frontend/src/actions/cartActions.js: -------------------------------------------------------------------------------- 1 | import {addCartItemRequest, addCartItemSuccess} from '../slices/cartSlice'; 2 | import axios from 'axios' 3 | 4 | export const addCartItem = (id, quantity) => async(dispatch) => { 5 | try { 6 | dispatch(addCartItemRequest()) 7 | const {data } = await axios.get(`/api/v1/product/${id}`) 8 | dispatch(addCartItemSuccess({ 9 | product: data.product._id, 10 | name: data.product.name, 11 | price: data.product.price, 12 | image: data.product.images[0].image, 13 | stock: data.product.stock, 14 | quantity 15 | })) 16 | } catch (error) { 17 | 18 | } 19 | } -------------------------------------------------------------------------------- /frontend/src/components/route/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import {Navigate} from 'react-router-dom'; 3 | import Loader from '../layouts/Loader'; 4 | 5 | export default function ProtectedRoute ({children, isAdmin}) { 6 | const { isAuthenticated, loading, user } = useSelector(state => state.authState) 7 | 8 | if(!isAuthenticated && !loading) { 9 | return 10 | } 11 | 12 | if(isAuthenticated) { 13 | if(isAdmin === true && user.role !== 'admin') { 14 | return 15 | } 16 | return children; 17 | } 18 | 19 | if(loading) { 20 | return 21 | } 22 | 23 | 24 | } -------------------------------------------------------------------------------- /backend/utils/email.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer') 2 | 3 | const sendEmail = async options => { 4 | const transport = { 5 | host: process.env.SMTP_HOST, 6 | port: process.env.SMTP_PORT, 7 | auth: { 8 | user: process.env.SMTP_USER, 9 | pass: process.env.SMTP_PASS 10 | } 11 | }; 12 | 13 | const transporter = nodemailer.createTransport(transport); 14 | 15 | const message = { 16 | from: `${process.env.SMTP_FROM_NAME} <${process.env.SMTP_FROM_EMAIL}>`, 17 | to: options.email, 18 | subject: options.subject, 19 | text: options.message 20 | } 21 | 22 | await transporter.sendMail(message) 23 | } 24 | 25 | module.exports = sendEmail -------------------------------------------------------------------------------- /frontend/src/components/product/ProductReview.js: -------------------------------------------------------------------------------- 1 | export default function ProductReview({reviews}) { 2 | return ( 3 |
4 |

Other's Reviews:

5 |
6 | {reviews && reviews.map(review => ( 7 |
8 |
9 |
10 |
11 |

by {review.user.name}

12 |

{review.comment}

13 | 14 |
15 |
16 | )) 17 | } 18 | 19 |
20 | ) 21 | } -------------------------------------------------------------------------------- /frontend/src/store.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from "@reduxjs/toolkit"; 2 | import thunk from "redux-thunk"; 3 | import productsReducer from "./slices/productsSlice"; 4 | import productReducer from './slices/productSlice'; 5 | import authReducer from './slices/authSlice'; 6 | import cartReducer from './slices/cartSlice'; 7 | import orderReducer from './slices/orderSlice'; 8 | import userReducer from './slices/userSlice' 9 | 10 | 11 | const reducer = combineReducers({ 12 | productsState: productsReducer, 13 | productState: productReducer , 14 | authState: authReducer, 15 | cartState: cartReducer, 16 | orderState: orderReducer, 17 | userState: userReducer 18 | }) 19 | 20 | 21 | const store = configureStore({ 22 | reducer, 23 | middleware: [thunk] 24 | }) 25 | 26 | export default store; -------------------------------------------------------------------------------- /backend/routes/order.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { newOrder, getSingleOrder, myOrders, orders, updateOrder, deleteOrder } = require('../controllers/orderController'); 3 | const router = express.Router(); 4 | const {isAuthenticatedUser, authorizeRoles} = require('../middlewares/authenticate'); 5 | 6 | router.route('/order/new').post(isAuthenticatedUser,newOrder); 7 | router.route('/order/:id').get(isAuthenticatedUser,getSingleOrder); 8 | router.route('/myorders').get(isAuthenticatedUser,myOrders); 9 | 10 | //Admin Routes 11 | router.route('/admin/orders').get(isAuthenticatedUser, authorizeRoles('admin'), orders) 12 | router.route('/admin/order/:id').put(isAuthenticatedUser, authorizeRoles('admin'), updateOrder) 13 | .delete(isAuthenticatedUser, authorizeRoles('admin'), deleteOrder) 14 | 15 | module.exports = router; -------------------------------------------------------------------------------- /backend/controllers/paymentController.js: -------------------------------------------------------------------------------- 1 | const catchAsyncError = require('../middlewares/catchAsyncError'); 2 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY) 3 | 4 | exports.processPayment = catchAsyncError(async(req, res, next) => { 5 | const paymentIntent = await stripe.paymentIntents.create({ 6 | amount: req.body.amount, 7 | currency: "usd", 8 | description: "TEST PAYMENT", 9 | metadata: { integration_check: "accept_payment"}, 10 | shipping: req.body.shipping 11 | }) 12 | 13 | res.status(200).json({ 14 | success: true, 15 | client_secret: paymentIntent.client_secret 16 | }) 17 | }) 18 | 19 | exports.sendStripeApi = catchAsyncError(async(req, res, next) => { 20 | res.status(200).json({ 21 | stripeApiKey: process.env.STRIPE_API_KEY 22 | }) 23 | }) 24 | 25 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const path = require('path'); 3 | const connectDatabase = require('./config/database'); 4 | 5 | 6 | connectDatabase(); 7 | 8 | const server = app.listen(process.env.PORT,()=>{ 9 | console.log(`My Server listening to the port: ${process.env.PORT} in ${process.env.NODE_ENV} `) 10 | }) 11 | 12 | process.on('unhandledRejection',(err)=>{ 13 | console.log(`Error: ${err.message}`); 14 | console.log('Shutting down the server due to unhandled rejection error'); 15 | server.close(()=>{ 16 | process.exit(1); 17 | }) 18 | }) 19 | 20 | process.on('uncaughtException',(err)=>{ 21 | console.log(`Error: ${err.message}`); 22 | console.log('Shutting down the server due to uncaught exception error'); 23 | server.close(()=>{ 24 | process.exit(1); 25 | }) 26 | }) 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /backend/middlewares/authenticate.js: -------------------------------------------------------------------------------- 1 | const ErrorHandler = require("../utils/errorHandler"); 2 | const User = require('../models/userModel') 3 | const catchAsyncError = require("./catchAsyncError"); 4 | const jwt = require('jsonwebtoken'); 5 | 6 | exports.isAuthenticatedUser = catchAsyncError( async (req, res, next) => { 7 | const { token } = req.cookies; 8 | 9 | if( !token ){ 10 | return next(new ErrorHandler('Login first to handle this resource', 401)) 11 | } 12 | 13 | const decoded = jwt.verify(token, process.env.JWT_SECRET) 14 | req.user = await User.findById(decoded.id) 15 | next(); 16 | }) 17 | 18 | exports.authorizeRoles = (...roles) => { 19 | return (req, res, next) => { 20 | if(!roles.includes(req.user.role)){ 21 | return next(new ErrorHandler(`Role ${req.user.role} is not allowed`, 401)) 22 | } 23 | next() 24 | } 25 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jvlcart", 3 | "version": "1.0.0", 4 | "description": "ecommerce using MERN stack", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "nodemon backend/server.js", 8 | "dev": "set NODE_ENV=development&&nodemon backend/server.js", 9 | "prod": "set NODE_ENV=production&&nodemon backend/server.js", 10 | "seeder": "node backend/utils/seeder.js" 11 | }, 12 | "author": "JV Logesh", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@reduxjs/toolkit": "^1.9.1", 16 | "bcrypt": "^5.1.0", 17 | "cookie-parser": "^1.4.6", 18 | "crypto": "^1.0.1", 19 | "dotenv": "^16.0.3", 20 | "express": "^4.18.2", 21 | "jsonwebtoken": "^8.5.1", 22 | "mongoose": "^6.8.0", 23 | "multer": "^1.4.5-lts.1", 24 | "nodemailer": "^6.8.0", 25 | "stripe": "^11.9.1", 26 | "validator": "^13.7.0" 27 | }, 28 | "devDependencies": { 29 | "nodemon": "^2.0.20" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JVL cart 2 | 3 | an E-commerce Website built with MERN stack. 4 | 5 | ## Instructions 6 | 7 | after cloning, run this command in the root folder 8 | ```bash 9 | npm install 10 | ``` 11 | navigate to "frontend" folder, run these commands 12 | ```bash 13 | npm install 14 | npm run build 15 | ``` 16 | wait for application build 17 | after that open the backend/config/config.env 18 | and update the MongoDB connection string 19 | ```bash 20 | ... 21 | DB_LOCAL_URI=mongodb://localhost:27017/jvlcart 22 | ``` 23 | 24 | navigate back to "root" folder and run this command for loading demo data 25 | ```bash 26 | npm run seeder 27 | ``` 28 | 29 | run this below command to run the app in production mode 30 | ```bash 31 | npm run prod 32 | ``` 33 | 34 | 35 | ## Test 36 | open the http://localhost:8000 and test the 37 | 38 | ## Postman Collection 39 | https://www.postman.com/jvlcode/workspace/nodejs-ecommerce/collection/19530322-997cf450-820a-4852-bc1f-a93c9072d6ec?action=share&creator=19530322 40 | 41 | 42 | ## License 43 | 44 | [MIT](https://choosealicense.com/licenses/mit/) -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const errorMiddleware = require('./middlewares/error'); 4 | const cookieParser = require('cookie-parser') 5 | const path = require('path') 6 | const dotenv = require('dotenv'); 7 | dotenv.config({path:path.join(__dirname,"config/config.env")}); 8 | 9 | 10 | app.use(express.json()); 11 | app.use(cookieParser()); 12 | app.use('/uploads', express.static(path.join(__dirname,'uploads') ) ) 13 | 14 | const products = require('./routes/product') 15 | const auth = require('./routes/auth') 16 | const order = require('./routes/order') 17 | const payment = require('./routes/payment') 18 | 19 | app.use('/api/v1/',products); 20 | app.use('/api/v1/',auth); 21 | app.use('/api/v1/',order); 22 | app.use('/api/v1/',payment); 23 | 24 | if(process.env.NODE_ENV === "production") { 25 | app.use(express.static(path.join(__dirname, '../frontend/build'))); 26 | app.get('*', (req, res) =>{ 27 | res.sendFile(path.resolve(__dirname, '../frontend/build/index.html')) 28 | }) 29 | } 30 | 31 | app.use(errorMiddleware) 32 | 33 | module.exports = app; -------------------------------------------------------------------------------- /backend/utils/apiFeatures.js: -------------------------------------------------------------------------------- 1 | class APIFeatures { 2 | constructor(query, queryStr){ 3 | this.query = query; 4 | this.queryStr = queryStr; 5 | } 6 | 7 | search(){ 8 | let keyword = this.queryStr.keyword ? { 9 | name: { 10 | $regex: this.queryStr.keyword, 11 | $options: 'i' 12 | } 13 | }: {}; 14 | 15 | this.query.find({...keyword}) 16 | return this; 17 | } 18 | 19 | 20 | filter(){ 21 | const queryStrCopy = { ...this.queryStr }; 22 | 23 | //removing fields from query 24 | const removeFields = ['keyword', 'limit', 'page']; 25 | removeFields.forEach( field => delete queryStrCopy[field]); 26 | 27 | let queryStr = JSON.stringify(queryStrCopy); 28 | queryStr = queryStr.replace(/\b(gt|gte|lt|lte)/g, match => `$${match}`) 29 | 30 | this.query.find(JSON.parse(queryStr)); 31 | 32 | return this; 33 | } 34 | 35 | paginate(resPerPage){ 36 | const currentPage = Number(this.queryStr.page) || 1; 37 | const skip = resPerPage * (currentPage - 1) 38 | this.query.limit(resPerPage).skip(skip); 39 | return this; 40 | } 41 | } 42 | 43 | module.exports = APIFeatures; -------------------------------------------------------------------------------- /frontend/src/components/product/Product.js: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | export default function Product ({product, col}) { 4 | return ( 5 |
6 |
7 | {product.images.length > 0 && 8 | {product.name}} 13 |
14 |
15 | {product.name} 16 |
17 |
18 |
19 |
20 |
21 | ({product.numOfReviews} Reviews) 22 |
23 |

${product.price}

24 | View Details 25 |
26 |
27 |
28 | ) 29 | } -------------------------------------------------------------------------------- /frontend/src/components/layouts/Search.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useLocation, useNavigate } from 'react-router-dom' 3 | 4 | export default function Search () { 5 | 6 | const navigate = useNavigate(); 7 | const location = useLocation(); 8 | const [keyword, setKeyword] = useState("") 9 | 10 | const searchHandler = (e) => { 11 | e.preventDefault(); 12 | navigate(`/search/${keyword}`) 13 | 14 | } 15 | 16 | const clearKeyword = () =>{ 17 | setKeyword(""); 18 | } 19 | 20 | useEffect(() => { 21 | if(location.pathname === '/') { 22 | clearKeyword(); 23 | } 24 | },[location]) 25 | 26 | return ( 27 |
28 |
29 | { setKeyword(e.target.value) }} 35 | value={keyword} 36 | /> 37 |
38 | 41 |
42 |
43 |
44 | ) 45 | } -------------------------------------------------------------------------------- /frontend/src/components/user/Profile.js: -------------------------------------------------------------------------------- 1 | import {useSelector } from 'react-redux'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export default function Profile () { 5 | const { user } = useSelector(state => state.authState); 6 | 7 | return ( 8 |
9 |
10 |
11 | 12 |
13 | 14 | Edit Profile 15 | 16 |
17 | 18 |
19 |

Full Name

20 |

{user.name}

21 | 22 |

Email Address

23 |

{user.email}

24 | 25 |

Joined

26 |

{String(user.createdAt).substring(0, 10)}

27 | 28 | 29 | My Orders 30 | 31 | 32 | 33 | Change Password 34 | 35 |
36 |
37 | ) 38 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@stripe/react-stripe-js": "^1.16.4", 7 | "@stripe/stripe-js": "^1.46.0", 8 | "@testing-library/jest-dom": "^5.16.5", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "axios": "^1.2.2", 12 | "countries-list": "^2.6.1", 13 | "mdbreact": "^5.2.0", 14 | "rc-slider": "^10.1.0", 15 | "rc-tooltip": "^5.3.0", 16 | "react": "^18.2.0", 17 | "react-bootstrap": "^1.6.6", 18 | "react-dom": "^18.2.0", 19 | "react-helmet-async": "^1.3.0", 20 | "react-js-pagination": "^3.0.3", 21 | "react-redux": "^8.0.5", 22 | "react-router-dom": "^6.6.1", 23 | "react-scripts": "5.0.1", 24 | "react-toastify": "^9.1.1", 25 | "redux": "^4.2.0", 26 | "redux-devtools-extension": "^2.13.9", 27 | "redux-thunk": "^2.4.2", 28 | "web-vitals": "^2.1.4" 29 | }, 30 | "scripts": { 31 | "start": "react-scripts start", 32 | "build": "react-scripts build", 33 | "test": "react-scripts test", 34 | "eject": "react-scripts eject" 35 | }, 36 | "eslintConfig": { 37 | "extends": [ 38 | "react-app", 39 | "react-app/jest" 40 | ] 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | }, 54 | "proxy": "http://127.0.0.1:8000" 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/components/admin/Sidebar.js: -------------------------------------------------------------------------------- 1 | import { Link, useNavigate } from 'react-router-dom'; 2 | import { NavDropdown } from 'react-bootstrap'; 3 | 4 | export default function Sidebar () { 5 | 6 | const navigate = useNavigate(); 7 | 8 | return ( 9 |
10 | 39 |
40 | ) 41 | } -------------------------------------------------------------------------------- /backend/routes/product.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { getProducts, newProduct, getSingleProduct, updateProduct, deleteProduct, createReview, getReviews, deleteReview, getAdminProducts } = require('../controllers/productController'); 3 | const router = express.Router(); 4 | const {isAuthenticatedUser, authorizeRoles } = require('../middlewares/authenticate'); 5 | const multer = require('multer'); 6 | const path = require('path') 7 | 8 | const upload = multer({storage: multer.diskStorage({ 9 | destination: function(req, file, cb) { 10 | cb(null, path.join( __dirname,'..' , 'uploads/product' ) ) 11 | }, 12 | filename: function(req, file, cb ) { 13 | cb(null, file.originalname) 14 | } 15 | }) }) 16 | 17 | 18 | router.route('/products').get( getProducts); 19 | router.route('/product/:id') 20 | .get(getSingleProduct); 21 | 22 | 23 | router.route('/review').put(isAuthenticatedUser, createReview) 24 | 25 | 26 | 27 | //Admin routes 28 | router.route('/admin/product/new').post(isAuthenticatedUser, authorizeRoles('admin'), upload.array('images'), newProduct); 29 | router.route('/admin/products').get(isAuthenticatedUser, authorizeRoles('admin'), getAdminProducts); 30 | router.route('/admin/product/:id').delete(isAuthenticatedUser, authorizeRoles('admin'), deleteProduct); 31 | router.route('/admin/product/:id').put(isAuthenticatedUser, authorizeRoles('admin'),upload.array('images'), updateProduct); 32 | router.route('/admin/reviews').get(isAuthenticatedUser, authorizeRoles('admin'),getReviews) 33 | router.route('/admin/review').delete(isAuthenticatedUser, authorizeRoles('admin'),deleteReview) 34 | module.exports = router; -------------------------------------------------------------------------------- /backend/middlewares/error.js: -------------------------------------------------------------------------------- 1 | module.exports = (err, req, res, next) =>{ 2 | err.statusCode = err.statusCode || 500; 3 | 4 | 5 | if(process.env.NODE_ENV == 'development'){ 6 | res.status(err.statusCode).json({ 7 | success: false, 8 | message: err.message, 9 | stack: err.stack, 10 | error: err 11 | }) 12 | } 13 | 14 | if(process.env.NODE_ENV == 'production'){ 15 | let message = err.message; 16 | let error = new Error(message); 17 | 18 | 19 | if(err.name == "ValidationError") { 20 | message = Object.values(err.errors).map(value => value.message) 21 | error = new Error(message) 22 | err.statusCode = 400 23 | } 24 | 25 | if(err.name == 'CastError'){ 26 | message = `Resource not found: ${err.path}` ; 27 | error = new Error(message) 28 | err.statusCode = 400 29 | } 30 | 31 | if(err.code == 11000) { 32 | let message = `Duplicate ${Object.keys(err.keyValue)} error`; 33 | error = new Error(message) 34 | err.statusCode = 400 35 | } 36 | 37 | if(err.name == 'JSONWebTokenError') { 38 | let message = `JSON Web Token is invalid. Try again`; 39 | error = new Error(message) 40 | err.statusCode = 400 41 | } 42 | 43 | if(err.name == 'TokenExpiredError') { 44 | let message = `JSON Web Token is expired. Try again`; 45 | error = new Error(message) 46 | err.statusCode = 400 47 | } 48 | 49 | res.status(err.statusCode).json({ 50 | success: false, 51 | message: error.message || 'Internal Server Error', 52 | }) 53 | } 54 | } -------------------------------------------------------------------------------- /backend/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const multer = require('multer'); 3 | const path = require('path') 4 | 5 | const upload = multer({storage: multer.diskStorage({ 6 | destination: function(req, file, cb) { 7 | cb(null, path.join( __dirname,'..' , 'uploads/user' ) ) 8 | }, 9 | filename: function(req, file, cb ) { 10 | cb(null, file.originalname) 11 | } 12 | }) }) 13 | 14 | 15 | const { 16 | registerUser, 17 | loginUser, 18 | logoutUser, 19 | forgotPassword, 20 | resetPassword, 21 | getUserProfile, 22 | changePassword, 23 | updateProfile, 24 | getAllUsers, 25 | getUser, 26 | updateUser, 27 | deleteUser 28 | } = require('../controllers/authController'); 29 | const router = express.Router(); 30 | const { isAuthenticatedUser, authorizeRoles } = require('../middlewares/authenticate') 31 | 32 | router.route('/register').post(upload.single('avatar'), registerUser); 33 | router.route('/login').post(loginUser); 34 | router.route('/logout').get(logoutUser); 35 | router.route('/password/forgot').post(forgotPassword); 36 | router.route('/password/reset/:token').post(resetPassword); 37 | router.route('/password/change').put(isAuthenticatedUser, changePassword); 38 | router.route('/myprofile').get(isAuthenticatedUser, getUserProfile); 39 | router.route('/update').put(isAuthenticatedUser,upload.single('avatar'), updateProfile); 40 | 41 | //Admin routes 42 | router.route('/admin/users').get(isAuthenticatedUser,authorizeRoles('admin'), getAllUsers); 43 | router.route('/admin/user/:id').get(isAuthenticatedUser,authorizeRoles('admin'), getUser) 44 | .put(isAuthenticatedUser,authorizeRoles('admin'), updateUser) 45 | .delete(isAuthenticatedUser,authorizeRoles('admin'), deleteUser); 46 | 47 | 48 | module.exports = router; -------------------------------------------------------------------------------- /frontend/src/slices/productsSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | 4 | const productsSlice = createSlice({ 5 | name: 'products', 6 | initialState: { 7 | loading: false 8 | }, 9 | reducers: { 10 | productsRequest(state, action){ 11 | return { 12 | loading: true 13 | } 14 | }, 15 | productsSuccess(state, action){ 16 | return { 17 | loading: false, 18 | products: action.payload.products, 19 | productsCount: action.payload.count, 20 | resPerPage : action.payload.resPerPage 21 | } 22 | }, 23 | productsFail(state, action){ 24 | return { 25 | loading: false, 26 | error: action.payload 27 | } 28 | }, 29 | adminProductsRequest(state, action){ 30 | return { 31 | loading: true 32 | } 33 | }, 34 | adminProductsSuccess(state, action){ 35 | return { 36 | loading: false, 37 | products: action.payload.products, 38 | } 39 | }, 40 | adminProductsFail(state, action){ 41 | return { 42 | loading: false, 43 | error: action.payload 44 | } 45 | }, 46 | clearError(state, action){ 47 | return { 48 | ...state, 49 | error: null 50 | } 51 | } 52 | } 53 | }); 54 | 55 | const { actions, reducer } = productsSlice; 56 | 57 | export const { 58 | productsRequest, 59 | productsSuccess, 60 | productsFail, 61 | adminProductsFail, 62 | adminProductsRequest, 63 | adminProductsSuccess 64 | 65 | } = actions; 66 | 67 | export default reducer; 68 | 69 | -------------------------------------------------------------------------------- /frontend/src/components/cart/CheckoutStep.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | export default function CheckoutSteps({shipping, confirmOrder, payment}) { 4 | return ( 5 | 6 |
7 | { 8 | shipping ? 9 | 10 |
11 |
Shipping Info
12 |
13 | : 14 | 15 |
16 |
Shipping Info
17 |
18 | 19 | } 20 | 21 | { confirmOrder ? 22 | 23 |
24 |
Confirm Order
25 |
26 | : 27 | 28 |
29 |
Confirm Order
30 |
31 | 32 | } 33 | 34 | 35 | { payment ? 36 | 37 |
38 |
Payment
39 |
40 | : 41 | 42 |
43 |
Payment
44 |
45 | 46 | } 47 | 48 |
49 | ) 50 | } -------------------------------------------------------------------------------- /backend/models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const validator = require('validator'); 3 | const bcrypt = require('bcrypt'); 4 | const jwt = require('jsonwebtoken'); 5 | const crypto = require('crypto') 6 | 7 | const userSchema = new mongoose.Schema({ 8 | name : { 9 | type: String, 10 | required: [true, 'Please enter name'] 11 | }, 12 | email:{ 13 | type: String, 14 | required: [true, 'Please enter email'], 15 | unique: true, 16 | validate: [validator.isEmail, 'Please enter valid email address'] 17 | }, 18 | password: { 19 | type: String, 20 | required: [true, 'Please enter password'], 21 | maxlength: [6, 'Password cannot exceed 6 characters'], 22 | select: false 23 | }, 24 | avatar: { 25 | type: String 26 | }, 27 | role :{ 28 | type: String, 29 | default: 'user' 30 | }, 31 | resetPasswordToken: String, 32 | resetPasswordTokenExpire: Date, 33 | createdAt :{ 34 | type: Date, 35 | default: Date.now 36 | } 37 | }) 38 | 39 | userSchema.pre('save', async function (next){ 40 | if(!this.isModified('password')){ 41 | next(); 42 | } 43 | this.password = await bcrypt.hash(this.password, 10) 44 | }) 45 | 46 | userSchema.methods.getJwtToken = function(){ 47 | return jwt.sign({id: this.id}, process.env.JWT_SECRET, { 48 | expiresIn: process.env.JWT_EXPIRES_TIME 49 | }) 50 | } 51 | 52 | userSchema.methods.isValidPassword = async function(enteredPassword){ 53 | return bcrypt.compare(enteredPassword, this.password) 54 | } 55 | 56 | userSchema.methods.getResetToken = function(){ 57 | //Generate Token 58 | const token = crypto.randomBytes(20).toString('hex'); 59 | 60 | //Generate Hash and set to resetPasswordToken 61 | this.resetPasswordToken = crypto.createHash('sha256').update(token).digest('hex'); 62 | 63 | //Set token expire time 64 | this.resetPasswordTokenExpire = Date.now() + 30 * 60 * 1000; 65 | 66 | return token 67 | } 68 | let model = mongoose.model('User', userSchema); 69 | 70 | 71 | module.exports = model; -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 32 | 37 | 42 | React App 43 | 44 | 45 | 46 |
47 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /frontend/src/components/user/ForgotPassword.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { toast } from "react-toastify"; 4 | import { forgotPassword,clearAuthError } from "../../actions/userActions"; 5 | 6 | export default function ForgotPassword() { 7 | const [email, setEmail] = useState(""); 8 | const dispatch = useDispatch(); 9 | const { error, message } = useSelector(state => state.authState); 10 | 11 | const submitHandler = (e) => { 12 | e.preventDefault(); 13 | const formData = new FormData(); 14 | formData.append('email', email); 15 | dispatch(forgotPassword(formData)) 16 | } 17 | 18 | useEffect(()=>{ 19 | if(message) { 20 | toast(message, { 21 | type: 'success', 22 | position: toast.POSITION.BOTTOM_CENTER 23 | }) 24 | setEmail(""); 25 | return; 26 | } 27 | 28 | if(error) { 29 | toast(error, { 30 | position: toast.POSITION.BOTTOM_CENTER, 31 | type: 'error', 32 | onOpen: ()=> { dispatch(clearAuthError) } 33 | }) 34 | return 35 | } 36 | }, [message, error, dispatch]) 37 | 38 | 39 | return ( 40 |
41 |
42 |
43 |

Forgot Password

44 |
45 | 46 | setEmail(e.target.value)} 52 | /> 53 |
54 | 55 | 61 | 62 |
63 |
64 |
65 | ) 66 | } -------------------------------------------------------------------------------- /frontend/src/actions/orderActions.js: -------------------------------------------------------------------------------- 1 | import {adminOrdersFail, adminOrdersRequest, adminOrdersSuccess, createOrderFail, createOrderRequest, createOrderSuccess, deleteOrderFail, deleteOrderRequest, deleteOrderSuccess, orderDetailFail, orderDetailRequest, orderDetailSuccess, updateOrderFail, updateOrderRequest, updateOrderSuccess, userOrdersFail, userOrdersRequest, userOrdersSuccess } from '../slices/orderSlice'; 2 | import axios from 'axios'; 3 | 4 | export const createOrder = order => async(dispatch) => { 5 | try { 6 | dispatch(createOrderRequest()) 7 | const {data} = await axios.post(`/api/v1/order/new`, order) 8 | dispatch(createOrderSuccess(data)) 9 | } catch (error) { 10 | dispatch(createOrderFail(error.response.data.message)) 11 | } 12 | } 13 | export const userOrders = async(dispatch) => { 14 | try { 15 | dispatch(userOrdersRequest()) 16 | const {data} = await axios.get(`/api/v1/myorders`) 17 | dispatch(userOrdersSuccess(data)) 18 | } catch (error) { 19 | dispatch(userOrdersFail(error.response.data.message)) 20 | } 21 | } 22 | export const orderDetail = id => async(dispatch) => { 23 | try { 24 | dispatch(orderDetailRequest()) 25 | const {data} = await axios.get(`/api/v1/order/${id}`) 26 | dispatch(orderDetailSuccess(data)) 27 | } catch (error) { 28 | dispatch(orderDetailFail(error.response.data.message)) 29 | } 30 | } 31 | 32 | export const adminOrders = async(dispatch) => { 33 | try { 34 | dispatch(adminOrdersRequest()) 35 | const {data} = await axios.get(`/api/v1/admin/orders`) 36 | dispatch(adminOrdersSuccess(data)) 37 | } catch (error) { 38 | dispatch(adminOrdersFail(error.response.data.message)) 39 | } 40 | } 41 | 42 | export const deleteOrder = id => async(dispatch) => { 43 | try { 44 | dispatch(deleteOrderRequest()) 45 | await axios.delete(`/api/v1/admin/order/${id}`) 46 | dispatch(deleteOrderSuccess()) 47 | } catch (error) { 48 | dispatch(deleteOrderFail(error.response.data.message)) 49 | } 50 | } 51 | 52 | export const updateOrder = (id, orderData) => async(dispatch) => { 53 | try { 54 | dispatch(updateOrderRequest()) 55 | const { data} = await axios.put(`/api/v1/admin/order/${id}`, orderData) 56 | dispatch(updateOrderSuccess(data)) 57 | } catch (error) { 58 | dispatch(updateOrderFail(error.response.data.message)) 59 | } 60 | } -------------------------------------------------------------------------------- /backend/models/productModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const productSchema = new mongoose.Schema({ 4 | name : { 5 | type: String, 6 | required: [true, "Please enter product name"], 7 | trim: true, 8 | maxLength: [100, "Product name cannot exceed 100 characters"] 9 | }, 10 | price: { 11 | type: Number, 12 | required: true, 13 | default: 0.0 14 | }, 15 | description: { 16 | type: String, 17 | required: [true, "Please enter product description"] 18 | }, 19 | ratings: { 20 | type: String, 21 | default: 0 22 | }, 23 | images: [ 24 | { 25 | image: { 26 | type: String, 27 | required: true 28 | } 29 | } 30 | ], 31 | category: { 32 | type: String, 33 | required: [true, "Please enter product category"], 34 | enum: { 35 | values: [ 36 | 'Electronics', 37 | 'Mobile Phones', 38 | 'Laptops', 39 | 'Accessories', 40 | 'Headphones', 41 | 'Food', 42 | 'Books', 43 | 'Clothes/Shoes', 44 | 'Beauty/Health', 45 | 'Sports', 46 | 'Outdoor', 47 | 'Home' 48 | ], 49 | message : "Please select correct category" 50 | } 51 | }, 52 | seller: { 53 | type: String, 54 | required: [true, "Please enter product seller"] 55 | }, 56 | stock: { 57 | type: Number, 58 | required: [true, "Please enter product stock"], 59 | maxLength: [20, 'Product stock cannot exceed 20'] 60 | }, 61 | numOfReviews: { 62 | type: Number, 63 | default: 0 64 | }, 65 | reviews: [ 66 | { 67 | user:{ 68 | type:mongoose.Schema.Types.ObjectId, 69 | ref: 'User' 70 | }, 71 | rating: { 72 | type: String, 73 | required: true 74 | }, 75 | comment: { 76 | type: String, 77 | required: true 78 | } 79 | } 80 | ], 81 | user: { 82 | type : mongoose.Schema.Types.ObjectId 83 | } 84 | , 85 | createdAt:{ 86 | type: Date, 87 | default: Date.now() 88 | } 89 | }) 90 | 91 | let schema = mongoose.model('Product', productSchema) 92 | 93 | module.exports = schema -------------------------------------------------------------------------------- /backend/models/orderModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const orderSchema = mongoose.Schema({ 4 | shippingInfo: { 5 | address: { 6 | type: String, 7 | required: true 8 | }, 9 | country: { 10 | type: String, 11 | required: true 12 | }, 13 | city: { 14 | type: String, 15 | required: true 16 | }, 17 | phoneNo: { 18 | type: String, 19 | required: true 20 | }, 21 | postalCode: { 22 | type: String, 23 | required: true 24 | } 25 | }, 26 | user: { 27 | type: mongoose.SchemaTypes.ObjectId, 28 | required: true, 29 | ref: 'User' 30 | }, 31 | orderItems: [{ 32 | name: { 33 | type: String, 34 | required: true 35 | }, 36 | quantity: { 37 | type: Number, 38 | required: true 39 | }, 40 | image: { 41 | type: String, 42 | required: true 43 | }, 44 | price: { 45 | type: Number, 46 | required: true 47 | }, 48 | product: { 49 | type: mongoose.SchemaTypes.ObjectId, 50 | required: true, 51 | ref: 'Product' 52 | } 53 | 54 | }], 55 | itemsPrice: { 56 | type: Number, 57 | required: true, 58 | default: 0.0 59 | }, 60 | taxPrice: { 61 | type: Number, 62 | required: true, 63 | default: 0.0 64 | }, 65 | shippingPrice: { 66 | type: Number, 67 | required: true, 68 | default: 0.0 69 | }, 70 | totalPrice: { 71 | type: Number, 72 | required: true, 73 | default: 0.0 74 | }, 75 | paymentInfo: { 76 | id: { 77 | type: String, 78 | required: true 79 | }, 80 | status: { 81 | type: String, 82 | required: true 83 | } 84 | }, 85 | paidAt: { 86 | type: Date 87 | }, 88 | deliveredAt: { 89 | type: Date 90 | }, 91 | orderStatus: { 92 | type: String, 93 | required: true, 94 | default: 'Processing' 95 | }, 96 | createdAt: { 97 | type: Date, 98 | default: Date.now 99 | } 100 | }) 101 | 102 | let orderModel = mongoose.model('Order', orderSchema); 103 | 104 | module.exports = orderModel; -------------------------------------------------------------------------------- /frontend/src/components/layouts/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import Search from './Search'; 4 | import {useDispatch, useSelector} from 'react-redux'; 5 | import {DropdownButton, Dropdown, Image} from 'react-bootstrap'; 6 | import { logout } from '../../actions/userActions'; 7 | 8 | 9 | export default function Header () { 10 | const { isAuthenticated, user } = useSelector(state => state.authState); 11 | const { items:cartItems } = useSelector(state => state.cartState) 12 | const dispatch = useDispatch(); 13 | const navigate = useNavigate(); 14 | const logoutHandler = () => { 15 | dispatch(logout); 16 | } 17 | 18 | 19 | return ( 20 | 59 | ) 60 | } -------------------------------------------------------------------------------- /frontend/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect, useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { getProducts } from "../actions/productActions"; 4 | import Loader from "./layouts/Loader"; 5 | import MetaData from "./layouts/MetaData"; 6 | import Product from "./product/Product"; 7 | import {toast} from 'react-toastify'; 8 | import Pagination from 'react-js-pagination'; 9 | 10 | export default function Home(){ 11 | const dispatch = useDispatch(); 12 | const {products, loading, error, productsCount, resPerPage} = useSelector((state) => state.productsState) 13 | const [currentPage, setCurrentPage] = useState(1); 14 | 15 | const setCurrentPageNo = (pageNo) =>{ 16 | 17 | setCurrentPage(pageNo) 18 | 19 | } 20 | 21 | useEffect(()=>{ 22 | if(error) { 23 | return toast.error(error,{ 24 | position: toast.POSITION.BOTTOM_CENTER 25 | }) 26 | } 27 | dispatch(getProducts(null, null, null, null, currentPage)) 28 | }, [error, dispatch, currentPage]) 29 | 30 | 31 | return ( 32 | 33 | {loading ? : 34 | 35 | 36 |

Latest Products

37 |
38 |
39 | { products && products.map(product => ( 40 | 41 | ))} 42 | 43 |
44 |
45 | {productsCount > 0 && productsCount > resPerPage? 46 |
47 | 58 |
: null } 59 |
60 | } 61 |
62 | ) 63 | } -------------------------------------------------------------------------------- /frontend/src/components/order/UserOrders.js: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect} from 'react' 2 | import MetaData from '../layouts/MetaData'; 3 | import {MDBDataTable} from 'mdbreact' 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { userOrders as userOrdersAction } from '../../actions/orderActions'; 6 | import { Link } from 'react-router-dom'; 7 | 8 | export default function UserOrders () { 9 | const { userOrders = []} = useSelector(state => state.orderState) 10 | const dispatch = useDispatch(); 11 | 12 | useEffect(() => { 13 | dispatch(userOrdersAction) 14 | },[]) 15 | 16 | const setOrders = () => { 17 | const data = { 18 | columns: [ 19 | { 20 | label: "Order ID", 21 | field: 'id', 22 | sort: "asc" 23 | }, 24 | { 25 | label: "Number of Items", 26 | field: 'numOfItems', 27 | sort: "asc" 28 | }, 29 | { 30 | label: "Amount", 31 | field: 'amount', 32 | sort: "asc" 33 | }, 34 | { 35 | label: "Status", 36 | field: 'status', 37 | sort: "asc" 38 | }, 39 | { 40 | label: "Actions", 41 | field: 'actions', 42 | sort: "asc" 43 | } 44 | ], 45 | rows:[] 46 | } 47 | 48 | userOrders.forEach(userOrder => { 49 | data.rows.push({ 50 | id: userOrder._id, 51 | numOfItems: userOrder.orderItems.length, 52 | amount: `$${userOrder.totalPrice}`, 53 | status: userOrder.orderStatus && userOrder.orderStatus.includes('Delivered') ? 54 | (

{userOrder.orderStatus}

): 55 | (

{userOrder.orderStatus}

), 56 | actions: 57 | 58 | 59 | }) 60 | }) 61 | 62 | 63 | return data; 64 | } 65 | 66 | 67 | return ( 68 | 69 | 70 |

My Orders

71 | 78 |
79 | ) 80 | } -------------------------------------------------------------------------------- /frontend/src/components/user/UpdatePassword.js: -------------------------------------------------------------------------------- 1 | import {useEffect, useState } from 'react'; 2 | import { updatePassword as updatePasswordAction, clearAuthError } from '../../actions/userActions'; 3 | import {useDispatch, useSelector} from 'react-redux'; 4 | import { toast } from 'react-toastify'; 5 | 6 | export default function UpdatePassword() { 7 | 8 | const [password, setPassword] = useState(""); 9 | const [oldPassword, setOldPassword] = useState(""); 10 | const dispatch = useDispatch(); 11 | const { isUpdated, error } = useSelector(state => state.authState) 12 | 13 | const submitHandler = (e) => { 14 | e.preventDefault(); 15 | const formData = new FormData(); 16 | formData.append('oldPassword', oldPassword); 17 | formData.append('password', password); 18 | dispatch(updatePasswordAction(formData)) 19 | } 20 | 21 | useEffect(() => { 22 | if(isUpdated) { 23 | toast('Password updated successfully',{ 24 | type: 'success', 25 | position: toast.POSITION.BOTTOM_CENTER 26 | }) 27 | setOldPassword(""); 28 | setPassword("") 29 | return; 30 | } 31 | if(error) { 32 | toast(error, { 33 | position: toast.POSITION.BOTTOM_CENTER, 34 | type: 'error', 35 | onOpen: ()=> { dispatch(clearAuthError) } 36 | }) 37 | return 38 | } 39 | },[isUpdated, error, dispatch]) 40 | 41 | return ( 42 |
43 |
44 |
45 |

Update Password

46 |
47 | 48 | setOldPassword(e.target.value)} 54 | /> 55 |
56 | 57 |
58 | 59 | setPassword(e.target.value)} 65 | /> 66 |
67 | 68 | 69 |
70 |
71 |
72 | ) 73 | } -------------------------------------------------------------------------------- /frontend/src/components/user/ResetPassword.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState} from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { resetPassword, clearAuthError } from '../../actions/userActions'; 4 | import {useNavigate, useParams} from 'react-router-dom'; 5 | import { toast } from 'react-toastify'; 6 | 7 | export default function ResetPassword() { 8 | const [password, setPassword] = useState(""); 9 | const [confirmPassword, setConfirmPassword] = useState(""); 10 | const dispatch = useDispatch(); 11 | const { isAuthenticated, error } = useSelector(state => state.authState) 12 | const navigate = useNavigate(); 13 | const { token } = useParams(); 14 | 15 | const submitHandler = (e) => { 16 | e.preventDefault(); 17 | const formData = new FormData(); 18 | formData.append('password', password); 19 | formData.append('confirmPassword', confirmPassword); 20 | 21 | dispatch(resetPassword(formData, token)) 22 | } 23 | 24 | useEffect(()=> { 25 | if(isAuthenticated) { 26 | toast('Password Reset Success!', { 27 | type: 'success', 28 | position: toast.POSITION.BOTTOM_CENTER 29 | }) 30 | navigate('/') 31 | return; 32 | } 33 | if(error) { 34 | toast(error, { 35 | position: toast.POSITION.BOTTOM_CENTER, 36 | type: 'error', 37 | onOpen: ()=> { dispatch(clearAuthError) } 38 | }) 39 | return 40 | } 41 | },[isAuthenticated, error, dispatch, navigate]) 42 | 43 | return ( 44 |
45 |
46 |
47 |

New Password

48 | 49 |
50 | 51 | setPassword(e.target.value)} 57 | /> 58 |
59 | 60 |
61 | 62 | setConfirmPassword(e.target.value)} 68 | /> 69 |
70 | 71 | 77 | 78 |
79 |
80 |
81 | ) 82 | } -------------------------------------------------------------------------------- /frontend/src/components/user/Login.js: -------------------------------------------------------------------------------- 1 | import {Fragment, useEffect, useState } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { clearAuthError, login } from '../../actions/userActions'; 4 | import MetaData from '../layouts/MetaData'; 5 | import { toast } from 'react-toastify'; 6 | import { Link, useLocation, useNavigate } from 'react-router-dom'; 7 | export default function Login() { 8 | const [email, setEmail] = useState("") 9 | const [password, setPassword] = useState("") 10 | const dispatch = useDispatch(); 11 | const navigate = useNavigate(); 12 | const location = useLocation(); 13 | 14 | const { loading, error, isAuthenticated } = useSelector(state => state.authState) 15 | const redirect = location.search?'/'+location.search.split('=')[1]:'/'; 16 | 17 | const submitHandler = (e) => { 18 | e.preventDefault(); 19 | dispatch(login(email, password)) 20 | } 21 | 22 | useEffect(() => { 23 | if(isAuthenticated) { 24 | navigate(redirect) 25 | } 26 | 27 | if(error) { 28 | toast(error, { 29 | position: toast.POSITION.BOTTOM_CENTER, 30 | type: 'error', 31 | onOpen: ()=> { dispatch(clearAuthError) } 32 | }) 33 | return 34 | } 35 | },[error, isAuthenticated, dispatch, navigate]) 36 | 37 | return ( 38 | 39 | 40 |
41 |
42 |
43 |

Login

44 |
45 | 46 | setEmail(e.target.value)} 52 | /> 53 |
54 | 55 |
56 | 57 | setPassword(e.target.value)} 63 | /> 64 |
65 | 66 | Forgot Password? 67 | 68 | 76 | 77 | New User? 78 |
79 |
80 |
81 |
82 | ) 83 | } -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | 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. 37 | 38 | 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. 39 | 40 | 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. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /frontend/src/slices/cartSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | 4 | 5 | const cartSlice = createSlice({ 6 | name: 'cart', 7 | initialState: { 8 | items: localStorage.getItem('cartItems')? JSON.parse(localStorage.getItem('cartItems')): [], 9 | loading: false, 10 | shippingInfo: localStorage.getItem('shippingInfo')? JSON.parse(localStorage.getItem('shippingInfo')): {} 11 | }, 12 | reducers: { 13 | addCartItemRequest(state, action){ 14 | return { 15 | ...state, 16 | loading: true 17 | } 18 | }, 19 | addCartItemSuccess(state, action){ 20 | const item = action.payload 21 | 22 | const isItemExist = state.items.find( i => i.product == item.product); 23 | 24 | if(isItemExist) { 25 | state = { 26 | ...state, 27 | loading: false, 28 | } 29 | }else{ 30 | state = { 31 | items: [...state.items, item], 32 | loading: false 33 | } 34 | 35 | localStorage.setItem('cartItems', JSON.stringify(state.items)); 36 | } 37 | return state 38 | 39 | }, 40 | increaseCartItemQty(state, action) { 41 | state.items = state.items.map(item => { 42 | if(item.product == action.payload) { 43 | item.quantity = item.quantity + 1 44 | } 45 | return item; 46 | }) 47 | localStorage.setItem('cartItems', JSON.stringify(state.items)); 48 | 49 | }, 50 | decreaseCartItemQty(state, action) { 51 | state.items = state.items.map(item => { 52 | if(item.product == action.payload) { 53 | item.quantity = item.quantity - 1 54 | } 55 | return item; 56 | }) 57 | localStorage.setItem('cartItems', JSON.stringify(state.items)); 58 | 59 | }, 60 | removeItemFromCart(state, action) { 61 | const filterItems = state.items.filter(item => { 62 | return item.product !== action.payload 63 | }) 64 | localStorage.setItem('cartItems', JSON.stringify(filterItems)); 65 | return { 66 | ...state, 67 | items: filterItems 68 | } 69 | }, 70 | saveShippingInfo(state, action) { 71 | localStorage.setItem('shippingInfo', JSON.stringify(action.payload)); 72 | return { 73 | ...state, 74 | shippingInfo: action.payload 75 | } 76 | }, 77 | orderCompleted(state, action) { 78 | localStorage.removeItem('shippingInfo'); 79 | localStorage.removeItem('cartItems'); 80 | sessionStorage.removeItem('orderInfo'); 81 | return { 82 | items: [], 83 | loading: false, 84 | shippingInfo: {} 85 | } 86 | } 87 | 88 | } 89 | }); 90 | 91 | const { actions, reducer } = cartSlice; 92 | 93 | export const { 94 | addCartItemRequest, 95 | addCartItemSuccess, 96 | decreaseCartItemQty, 97 | increaseCartItemQty, 98 | removeItemFromCart, 99 | saveShippingInfo, 100 | orderCompleted 101 | } = actions; 102 | 103 | export default reducer; 104 | 105 | -------------------------------------------------------------------------------- /backend/controllers/orderController.js: -------------------------------------------------------------------------------- 1 | const catchAsyncError = require('../middlewares/catchAsyncError'); 2 | const Order = require('../models/orderModel'); 3 | const Product = require('../models/productModel'); 4 | const ErrorHandler = require('../utils/errorHandler'); 5 | //Create New Order - api/v1/order/new 6 | exports.newOrder = catchAsyncError( async (req, res, next) => { 7 | const { 8 | orderItems, 9 | shippingInfo, 10 | itemsPrice, 11 | taxPrice, 12 | shippingPrice, 13 | totalPrice, 14 | paymentInfo 15 | } = req.body; 16 | 17 | const order = await Order.create({ 18 | orderItems, 19 | shippingInfo, 20 | itemsPrice, 21 | taxPrice, 22 | shippingPrice, 23 | totalPrice, 24 | paymentInfo, 25 | paidAt: Date.now(), 26 | user: req.user.id 27 | }) 28 | 29 | res.status(200).json({ 30 | success: true, 31 | order 32 | }) 33 | }) 34 | 35 | //Get Single Order - api/v1/order/:id 36 | exports.getSingleOrder = catchAsyncError(async (req, res, next) => { 37 | const order = await Order.findById(req.params.id).populate('user', 'name email'); 38 | if(!order) { 39 | return next(new ErrorHandler(`Order not found with this id: ${req.params.id}`, 404)) 40 | } 41 | 42 | res.status(200).json({ 43 | success: true, 44 | order 45 | }) 46 | }) 47 | 48 | //Get Loggedin User Orders - /api/v1/myorders 49 | exports.myOrders = catchAsyncError(async (req, res, next) => { 50 | const orders = await Order.find({user: req.user.id}); 51 | 52 | res.status(200).json({ 53 | success: true, 54 | orders 55 | }) 56 | }) 57 | 58 | //Admin: Get All Orders - api/v1/orders 59 | exports.orders = catchAsyncError(async (req, res, next) => { 60 | const orders = await Order.find(); 61 | 62 | let totalAmount = 0; 63 | 64 | orders.forEach(order => { 65 | totalAmount += order.totalPrice 66 | }) 67 | 68 | res.status(200).json({ 69 | success: true, 70 | totalAmount, 71 | orders 72 | }) 73 | }) 74 | 75 | //Admin: Update Order / Order Status - api/v1/order/:id 76 | exports.updateOrder = catchAsyncError(async (req, res, next) => { 77 | const order = await Order.findById(req.params.id); 78 | 79 | if(order.orderStatus == 'Delivered') { 80 | return next(new ErrorHandler('Order has been already delivered!', 400)) 81 | } 82 | //Updating the product stock of each order item 83 | order.orderItems.forEach(async orderItem => { 84 | await updateStock(orderItem.product, orderItem.quantity) 85 | }) 86 | 87 | order.orderStatus = req.body.orderStatus; 88 | order.deliveredAt = Date.now(); 89 | await order.save(); 90 | 91 | res.status(200).json({ 92 | success: true 93 | }) 94 | 95 | }); 96 | 97 | async function updateStock (productId, quantity){ 98 | const product = await Product.findById(productId); 99 | product.stock = product.stock - quantity; 100 | product.save({validateBeforeSave: false}) 101 | } 102 | 103 | //Admin: Delete Order - api/v1/order/:id 104 | exports.deleteOrder = catchAsyncError(async (req, res, next) => { 105 | const order = await Order.findById(req.params.id); 106 | if(!order) { 107 | return next(new ErrorHandler(`Order not found with this id: ${req.params.id}`, 404)) 108 | } 109 | 110 | await order.remove(); 111 | res.status(200).json({ 112 | success: true 113 | }) 114 | }) 115 | 116 | -------------------------------------------------------------------------------- /frontend/src/components/order/OrderDetail.js: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { Link, useParams } from 'react-router-dom'; 4 | import Loader from '../layouts/Loader'; 5 | import {orderDetail as orderDetailAction } from '../../actions/orderActions'; 6 | export default function OrderDetail () { 7 | const { orderDetail, loading } = useSelector(state => state.orderState) 8 | const { shippingInfo={}, user={}, orderStatus="Processing", orderItems=[], totalPrice=0, paymentInfo={} } = orderDetail; 9 | const isPaid = paymentInfo && paymentInfo.status === "succeeded" ? true: false; 10 | const dispatch = useDispatch(); 11 | const {id } = useParams(); 12 | 13 | useEffect(() => { 14 | dispatch(orderDetailAction(id)) 15 | },[id]) 16 | 17 | return ( 18 | 19 | { loading ? : 20 | 21 |
22 |
23 | 24 |

Order # {orderDetail._id}

25 | 26 |

Shipping Info

27 |

Name: {user.name}

28 |

Phone: {shippingInfo.phoneNo}

29 |

Address:{shippingInfo.address}, {shippingInfo.city}, {shippingInfo.postalCode}, {shippingInfo.state}, {shippingInfo.country}

30 |

Amount: ${totalPrice}

31 | 32 |
33 | 34 |

Payment

35 |

{isPaid ? 'PAID' : 'NOT PAID' }

36 | 37 | 38 |

Order Status:

39 |

{orderStatus}

40 | 41 | 42 |

Order Items:

43 | 44 |
45 |
46 | {orderItems && orderItems.map(item => ( 47 |
48 |
49 | {item.name} 50 |
51 | 52 |
53 | {item.name} 54 |
55 | 56 | 57 |
58 |

${item.price}

59 |
60 | 61 |
62 |

{item.quantity} Piece(s)

63 |
64 |
65 | ))} 66 | 67 |
68 |
69 |
70 |
71 |
72 | } 73 |
74 | ) 75 | } -------------------------------------------------------------------------------- /frontend/src/slices/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | 4 | const userSlice = createSlice({ 5 | name: 'user', 6 | initialState: { 7 | loading: false, 8 | user: {}, 9 | users: [], 10 | isUserUpdated: false, 11 | isUserDeleted: false 12 | }, 13 | reducers: { 14 | usersRequest(state, action){ 15 | return { 16 | ...state, 17 | loading: true 18 | } 19 | }, 20 | usersSuccess(state, action){ 21 | return { 22 | ...state, 23 | loading: false, 24 | users: action.payload.users, 25 | } 26 | }, 27 | usersFail(state, action){ 28 | return { 29 | ...state, 30 | loading: false, 31 | error: action.payload 32 | } 33 | }, 34 | userRequest(state, action){ 35 | return { 36 | ...state, 37 | loading: true 38 | } 39 | }, 40 | userSuccess(state, action){ 41 | return { 42 | ...state, 43 | loading: false, 44 | user: action.payload.user, 45 | } 46 | }, 47 | userFail(state, action){ 48 | return { 49 | ...state, 50 | loading: false, 51 | error: action.payload 52 | } 53 | }, 54 | 55 | deleteUserRequest(state, action){ 56 | return { 57 | ...state, 58 | loading: true 59 | } 60 | }, 61 | deleteUserSuccess(state, action){ 62 | return { 63 | ...state, 64 | loading: false, 65 | isUserDeleted : true 66 | } 67 | }, 68 | deleteUserFail(state, action){ 69 | return { 70 | ...state, 71 | loading: false, 72 | error: action.payload 73 | } 74 | }, 75 | updateUserRequest(state, action){ 76 | return { 77 | ...state, 78 | loading: true 79 | } 80 | }, 81 | updateUserSuccess(state, action){ 82 | return { 83 | ...state, 84 | loading: false, 85 | isUserUpdated : true 86 | } 87 | }, 88 | updateUserFail(state, action){ 89 | return { 90 | ...state, 91 | loading: false, 92 | error: action.payload 93 | } 94 | }, 95 | clearUserDeleted(state, action){ 96 | return { 97 | ...state, 98 | isUserDeleted : false 99 | } 100 | }, 101 | clearUserUpdated(state, action){ 102 | return { 103 | ...state, 104 | isUserUpdated : false 105 | } 106 | }, 107 | clearError(state, action){ 108 | return { 109 | ...state, 110 | error: null 111 | } 112 | } 113 | 114 | } 115 | }); 116 | 117 | const { actions, reducer } = userSlice; 118 | 119 | export const { 120 | usersRequest, 121 | usersSuccess, 122 | usersFail, 123 | userRequest, 124 | userSuccess, 125 | userFail, 126 | deleteUserRequest, 127 | deleteUserFail, 128 | deleteUserSuccess, 129 | updateUserRequest, 130 | updateUserSuccess, 131 | updateUserFail, 132 | clearUserDeleted, 133 | clearUserUpdated, 134 | clearError 135 | 136 | } = actions; 137 | 138 | export default reducer; 139 | 140 | -------------------------------------------------------------------------------- /frontend/src/components/admin/UserList.js: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect } from "react" 2 | import { Button } from "react-bootstrap" 3 | import { useDispatch, useSelector } from "react-redux" 4 | import { Link } from "react-router-dom" 5 | import { deleteUser, getUsers } from "../../actions/userActions" 6 | import { clearError, clearUserDeleted } from "../../slices/userSlice" 7 | import Loader from '../layouts/Loader'; 8 | import { MDBDataTable} from 'mdbreact'; 9 | import {toast } from 'react-toastify' 10 | import Sidebar from "./Sidebar" 11 | 12 | export default function UserList() { 13 | const { users = [], loading = true, error, isUserDeleted } = useSelector(state => state.userState) 14 | 15 | const dispatch = useDispatch(); 16 | 17 | const setUsers = () => { 18 | const data = { 19 | columns : [ 20 | { 21 | label: 'ID', 22 | field: 'id', 23 | sort: 'asc' 24 | }, 25 | { 26 | label: 'Name', 27 | field: 'name', 28 | sort: 'asc' 29 | }, 30 | { 31 | label: 'Email', 32 | field: 'email', 33 | sort: 'asc' 34 | }, 35 | { 36 | label: 'Role', 37 | field: 'role', 38 | sort: 'asc' 39 | }, 40 | { 41 | label: 'Actions', 42 | field: 'actions', 43 | sort: 'asc' 44 | } 45 | ], 46 | rows : [] 47 | } 48 | 49 | users.forEach( user => { 50 | data.rows.push({ 51 | id: user._id, 52 | name: user.name, 53 | email : user.email, 54 | role: user.role , 55 | actions: ( 56 | 57 | 58 | 61 | 62 | ) 63 | }) 64 | }) 65 | 66 | return data; 67 | } 68 | 69 | const deleteHandler = (e, id) => { 70 | e.target.disabled = true; 71 | dispatch(deleteUser(id)) 72 | } 73 | 74 | useEffect(() => { 75 | if(error) { 76 | toast(error, { 77 | position: toast.POSITION.BOTTOM_CENTER, 78 | type: 'error', 79 | onOpen: ()=> { dispatch(clearError()) } 80 | }) 81 | return 82 | } 83 | if(isUserDeleted) { 84 | toast('User Deleted Succesfully!',{ 85 | type: 'success', 86 | position: toast.POSITION.BOTTOM_CENTER, 87 | onOpen: () => dispatch(clearUserDeleted()) 88 | }) 89 | return; 90 | } 91 | 92 | dispatch(getUsers) 93 | },[dispatch, error, isUserDeleted]) 94 | 95 | 96 | return ( 97 |
98 |
99 | 100 |
101 |
102 |

User List

103 | 104 | {loading ? : 105 | 112 | } 113 | 114 |
115 |
116 | ) 117 | } -------------------------------------------------------------------------------- /backend/data/products.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "OPPO F21s Pro 5G", 4 | "price": 245.67, 5 | "description": "OPPO F21s Pro 5G is a powerful device with a RAM extension feature, that offers brilliant operational speed to users.", 6 | "ratings": 4.5, 7 | "images": [ 8 | { 9 | "image": "/images/products/1.jpg" 10 | }, 11 | { 12 | "image": "/images/products/2.jpg" 13 | } 14 | ], 15 | "category": "Mobile Phones", 16 | "seller": "Amazon", 17 | "stock": 5, 18 | "numOfReviews": 15, 19 | "reviews": [] 20 | }, 21 | { 22 | "name": "WRISTIO HD, Bluetooth Calling Smart Watch", 23 | "price": 150.32, 24 | "description": "Minix watches are exclusively designed to fulfill the advanced tech needs of today’s generation.", 25 | "ratings": 3.5, 26 | "images": [ 27 | { 28 | "image": "/images/products/2.jpg" 29 | } 30 | ], 31 | "category": "Accessories", 32 | "seller": "Flipkart", 33 | "stock": 9, 34 | "numOfReviews": 5, 35 | "reviews": [] 36 | }, 37 | { 38 | "name": "Dell Inspiron 3511 Laptop", 39 | "price": 440.57, 40 | "description": "Dell Inspiron 3511 11th Generation Intel Core i5-1135G7 Processor (8MB Cache, up to 4.2 GHz);Operating System: Windows 10 Home Single Language, English", 41 | "ratings": 2, 42 | "images": [ 43 | { 44 | "image": "/images/products/3.jpg" 45 | } 46 | ], 47 | "category": "Laptops", 48 | "seller": "Ebay", 49 | "stock": 9, 50 | "numOfReviews": 12, 51 | "reviews": [] 52 | }, 53 | { 54 | "name": "Lenovo IdeaPad Slim 3 Laptop", 55 | "price": 250.45, 56 | "description": "Lenovo IdeaPad Slim 311th Gen Intel Core i5-1135G7 | Speed: 2.4 GHz (Base) - 4.2 GHz (Max) | 4 Cores | 8 Threads | 8 MB Cache", 57 | "ratings": 4, 58 | "images": [ 59 | { 60 | "image": "/images/products/6.jpg" 61 | } 62 | ], 63 | "category": "Laptops", 64 | "seller": "Ebay", 65 | "stock": 9, 66 | "numOfReviews": 12, 67 | "reviews": [] 68 | }, 69 | { 70 | "name": "ASUS VivoBook 15 Laptop", 71 | "price": 767.32, 72 | "description": "ASUS VivoBook 15 15.6-inch (39.62 cm) HD, Dual Core Intel Celeron N4020, Thin and Light Laptop (4GB RAM/256GB SSD/Integrated Graphics/Windows 11 Home/Transparent Silver/1.8 Kg), X515MA-BR011W", 73 | "ratings": 5, 74 | "images": [ 75 | { 76 | "image": "/images/products/7.jpg" 77 | } 78 | ], 79 | "category": "Laptops", 80 | "seller": "Ebay", 81 | "stock": 9, 82 | "numOfReviews": 12, 83 | "reviews": [] 84 | }, 85 | { 86 | "name": "PTron Newly Launched Tangent Sports, 60Hrs Playtime", 87 | "price": 15.46, 88 | "description": "Gigantic 60 + Hours of music playtime on a single charge; BT5.2 Wireless headphones with ENC (Environmental Noise Cancellation) Technology to enhance your voice quality over the voice calls", 89 | "ratings": 5, 90 | "images": [ 91 | { 92 | "image": "/images/products/4.jpg" 93 | } 94 | ], 95 | "category": "Headphones", 96 | "seller": "Amazon", 97 | "stock": 4, 98 | "numOfReviews": 20, 99 | "reviews": [] 100 | }, 101 | { 102 | "name": "Campus Men's Maxico Running Shoes", 103 | "price": 10.12, 104 | "description": "The high raised back cover with extra padding.", 105 | "ratings": 3, 106 | "images": [ 107 | { 108 | "image": "/images/products/5.jpg" 109 | } 110 | ], 111 | "category": "Sports", 112 | "seller": "Ebay", 113 | "stock": 6, 114 | "numOfReviews": 9, 115 | "reviews": [] 116 | } 117 | ] -------------------------------------------------------------------------------- /frontend/src/components/admin/ProductList.js: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect } from "react" 2 | import { Button } from "react-bootstrap" 3 | import { useDispatch, useSelector } from "react-redux" 4 | import { Link } from "react-router-dom" 5 | import { deleteProduct, getAdminProducts } from "../../actions/productActions" 6 | import { clearError, clearProductDeleted } from "../../slices/productSlice" 7 | import Loader from '../layouts/Loader'; 8 | import { MDBDataTable} from 'mdbreact'; 9 | import {toast } from 'react-toastify' 10 | import Sidebar from "./Sidebar" 11 | 12 | export default function ProductList() { 13 | const { products = [], loading = true, error } = useSelector(state => state.productsState) 14 | const { isProductDeleted, error:productError } = useSelector(state => state.productState) 15 | const dispatch = useDispatch(); 16 | 17 | const setProducts = () => { 18 | const data = { 19 | columns : [ 20 | { 21 | label: 'ID', 22 | field: 'id', 23 | sort: 'asc' 24 | }, 25 | { 26 | label: 'Name', 27 | field: 'name', 28 | sort: 'asc' 29 | }, 30 | { 31 | label: 'Price', 32 | field: 'price', 33 | sort: 'asc' 34 | }, 35 | { 36 | label: 'Stock', 37 | field: 'stock', 38 | sort: 'asc' 39 | }, 40 | { 41 | label: 'Actions', 42 | field: 'actions', 43 | sort: 'asc' 44 | } 45 | ], 46 | rows : [] 47 | } 48 | 49 | products.forEach( product => { 50 | data.rows.push({ 51 | id: product._id, 52 | name: product.name, 53 | price : `$${product.price}`, 54 | stock: product.stock, 55 | actions: ( 56 | 57 | 58 | 61 | 62 | ) 63 | }) 64 | }) 65 | 66 | return data; 67 | } 68 | 69 | const deleteHandler = (e, id) => { 70 | e.target.disabled = true; 71 | dispatch(deleteProduct(id)) 72 | } 73 | 74 | useEffect(() => { 75 | if(error || productError) { 76 | toast(error || productError, { 77 | position: toast.POSITION.BOTTOM_CENTER, 78 | type: 'error', 79 | onOpen: ()=> { dispatch(clearError()) } 80 | }) 81 | return 82 | } 83 | if(isProductDeleted) { 84 | toast('Product Deleted Succesfully!',{ 85 | type: 'success', 86 | position: toast.POSITION.BOTTOM_CENTER, 87 | onOpen: () => dispatch(clearProductDeleted()) 88 | }) 89 | return; 90 | } 91 | 92 | dispatch(getAdminProducts) 93 | },[dispatch, error, isProductDeleted]) 94 | 95 | 96 | return ( 97 |
98 |
99 | 100 |
101 |
102 |

Product List

103 | 104 | {loading ? : 105 | 112 | } 113 | 114 |
115 |
116 | ) 117 | } -------------------------------------------------------------------------------- /frontend/src/components/admin/OrderList.js: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect } from "react" 2 | import { Button } from "react-bootstrap" 3 | import { useDispatch, useSelector } from "react-redux" 4 | import { Link } from "react-router-dom" 5 | import { deleteOrder, adminOrders as adminOrdersAction } from "../../actions/orderActions" 6 | import { clearError, clearOrderDeleted } from "../../slices/orderSlice" 7 | import Loader from '../layouts/Loader'; 8 | import { MDBDataTable} from 'mdbreact'; 9 | import {toast } from 'react-toastify' 10 | import Sidebar from "./Sidebar" 11 | 12 | export default function OrderList() { 13 | const { adminOrders = [], loading = true, error, isOrderDeleted } = useSelector(state => state.orderState) 14 | 15 | const dispatch = useDispatch(); 16 | 17 | const setOrders = () => { 18 | const data = { 19 | columns : [ 20 | { 21 | label: 'ID', 22 | field: 'id', 23 | sort: 'asc' 24 | }, 25 | { 26 | label: 'Number of Items', 27 | field: 'noOfItems', 28 | sort: 'asc' 29 | }, 30 | { 31 | label: 'Amount', 32 | field: 'amount', 33 | sort: 'asc' 34 | }, 35 | { 36 | label: 'Status', 37 | field: 'status', 38 | sort: 'asc' 39 | }, 40 | { 41 | label: 'Actions', 42 | field: 'actions', 43 | sort: 'asc' 44 | } 45 | ], 46 | rows : [] 47 | } 48 | 49 | adminOrders.forEach( order => { 50 | data.rows.push({ 51 | id: order._id, 52 | noOfItems: order.orderItems.length, 53 | amount : `$${order.totalPrice}`, 54 | status:

{order.orderStatus}

, 55 | actions: ( 56 | 57 | 58 | 61 | 62 | ) 63 | }) 64 | }) 65 | 66 | return data; 67 | } 68 | 69 | const deleteHandler = (e, id) => { 70 | e.target.disabled = true; 71 | dispatch(deleteOrder(id)) 72 | } 73 | 74 | useEffect(() => { 75 | if(error) { 76 | toast(error, { 77 | position: toast.POSITION.BOTTOM_CENTER, 78 | type: 'error', 79 | onOpen: ()=> { dispatch(clearError()) } 80 | }) 81 | return 82 | } 83 | if(isOrderDeleted) { 84 | toast('Order Deleted Succesfully!',{ 85 | type: 'success', 86 | position: toast.POSITION.BOTTOM_CENTER, 87 | onOpen: () => dispatch(clearOrderDeleted()) 88 | }) 89 | return; 90 | } 91 | 92 | dispatch(adminOrdersAction) 93 | },[dispatch, error, isOrderDeleted]) 94 | 95 | 96 | return ( 97 |
98 |
99 | 100 |
101 |
102 |

Order List

103 | 104 | {loading ? : 105 | 112 | } 113 | 114 |
115 |
116 | ) 117 | } -------------------------------------------------------------------------------- /frontend/src/components/cart/ConfirmOrder.js: -------------------------------------------------------------------------------- 1 | import MetaData from '../layouts/MetaData'; 2 | import { Fragment, useEffect } from 'react'; 3 | import { validateShipping } from './Shipping'; 4 | import { useSelector } from 'react-redux'; 5 | import { Link, useNavigate } from 'react-router-dom'; 6 | import CheckoutSteps from './CheckoutStep'; 7 | 8 | export default function ConfirmOrder () { 9 | const { shippingInfo, items:cartItems } = useSelector(state => state.cartState); 10 | const { user } = useSelector(state => state.authState); 11 | const navigate = useNavigate(); 12 | const itemsPrice = cartItems.reduce((acc, item)=> (acc + item.price * item.quantity),0); 13 | const shippingPrice = itemsPrice > 200 ? 0 : 25; 14 | let taxPrice = Number(0.05 * itemsPrice); 15 | const totalPrice = Number(itemsPrice + shippingPrice + taxPrice).toFixed(2); 16 | taxPrice = Number(taxPrice).toFixed(2) 17 | 18 | const processPayment = () => { 19 | const data = { 20 | itemsPrice, 21 | shippingPrice, 22 | taxPrice, 23 | totalPrice 24 | } 25 | sessionStorage.setItem('orderInfo', JSON.stringify(data)) 26 | navigate('/payment') 27 | } 28 | 29 | 30 | useEffect(()=>{ 31 | validateShipping(shippingInfo, navigate) 32 | },[]) 33 | 34 | return ( 35 | 36 | 37 | 38 |
39 |
40 | 41 |

Shipping Info

42 |

Name: {user.name}

43 |

Phone: {shippingInfo.phoneNo}

44 |

Address: {shippingInfo.address}, {shippingInfo.city}, {shippingInfo.postalCode}, {shippingInfo.state}, {shippingInfo.country}

45 | 46 |
47 |

Your Cart Items:

48 | 49 | {cartItems.map(item => ( 50 | 51 |
52 |
53 |
54 | {item.name} 55 |
56 | 57 |
58 | {item.name} 59 |
60 | 61 | 62 |
63 |

{item.quantity} x ${item.price} = ${item.quantity * item.price}

64 |
65 | 66 |
67 |
68 |
69 |
70 | ) 71 | 72 | ) 73 | 74 | } 75 | 76 | 77 | 78 | 79 |
80 | 81 |
82 |
83 |

Order Summary

84 |
85 |

Subtotal: ${itemsPrice}

86 |

Shipping: ${shippingPrice}

87 |

Tax: ${taxPrice}

88 | 89 |
90 | 91 |

Total: ${totalPrice}

92 | 93 |
94 | 95 |
96 |
97 |
98 |
99 | 100 | ) 101 | } -------------------------------------------------------------------------------- /frontend/src/components/cart/Cart.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react' 2 | import {useDispatch, useSelector} from 'react-redux'; 3 | import { Link, useNavigate } from 'react-router-dom'; 4 | import { decreaseCartItemQty, increaseCartItemQty,removeItemFromCart } from '../../slices/cartSlice'; 5 | 6 | export default function Cart() { 7 | const {items } = useSelector(state => state.cartState) 8 | const dispatch = useDispatch(); 9 | const navigate = useNavigate(); 10 | 11 | const increaseQty = (item) => { 12 | const count = item.quantity; 13 | if(item.stock ==0 || count >= item.stock) return; 14 | dispatch(increaseCartItemQty(item.product)) 15 | } 16 | const decreaseQty = (item) => { 17 | const count = item.quantity; 18 | if(count == 1) return; 19 | dispatch(decreaseCartItemQty(item.product)) 20 | } 21 | 22 | const checkoutHandler = () =>{ 23 | navigate('/login?redirect=shipping') 24 | } 25 | 26 | 27 | return ( 28 | 29 | {items.length==0 ? 30 |

Your Cart is Empty

: 31 | 32 |

Your Cart: {items.length} items

33 |
34 |
35 | {items.map(item => ( 36 | 37 |
38 |
39 |
40 |
41 | {item.name} 42 |
43 | 44 |
45 | {item.name} 46 |
47 | 48 | 49 |
50 |

${item.price}

51 |
52 | 53 |
54 |
55 | decreaseQty(item)}>- 56 | 57 | 58 | increaseQty(item)}>+ 59 |
60 |
61 | 62 |
63 | dispatch(removeItemFromCart(item.product))} className="fa fa-trash btn btn-danger"> 64 |
65 | 66 |
67 |
68 |
69 | ) 70 | ) 71 | } 72 | 73 | 74 |
75 |
76 | 77 |
78 |
79 |

Order Summary

80 |
81 |

Subtotal: {items.reduce((acc, item)=>(acc + item.quantity), 0)} (Units)

82 |

Est. total: ${items.reduce((acc, item)=>(acc + item.quantity * item.price), 0)}

83 | 84 |
85 | 86 |
87 |
88 |
89 |
90 | } 91 | 92 |
93 | ) 94 | } -------------------------------------------------------------------------------- /frontend/src/components/admin/UpdateUser.js: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect, useState } from "react"; 2 | import Sidebar from "./Sidebar"; 3 | import { useDispatch, useSelector} from 'react-redux'; 4 | import { useNavigate, useParams } from "react-router-dom"; 5 | import { getUser, updateUser } from "../../actions/userActions"; 6 | import { clearError, clearUserUpdated } from "../../slices/userSlice"; 7 | import { toast } from "react-toastify"; 8 | 9 | export default function UpdateUser () { 10 | const [name, setName] = useState(""); 11 | const [email, setEmail] = useState(""); 12 | const [role, setRole] = useState(""); 13 | 14 | const { id:userId } = useParams(); 15 | 16 | const { loading, isUserUpdated, error, user } = useSelector( state => state.userState) 17 | const { user:authUser } = useSelector( state => state.authState) 18 | 19 | const dispatch = useDispatch(); 20 | 21 | const submitHandler = (e) => { 22 | e.preventDefault(); 23 | const formData = new FormData(); 24 | formData.append('name' , name); 25 | formData.append('email' , email); 26 | formData.append('role' , role); 27 | dispatch(updateUser(userId, formData)) 28 | } 29 | 30 | useEffect(() => { 31 | if(isUserUpdated) { 32 | toast('User Updated Succesfully!',{ 33 | type: 'success', 34 | position: toast.POSITION.BOTTOM_CENTER, 35 | onOpen: () => dispatch(clearUserUpdated()) 36 | }) 37 | return; 38 | } 39 | 40 | if(error) { 41 | toast(error, { 42 | position: toast.POSITION.BOTTOM_CENTER, 43 | type: 'error', 44 | onOpen: ()=> { dispatch(clearError()) } 45 | }) 46 | return 47 | } 48 | 49 | dispatch(getUser(userId)) 50 | }, [isUserUpdated, error, dispatch]) 51 | 52 | 53 | useEffect(() => { 54 | if(user._id) { 55 | setName(user.name); 56 | setEmail(user.email); 57 | setRole(user.role); 58 | } 59 | },[user]) 60 | 61 | 62 | return ( 63 |
64 |
65 | 66 |
67 |
68 | 69 |
70 |
71 |

Update User

72 | 73 |
74 | 75 | setName(e.target.value)} 80 | value={name} 81 | /> 82 |
83 | 84 |
85 | 86 | setEmail(e.target.value)} 91 | value={email} 92 | /> 93 |
94 |
95 | 96 | 100 |
101 | 109 | 110 |
111 |
112 |
113 |
114 |
115 | 116 | ) 117 | } -------------------------------------------------------------------------------- /frontend/src/actions/productActions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { productsFail, productsSuccess, productsRequest, adminProductsRequest, adminProductsSuccess, adminProductsFail } from '../slices/productsSlice'; 3 | import { productFail, productSuccess, productRequest, createReviewRequest, createReviewSuccess, createReviewFail, newProductRequest, newProductSuccess, newProductFail, deleteProductRequest, deleteProductSuccess, deleteProductFail, updateProductRequest, updateProductSuccess, updateProductFail, reviewsRequest, reviewsSuccess, reviewsFail, deleteReviewRequest, deleteReviewSuccess, deleteReviewFail } from '../slices/productSlice'; 4 | 5 | export const getProducts = (keyword, price, category, rating, currentPage) => async (dispatch) => { 6 | 7 | try { 8 | dispatch(productsRequest()) 9 | let link = `/api/v1/products?page=${currentPage}`; 10 | 11 | if(keyword) { 12 | link += `&keyword=${keyword}` 13 | } 14 | if(price) { 15 | link += `&price[gte]=${price[0]}&price[lte]=${price[1]}` 16 | } 17 | if(category) { 18 | link += `&category=${category}` 19 | } 20 | if(rating) { 21 | link += `&ratings=${rating}` 22 | } 23 | 24 | const { data } = await axios.get(link); 25 | dispatch(productsSuccess(data)) 26 | } catch (error) { 27 | //handle error 28 | dispatch(productsFail(error.response.data.message)) 29 | } 30 | 31 | } 32 | 33 | 34 | export const getProduct = id => async (dispatch) => { 35 | 36 | try { 37 | dispatch(productRequest()) 38 | const { data } = await axios.get(`/api/v1/product/${id}`); 39 | dispatch(productSuccess(data)) 40 | } catch (error) { 41 | //handle error 42 | dispatch(productFail(error.response.data.message)) 43 | } 44 | 45 | } 46 | 47 | export const createReview = reviewData => async (dispatch) => { 48 | 49 | try { 50 | dispatch(createReviewRequest()) 51 | const config = { 52 | headers : { 53 | 'Content-type': 'application/json' 54 | } 55 | } 56 | const { data } = await axios.put(`/api/v1/review`,reviewData, config); 57 | dispatch(createReviewSuccess(data)) 58 | } catch (error) { 59 | //handle error 60 | dispatch(createReviewFail(error.response.data.message)) 61 | } 62 | 63 | } 64 | 65 | export const getAdminProducts = async (dispatch) => { 66 | 67 | try { 68 | dispatch(adminProductsRequest()) 69 | const { data } = await axios.get(`/api/v1/admin/products`); 70 | dispatch(adminProductsSuccess(data)) 71 | } catch (error) { 72 | //handle error 73 | dispatch(adminProductsFail(error.response.data.message)) 74 | } 75 | 76 | } 77 | 78 | export const createNewProduct = productData => async (dispatch) => { 79 | 80 | try { 81 | dispatch(newProductRequest()) 82 | const { data } = await axios.post(`/api/v1/admin/product/new`, productData); 83 | dispatch(newProductSuccess(data)) 84 | } catch (error) { 85 | //handle error 86 | dispatch(newProductFail(error.response.data.message)) 87 | } 88 | 89 | } 90 | 91 | export const deleteProduct = id => async (dispatch) => { 92 | 93 | try { 94 | dispatch(deleteProductRequest()) 95 | await axios.delete(`/api/v1/admin/product/${id}`); 96 | dispatch(deleteProductSuccess()) 97 | } catch (error) { 98 | //handle error 99 | dispatch(deleteProductFail(error.response.data.message)) 100 | } 101 | 102 | } 103 | 104 | export const updateProduct = (id, productData) => async (dispatch) => { 105 | 106 | try { 107 | dispatch(updateProductRequest()) 108 | const { data } = await axios.put(`/api/v1/admin/product/${id}`, productData); 109 | dispatch(updateProductSuccess(data)) 110 | } catch (error) { 111 | //handle error 112 | dispatch(updateProductFail(error.response.data.message)) 113 | } 114 | 115 | } 116 | 117 | 118 | export const getReviews = id => async (dispatch) => { 119 | 120 | try { 121 | dispatch(reviewsRequest()) 122 | const { data } = await axios.get(`/api/v1/admin/reviews`,{params: {id}}); 123 | dispatch(reviewsSuccess(data)) 124 | } catch (error) { 125 | //handle error 126 | dispatch(reviewsFail(error.response.data.message)) 127 | } 128 | 129 | } 130 | 131 | export const deleteReview = (productId, id) => async (dispatch) => { 132 | 133 | try { 134 | dispatch(deleteReviewRequest()) 135 | await axios.delete(`/api/v1/admin/review`,{params: {productId, id}}); 136 | dispatch(deleteReviewSuccess()) 137 | } catch (error) { 138 | //handle error 139 | dispatch(deleteReviewFail(error.response.data.message)) 140 | } 141 | 142 | } -------------------------------------------------------------------------------- /frontend/src/components/admin/ReviewList.js: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect, useState } from "react" 2 | import { Button } from "react-bootstrap" 3 | import { useDispatch, useSelector } from "react-redux" 4 | import { deleteReview, getReviews } from "../../actions/productActions" 5 | import { clearError, clearReviewDeleted } from "../../slices/productSlice" 6 | import Loader from '../layouts/Loader'; 7 | import { MDBDataTable} from 'mdbreact'; 8 | import {toast } from 'react-toastify' 9 | import Sidebar from "./Sidebar" 10 | 11 | export default function ReviewList() { 12 | const { reviews = [], loading = true, error, isReviewDeleted } = useSelector(state => state.productState) 13 | const [productId, setProductId] = useState(""); 14 | const dispatch = useDispatch(); 15 | 16 | const setReviews = () => { 17 | const data = { 18 | columns : [ 19 | { 20 | label: 'ID', 21 | field: 'id', 22 | sort: 'asc' 23 | }, 24 | { 25 | label: 'Rating', 26 | field: 'rating', 27 | sort: 'asc' 28 | }, 29 | { 30 | label: 'User', 31 | field: 'user', 32 | sort: 'asc' 33 | }, 34 | { 35 | label: 'Comment', 36 | field: 'comment', 37 | sort: 'asc' 38 | }, 39 | { 40 | label: 'Actions', 41 | field: 'actions', 42 | sort: 'asc' 43 | } 44 | ], 45 | rows : [] 46 | } 47 | 48 | reviews.forEach( review => { 49 | data.rows.push({ 50 | id: review._id, 51 | rating: review.rating, 52 | user : review.user.name, 53 | comment: review.comment , 54 | actions: ( 55 | 56 | 59 | 60 | ) 61 | }) 62 | }) 63 | 64 | return data; 65 | } 66 | 67 | const deleteHandler = (e, id) => { 68 | e.target.disabled = true; 69 | dispatch(deleteReview(productId, id)) 70 | } 71 | 72 | const submitHandler = (e) =>{ 73 | e.preventDefault(); 74 | dispatch(getReviews(productId)) 75 | } 76 | 77 | useEffect(() => { 78 | if(error) { 79 | toast(error, { 80 | position: toast.POSITION.BOTTOM_CENTER, 81 | type: 'error', 82 | onOpen: ()=> { dispatch(clearError()) } 83 | }) 84 | return 85 | } 86 | if(isReviewDeleted) { 87 | toast('Review Deleted Succesfully!',{ 88 | type: 'success', 89 | position: toast.POSITION.BOTTOM_CENTER, 90 | onOpen: () => dispatch(clearReviewDeleted()) 91 | }) 92 | dispatch(getReviews(productId)) 93 | return; 94 | } 95 | 96 | 97 | },[dispatch, error, isReviewDeleted]) 98 | 99 | 100 | return ( 101 |
102 |
103 | 104 |
105 |
106 |

Review List

107 |
108 |
109 |
110 |
111 | 112 | setProductId(e.target.value)} 115 | value={productId} 116 | className="form-control" 117 | /> 118 |
119 | 122 |
123 |
124 |
125 | 126 | {loading ? : 127 | 134 | } 135 | 136 |
137 |
138 | ) 139 | } -------------------------------------------------------------------------------- /frontend/src/components/user/UpdateProfile.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux" 3 | import { toast } from "react-toastify"; 4 | import { updateProfile, clearAuthError } from "../../actions/userActions"; 5 | import { clearUpdateProfile } from "../../slices/authSlice"; 6 | 7 | export default function UpdateProfile () { 8 | const { error, user, isUpdated } = useSelector(state => state.authState); 9 | const [name, setName] = useState(""); 10 | const [email, setEmail] = useState(""); 11 | const [avatar, setAvatar] = useState(""); 12 | const [avatarPreview, setAvatarPreview] = useState("/images/default_avatar.png"); 13 | const dispatch = useDispatch(); 14 | 15 | const onChangeAvatar = (e) => { 16 | const reader = new FileReader(); 17 | reader.onload = () => { 18 | if(reader.readyState === 2) { 19 | setAvatarPreview(reader.result); 20 | setAvatar(e.target.files[0]) 21 | } 22 | } 23 | 24 | 25 | reader.readAsDataURL(e.target.files[0]) 26 | } 27 | 28 | const submitHandler = (e) =>{ 29 | e.preventDefault(); 30 | const formData = new FormData(); 31 | formData.append('name', name) 32 | formData.append('email', email) 33 | formData.append('avatar', avatar); 34 | dispatch(updateProfile(formData)) 35 | } 36 | 37 | useEffect(() => { 38 | if(user) { 39 | setName(user.name); 40 | setEmail(user.email); 41 | if(user.avatar) { 42 | setAvatarPreview(user.avatar) 43 | } 44 | } 45 | 46 | if(isUpdated) { 47 | toast('Profile updated successfully',{ 48 | type: 'success', 49 | position: toast.POSITION.BOTTOM_CENTER, 50 | onOpen: () => dispatch(clearUpdateProfile()) 51 | }) 52 | return; 53 | } 54 | 55 | if(error) { 56 | toast(error, { 57 | position: toast.POSITION.BOTTOM_CENTER, 58 | type: 'error', 59 | onOpen: ()=> { dispatch(clearAuthError) } 60 | }) 61 | return 62 | } 63 | },[user, isUpdated, error, dispatch]) 64 | 65 | return ( 66 |
67 |
68 |
69 |

Update Profile

70 | 71 |
72 | 73 | setName(e.target.value)} 80 | /> 81 |
82 | 83 |
84 | 85 | setEmail(e.target.value)} 92 | /> 93 |
94 | 95 |
96 | 97 |
98 |
99 |
100 | Avatar Preview 105 |
106 |
107 |
108 | 115 | 118 |
119 |
120 |
121 | 122 | 123 |
124 |
125 |
) 126 | } -------------------------------------------------------------------------------- /frontend/src/components/user/Register.js: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | import {useDispatch, useSelector } from 'react-redux' 3 | import { register, clearAuthError } from '../../actions/userActions' 4 | import { toast } from 'react-toastify'; 5 | import { useNavigate } from 'react-router-dom'; 6 | 7 | export default function Register() { 8 | const [userData, setUserData] = useState({ 9 | name: "", 10 | email: "", 11 | password: "" 12 | }); 13 | const [avatar, setAvatar] = useState(""); 14 | const [avatarPreview, setAvatarPreview] = useState("/images/default_avatar.png"); 15 | const dispatch = useDispatch(); 16 | const navigate = useNavigate(); 17 | const { loading, error, isAuthenticated } = useSelector(state => state.authState) 18 | 19 | const onChange = (e) => { 20 | if(e.target.name === 'avatar') { 21 | const reader = new FileReader(); 22 | reader.onload = () => { 23 | if(reader.readyState === 2) { 24 | setAvatarPreview(reader.result); 25 | setAvatar(e.target.files[0]) 26 | } 27 | } 28 | 29 | 30 | reader.readAsDataURL(e.target.files[0]) 31 | }else{ 32 | setUserData({...userData, [e.target.name]:e.target.value }) 33 | } 34 | } 35 | 36 | const submitHandler = (e) => { 37 | e.preventDefault(); 38 | const formData = new FormData(); 39 | formData.append('name', userData.name) 40 | formData.append('email', userData.email) 41 | formData.append('password', userData.password) 42 | formData.append('avatar', avatar); 43 | dispatch(register(formData)) 44 | } 45 | 46 | useEffect(()=>{ 47 | if(isAuthenticated) { 48 | navigate('/'); 49 | return 50 | } 51 | if(error) { 52 | toast(error, { 53 | position: toast.POSITION.BOTTOM_CENTER, 54 | type: 'error', 55 | onOpen: ()=> { dispatch(clearAuthError) } 56 | }) 57 | return 58 | } 59 | },[error, isAuthenticated, dispatch, navigate]) 60 | 61 | return ( 62 |
63 |
64 |
65 |

Register

66 | 67 |
68 | 69 | 70 |
71 | 72 |
73 | 74 | 82 |
83 | 84 |
85 | 86 | 94 |
95 | 96 |
97 | 98 |
99 |
100 |
101 | Avatar 106 |
107 |
108 |
109 | 116 | 119 |
120 |
121 |
122 | 123 | 131 |
132 |
133 |
134 | ) 135 | } -------------------------------------------------------------------------------- /frontend/src/components/admin/Dashboard.js: -------------------------------------------------------------------------------- 1 | import Sidebar from "./Sidebar"; 2 | import {useDispatch, useSelector} from 'react-redux'; 3 | import { useEffect } from "react"; 4 | import { getAdminProducts } from "../../actions/productActions"; 5 | import {getUsers} from '../../actions/userActions' 6 | import {adminOrders as adminOrdersAction} from '../../actions/orderActions' 7 | import { Link } from "react-router-dom"; 8 | 9 | export default function Dashboard () { 10 | const { products = [] } = useSelector( state => state.productsState); 11 | const { adminOrders = [] } = useSelector( state => state.orderState); 12 | const { users = [] } = useSelector( state => state.userState); 13 | const dispatch = useDispatch(); 14 | let outOfStock = 0; 15 | 16 | if (products.length > 0) { 17 | products.forEach( product => { 18 | if( product.stock === 0 ) { 19 | outOfStock = outOfStock + 1; 20 | } 21 | }) 22 | } 23 | 24 | let totalAmount = 0; 25 | if (adminOrders.length > 0) { 26 | adminOrders.forEach( order => { 27 | totalAmount += order.totalPrice 28 | }) 29 | } 30 | 31 | 32 | 33 | useEffect( () => { 34 | dispatch(getAdminProducts); 35 | dispatch(getUsers); 36 | dispatch(adminOrdersAction) 37 | }, []) 38 | 39 | 40 | return ( 41 |
42 |
43 | 44 |
45 |
46 |

Dashboard

47 |
48 |
49 |
50 |
51 |
Total Amount
${totalAmount} 52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
Products
{products.length}
62 |
63 | 64 | View Details 65 | 66 | 67 | 68 | 69 |
70 |
71 | 72 | 73 |
74 |
75 |
76 |
Orders
{adminOrders.length}
77 |
78 | 79 | View Details 80 | 81 | 82 | 83 | 84 |
85 |
86 | 87 | 88 |
89 |
90 |
91 |
Users
{users.length}
92 |
93 | 94 | View Details 95 | 96 | 97 | 98 | 99 |
100 |
101 | 102 | 103 |
104 |
105 |
106 |
Out of Stock
{outOfStock}
107 |
108 |
109 |
110 |
111 |
112 |
113 | ) 114 | } -------------------------------------------------------------------------------- /frontend/src/slices/orderSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | 4 | 5 | const orderSlice = createSlice({ 6 | name: 'order', 7 | initialState: { 8 | orderDetail: {}, 9 | userOrders : [], 10 | adminOrders: [], 11 | loading: false, 12 | isOrderDeleted: false, 13 | isOrderUpdated: false 14 | }, 15 | reducers: { 16 | createOrderRequest(state, action) { 17 | return { 18 | ...state, 19 | loading: true 20 | } 21 | }, 22 | createOrderSuccess(state, action) { 23 | return { 24 | ...state, 25 | loading: false, 26 | orderDetail: action.payload.order 27 | } 28 | }, 29 | createOrderFail(state, action) { 30 | return { 31 | ...state, 32 | loading: false, 33 | error: action.payload 34 | } 35 | }, 36 | clearError(state, action) { 37 | return { 38 | ...state, 39 | error: null 40 | } 41 | }, 42 | userOrdersRequest(state, action) { 43 | return { 44 | ...state, 45 | loading: true 46 | } 47 | }, 48 | userOrdersSuccess(state, action) { 49 | return { 50 | ...state, 51 | loading: false, 52 | userOrders: action.payload.orders 53 | } 54 | }, 55 | userOrdersFail(state, action) { 56 | return { 57 | ...state, 58 | loading: false, 59 | error: action.payload 60 | } 61 | }, 62 | orderDetailRequest(state, action) { 63 | return { 64 | ...state, 65 | loading: true 66 | } 67 | }, 68 | orderDetailSuccess(state, action) { 69 | return { 70 | ...state, 71 | loading: false, 72 | orderDetail: action.payload.order 73 | } 74 | }, 75 | orderDetailFail(state, action) { 76 | return { 77 | ...state, 78 | loading: false, 79 | error: action.payload 80 | } 81 | }, 82 | adminOrdersRequest(state, action) { 83 | return { 84 | ...state, 85 | loading: true 86 | } 87 | }, 88 | adminOrdersSuccess(state, action) { 89 | return { 90 | ...state, 91 | loading: false, 92 | adminOrders: action.payload.orders 93 | } 94 | }, 95 | adminOrdersFail(state, action) { 96 | return { 97 | ...state, 98 | loading: false, 99 | error: action.payload 100 | } 101 | }, 102 | 103 | deleteOrderRequest(state, action) { 104 | return { 105 | ...state, 106 | loading: true 107 | } 108 | }, 109 | deleteOrderSuccess(state, action) { 110 | return { 111 | ...state, 112 | loading: false, 113 | isOrderDeleted: true 114 | } 115 | }, 116 | deleteOrderFail(state, action) { 117 | return { 118 | ...state, 119 | loading: false, 120 | error: action.payload 121 | } 122 | }, 123 | 124 | updateOrderRequest(state, action) { 125 | return { 126 | ...state, 127 | loading: true 128 | } 129 | }, 130 | updateOrderSuccess(state, action) { 131 | return { 132 | ...state, 133 | loading: false, 134 | isOrderUpdated: true 135 | } 136 | }, 137 | updateOrderFail(state, action) { 138 | return { 139 | ...state, 140 | loading: false, 141 | error: action.payload 142 | } 143 | }, 144 | 145 | clearOrderDeleted(state, action) { 146 | return { 147 | ...state, 148 | isOrderDeleted: false 149 | } 150 | }, 151 | clearOrderUpdated(state, action) { 152 | return { 153 | ...state, 154 | isOrderUpdated: false 155 | } 156 | } 157 | 158 | } 159 | }); 160 | 161 | const { actions, reducer } = orderSlice; 162 | 163 | export const { 164 | createOrderFail, 165 | createOrderSuccess, 166 | createOrderRequest, 167 | clearError, 168 | userOrdersFail, 169 | userOrdersSuccess, 170 | userOrdersRequest, 171 | orderDetailFail, 172 | orderDetailSuccess, 173 | orderDetailRequest, 174 | adminOrdersFail, 175 | adminOrdersRequest, 176 | adminOrdersSuccess, 177 | deleteOrderFail, 178 | deleteOrderRequest, 179 | deleteOrderSuccess, 180 | updateOrderFail, 181 | updateOrderRequest, 182 | updateOrderSuccess, 183 | clearOrderDeleted, 184 | clearOrderUpdated 185 | } = actions; 186 | 187 | export default reducer; 188 | 189 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import Home from './components/Home'; 3 | import Footer from './components/layouts/Footer'; 4 | import Header from './components/layouts/Header'; 5 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom' 6 | import { HelmetProvider } from 'react-helmet-async' 7 | import { ToastContainer } from 'react-toastify'; 8 | import 'react-toastify/dist/ReactToastify.css'; 9 | import ProductDetail from './components/product/ProductDetail'; 10 | import ProductSearch from './components/product/ProductSearch'; 11 | import Login from './components/user/Login'; 12 | import Register from './components/user/Register'; 13 | import { useEffect, useState } from 'react'; 14 | import store from './store'; 15 | import { loadUser } from './actions/userActions'; 16 | import Profile from './components/user/Profile'; 17 | import ProtectedRoute from './components/route/ProtectedRoute'; 18 | import UpdateProfile from './components/user/UpdateProfile'; 19 | import UpdatePassword from './components/user/UpdatePassword'; 20 | import ForgotPassword from './components/user/ForgotPassword'; 21 | import ResetPassword from './components/user/ResetPassword'; 22 | import Cart from './components/cart/Cart'; 23 | import Shipping from './components/cart/Shipping'; 24 | import ConfirmOrder from './components/cart/ConfirmOrder'; 25 | import Payment from './components/cart/Payment'; 26 | import axios from 'axios'; 27 | import { Elements } from '@stripe/react-stripe-js'; 28 | import { loadStripe } from '@stripe/stripe-js'; 29 | import OrderSuccess from './components/cart/OrderSuccess'; 30 | import UserOrders from './components/order/UserOrders'; 31 | import OrderDetail from './components/order/OrderDetail'; 32 | import Dashboard from './components/admin/Dashboard'; 33 | import ProductList from './components/admin/ProductList'; 34 | import NewProduct from './components/admin/NewProduct'; 35 | import UpdateProduct from './components/admin/UpdateProduct'; 36 | import OrderList from './components/admin/OrderList'; 37 | import UpdateOrder from './components/admin/UpdateOrder'; 38 | import UserList from './components/admin/UserList'; 39 | import UpdateUser from './components/admin/UpdateUser'; 40 | import ReviewList from './components/admin/ReviewList'; 41 | 42 | function App() { 43 | const [stripeApiKey, setStripeApiKey] = useState("") 44 | useEffect(() => { 45 | store.dispatch(loadUser) 46 | async function getStripeApiKey(){ 47 | const {data} = await axios.get('/api/v1/stripeapi') 48 | setStripeApiKey(data.stripeApiKey) 49 | } 50 | getStripeApiKey() 51 | },[]) 52 | 53 | return ( 54 | 55 |
56 | 57 |
58 |
59 | 60 | 61 | } /> 62 | } /> 63 | } /> 64 | } /> 65 | } /> 66 | } /> 67 | } /> 68 | } /> 69 | } /> 70 | } /> 71 | } /> 72 | } /> 73 | } /> 74 | } /> 75 | } /> 76 | } /> 77 | {stripeApiKey && } /> 78 | } 79 | 80 |
81 | {/* Admin Routes */} 82 | 83 | } /> 84 | } /> 85 | } /> 86 | } /> 87 | } /> 88 | } /> 89 | } /> 90 | } /> 91 | } /> 92 | 93 |
94 | 95 |
96 |
97 | ); 98 | } 99 | 100 | export default App; 101 | -------------------------------------------------------------------------------- /frontend/src/slices/authSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | 4 | const authSlice = createSlice({ 5 | name: 'auth', 6 | initialState: { 7 | loading: true, 8 | isAuthenticated: false 9 | }, 10 | reducers: { 11 | loginRequest(state, action){ 12 | return { 13 | ...state, 14 | loading: true, 15 | } 16 | }, 17 | loginSuccess(state, action){ 18 | return { 19 | loading: false, 20 | isAuthenticated: true, 21 | user: action.payload.user 22 | } 23 | }, 24 | loginFail(state, action){ 25 | return { 26 | ...state, 27 | loading: false, 28 | error: action.payload 29 | } 30 | }, 31 | clearError(state, action){ 32 | return { 33 | ...state, 34 | error: null 35 | } 36 | }, 37 | registerRequest(state, action){ 38 | return { 39 | ...state, 40 | loading: true, 41 | } 42 | }, 43 | registerSuccess(state, action){ 44 | return { 45 | loading: false, 46 | isAuthenticated: true, 47 | user: action.payload.user 48 | } 49 | }, 50 | registerFail(state, action){ 51 | return { 52 | ...state, 53 | loading: false, 54 | error: action.payload 55 | } 56 | }, 57 | loadUserRequest(state, action){ 58 | return { 59 | ...state, 60 | isAuthenticated: false, 61 | loading: true, 62 | } 63 | }, 64 | loadUserSuccess(state, action){ 65 | return { 66 | loading: false, 67 | isAuthenticated: true, 68 | user: action.payload.user 69 | } 70 | }, 71 | loadUserFail(state, action){ 72 | return { 73 | ...state, 74 | loading: false, 75 | } 76 | }, 77 | logoutSuccess(state, action){ 78 | return { 79 | loading: false, 80 | isAuthenticated: false, 81 | } 82 | }, 83 | logoutFail(state, action){ 84 | return { 85 | ...state, 86 | error: action.payload 87 | } 88 | }, 89 | updateProfileRequest(state, action){ 90 | return { 91 | ...state, 92 | loading: true, 93 | isUpdated: false 94 | } 95 | }, 96 | updateProfileSuccess(state, action){ 97 | return { 98 | ...state, 99 | loading: false, 100 | user: action.payload.user, 101 | isUpdated: true 102 | } 103 | }, 104 | updateProfileFail(state, action){ 105 | return { 106 | ...state, 107 | loading: false, 108 | error: action.payload 109 | } 110 | }, 111 | clearUpdateProfile(state, action){ 112 | return { 113 | ...state, 114 | isUpdated: false 115 | } 116 | }, 117 | 118 | updatePasswordRequest(state, action){ 119 | return { 120 | ...state, 121 | loading: true, 122 | isUpdated: false 123 | } 124 | }, 125 | updatePasswordSuccess(state, action){ 126 | return { 127 | ...state, 128 | loading: false, 129 | isUpdated: true 130 | } 131 | }, 132 | updatePasswordFail(state, action){ 133 | return { 134 | ...state, 135 | loading: false, 136 | error: action.payload 137 | } 138 | }, 139 | forgotPasswordRequest(state, action){ 140 | return { 141 | ...state, 142 | loading: true, 143 | message: null 144 | } 145 | }, 146 | forgotPasswordSuccess(state, action){ 147 | return { 148 | ...state, 149 | loading: false, 150 | message: action.payload.message 151 | } 152 | }, 153 | forgotPasswordFail(state, action){ 154 | return { 155 | ...state, 156 | loading: false, 157 | error: action.payload 158 | } 159 | }, 160 | resetPasswordRequest(state, action){ 161 | return { 162 | ...state, 163 | loading: true, 164 | } 165 | }, 166 | resetPasswordSuccess(state, action){ 167 | return { 168 | ...state, 169 | loading: false, 170 | isAuthenticated: true, 171 | user: action.payload.user 172 | } 173 | }, 174 | resetPasswordFail(state, action){ 175 | return { 176 | ...state, 177 | loading: false, 178 | error: action.payload 179 | } 180 | }, 181 | 182 | } 183 | }); 184 | 185 | const { actions, reducer } = authSlice; 186 | 187 | export const { 188 | loginRequest, 189 | loginSuccess, 190 | loginFail, 191 | clearError, 192 | registerRequest, 193 | registerSuccess, 194 | registerFail, 195 | loadUserRequest, 196 | loadUserSuccess, 197 | loadUserFail, 198 | logoutFail, 199 | logoutSuccess, 200 | updateProfileFail, 201 | updateProfileRequest, 202 | updateProfileSuccess, 203 | clearUpdateProfile, 204 | updatePasswordFail, 205 | updatePasswordSuccess, 206 | updatePasswordRequest, 207 | forgotPasswordFail, 208 | forgotPasswordSuccess, 209 | forgotPasswordRequest, 210 | resetPasswordFail, 211 | resetPasswordRequest, 212 | resetPasswordSuccess, 213 | 214 | } = actions; 215 | 216 | export default reducer; 217 | 218 | -------------------------------------------------------------------------------- /frontend/src/components/cart/Payment.js: -------------------------------------------------------------------------------- 1 | import { useElements, useStripe } from "@stripe/react-stripe-js" 2 | import { CardNumberElement, CardExpiryElement, CardCvcElement } from "@stripe/react-stripe-js"; 3 | import axios from "axios"; 4 | import { useEffect } from "react"; 5 | import {useDispatch, useSelector} from 'react-redux'; 6 | import {useNavigate} from 'react-router-dom' 7 | import { toast } from "react-toastify"; 8 | import { orderCompleted } from "../../slices/cartSlice"; 9 | import {validateShipping} from '../cart/Shipping'; 10 | import {createOrder} from '../../actions/orderActions' 11 | import { clearError as clearOrderError } from "../../slices/orderSlice"; 12 | 13 | export default function Payment() { 14 | const stripe = useStripe(); 15 | const elements = useElements(); 16 | const dispatch = useDispatch() 17 | const navigate = useNavigate(); 18 | const orderInfo = JSON.parse(sessionStorage.getItem('orderInfo')) 19 | const { user } = useSelector(state => state.authState) 20 | const {items:cartItems, shippingInfo } = useSelector(state => state.cartState) 21 | const { error:orderError } = useSelector(state => state.orderState) 22 | 23 | const paymentData = { 24 | amount : Math.round( orderInfo.totalPrice * 100), 25 | shipping :{ 26 | name: user.name, 27 | address:{ 28 | city: shippingInfo.city, 29 | postal_code : shippingInfo.postalCode, 30 | country: shippingInfo.country, 31 | state: shippingInfo.state, 32 | line1 : shippingInfo.address 33 | }, 34 | phone: shippingInfo.phoneNo 35 | } 36 | } 37 | 38 | const order = { 39 | orderItems: cartItems, 40 | shippingInfo 41 | } 42 | 43 | if(orderInfo) { 44 | order.itemsPrice = orderInfo.itemsPrice 45 | order.shippingPrice = orderInfo.shippingPrice 46 | order.taxPrice = orderInfo.taxPrice 47 | order.totalPrice = orderInfo.totalPrice 48 | 49 | } 50 | 51 | useEffect(() => { 52 | validateShipping(shippingInfo, navigate) 53 | if(orderError) { 54 | toast(orderError, { 55 | position: toast.POSITION.BOTTOM_CENTER, 56 | type: 'error', 57 | onOpen: ()=> { dispatch(clearOrderError()) } 58 | }) 59 | return 60 | } 61 | 62 | },[]) 63 | 64 | const submitHandler = async (e) => { 65 | e.preventDefault(); 66 | document.querySelector('#pay_btn').disabled = true; 67 | try { 68 | const {data} = await axios.post('/api/v1/payment/process', paymentData) 69 | const clientSecret = data.client_secret 70 | const result = await stripe.confirmCardPayment(clientSecret, { 71 | payment_method: { 72 | card: elements.getElement(CardNumberElement), 73 | billing_details: { 74 | name: user.name, 75 | email: user.email 76 | } 77 | } 78 | }) 79 | 80 | if(result.error){ 81 | toast(result.error.message, { 82 | type: 'error', 83 | position: toast.POSITION.BOTTOM_CENTER 84 | }) 85 | document.querySelector('#pay_btn').disabled = false; 86 | }else{ 87 | if((await result).paymentIntent.status === 'succeeded') { 88 | toast('Payment Success!', { 89 | type: 'success', 90 | position: toast.POSITION.BOTTOM_CENTER 91 | }) 92 | order.paymentInfo = { 93 | id: result.paymentIntent.id, 94 | status: result.paymentIntent.status 95 | } 96 | dispatch(orderCompleted()) 97 | dispatch(createOrder(order)) 98 | 99 | navigate('/order/success') 100 | }else{ 101 | toast('Please Try again!', { 102 | type: 'warning', 103 | position: toast.POSITION.BOTTOM_CENTER 104 | }) 105 | } 106 | } 107 | 108 | 109 | } catch (error) { 110 | 111 | } 112 | } 113 | 114 | 115 | return ( 116 |
117 |
118 |
119 |

Card Info

120 |
121 | 122 | 128 |
129 | 130 |
131 | 132 | 138 |
139 | 140 |
141 | 142 | 148 |
149 | 150 | 151 | 158 | 159 |
160 |
161 |
162 | ) 163 | } -------------------------------------------------------------------------------- /frontend/src/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | loginFail, 3 | loginRequest, 4 | loginSuccess, 5 | clearError, 6 | registerFail, 7 | registerRequest, 8 | registerSuccess, 9 | loadUserRequest, 10 | loadUserSuccess, 11 | loadUserFail, 12 | logoutSuccess, 13 | logoutFail, 14 | updateProfileRequest, 15 | updateProfileSuccess, 16 | updateProfileFail, 17 | updatePasswordRequest, 18 | updatePasswordSuccess, 19 | updatePasswordFail, 20 | forgotPasswordRequest, 21 | forgotPasswordSuccess, 22 | forgotPasswordFail, 23 | resetPasswordRequest, 24 | resetPasswordSuccess, 25 | resetPasswordFail 26 | } from '../slices/authSlice'; 27 | 28 | import { 29 | usersRequest, 30 | usersSuccess, 31 | usersFail, 32 | userRequest, 33 | userSuccess, 34 | userFail, 35 | deleteUserRequest, 36 | deleteUserSuccess, 37 | deleteUserFail, 38 | updateUserRequest, 39 | updateUserSuccess, 40 | updateUserFail 41 | 42 | } from '../slices/userSlice' 43 | import axios from 'axios'; 44 | 45 | export const login = (email, password) => async (dispatch) => { 46 | 47 | try { 48 | dispatch(loginRequest()) 49 | const { data } = await axios.post(`/api/v1/login`,{email,password}); 50 | dispatch(loginSuccess(data)) 51 | } catch (error) { 52 | dispatch(loginFail(error.response.data.message)) 53 | } 54 | 55 | } 56 | 57 | export const clearAuthError = dispatch => { 58 | dispatch(clearError()) 59 | } 60 | 61 | export const register = (userData) => async (dispatch) => { 62 | 63 | try { 64 | dispatch(registerRequest()) 65 | const config = { 66 | headers: { 67 | 'Content-type': 'multipart/form-data' 68 | } 69 | } 70 | 71 | const { data } = await axios.post(`/api/v1/register`,userData, config); 72 | dispatch(registerSuccess(data)) 73 | } catch (error) { 74 | dispatch(registerFail(error.response.data.message)) 75 | } 76 | 77 | } 78 | 79 | export const loadUser = async (dispatch) => { 80 | 81 | try { 82 | dispatch(loadUserRequest()) 83 | 84 | 85 | const { data } = await axios.get(`/api/v1/myprofile`); 86 | dispatch(loadUserSuccess(data)) 87 | } catch (error) { 88 | dispatch(loadUserFail(error.response.data.message)) 89 | } 90 | 91 | } 92 | 93 | export const logout = async (dispatch) => { 94 | 95 | try { 96 | await axios.get(`/api/v1/logout`); 97 | dispatch(logoutSuccess()) 98 | } catch (error) { 99 | dispatch(logoutFail) 100 | } 101 | 102 | } 103 | 104 | export const updateProfile = (userData) => async (dispatch) => { 105 | 106 | try { 107 | dispatch(updateProfileRequest()) 108 | const config = { 109 | headers: { 110 | 'Content-type': 'multipart/form-data' 111 | } 112 | } 113 | 114 | const { data } = await axios.put(`/api/v1/update`,userData, config); 115 | dispatch(updateProfileSuccess(data)) 116 | } catch (error) { 117 | dispatch(updateProfileFail(error.response.data.message)) 118 | } 119 | 120 | } 121 | 122 | export const updatePassword = (formData) => async (dispatch) => { 123 | 124 | try { 125 | dispatch(updatePasswordRequest()) 126 | const config = { 127 | headers: { 128 | 'Content-type': 'application/json' 129 | } 130 | } 131 | await axios.put(`/api/v1/password/change`, formData, config); 132 | dispatch(updatePasswordSuccess()) 133 | } catch (error) { 134 | dispatch(updatePasswordFail(error.response.data.message)) 135 | } 136 | 137 | } 138 | 139 | export const forgotPassword = (formData) => async (dispatch) => { 140 | 141 | try { 142 | dispatch(forgotPasswordRequest()) 143 | const config = { 144 | headers: { 145 | 'Content-type': 'application/json' 146 | } 147 | } 148 | const { data} = await axios.post(`/api/v1/password/forgot`, formData, config); 149 | dispatch(forgotPasswordSuccess(data)) 150 | } catch (error) { 151 | dispatch(forgotPasswordFail(error.response.data.message)) 152 | } 153 | 154 | } 155 | 156 | export const resetPassword = (formData, token) => async (dispatch) => { 157 | 158 | try { 159 | dispatch(resetPasswordRequest()) 160 | const config = { 161 | headers: { 162 | 'Content-type': 'application/json' 163 | } 164 | } 165 | const { data} = await axios.post(`/api/v1/password/reset/${token}`, formData, config); 166 | dispatch(resetPasswordSuccess(data)) 167 | } catch (error) { 168 | dispatch(resetPasswordFail(error.response.data.message)) 169 | } 170 | 171 | } 172 | 173 | export const getUsers = async (dispatch) => { 174 | 175 | try { 176 | dispatch(usersRequest()) 177 | const { data } = await axios.get(`/api/v1/admin/users`); 178 | dispatch(usersSuccess(data)) 179 | } catch (error) { 180 | dispatch(usersFail(error.response.data.message)) 181 | } 182 | 183 | } 184 | 185 | export const getUser = id => async (dispatch) => { 186 | 187 | try { 188 | dispatch(userRequest()) 189 | const { data } = await axios.get(`/api/v1/admin/user/${id}`); 190 | dispatch(userSuccess(data)) 191 | } catch (error) { 192 | dispatch(userFail(error.response.data.message)) 193 | } 194 | 195 | } 196 | 197 | export const deleteUser = id => async (dispatch) => { 198 | 199 | try { 200 | dispatch(deleteUserRequest()) 201 | await axios.delete(`/api/v1/admin/user/${id}`); 202 | dispatch(deleteUserSuccess()) 203 | } catch (error) { 204 | dispatch(deleteUserFail(error.response.data.message)) 205 | } 206 | 207 | } 208 | 209 | export const updateUser = (id, formData) => async (dispatch) => { 210 | 211 | try { 212 | dispatch(updateUserRequest()) 213 | const config = { 214 | headers: { 215 | 'Content-type': 'application/json' 216 | } 217 | } 218 | await axios.put(`/api/v1/admin/user/${id}`, formData, config); 219 | dispatch(updateUserSuccess()) 220 | } catch (error) { 221 | dispatch(updateUserFail(error.response.data.message)) 222 | } 223 | 224 | } -------------------------------------------------------------------------------- /frontend/src/components/admin/UpdateOrder.js: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect, useState } from "react"; 2 | import Sidebar from "./Sidebar"; 3 | import { useDispatch, useSelector} from 'react-redux'; 4 | import { useNavigate, useParams } from "react-router-dom"; 5 | import { orderDetail as orderDetailAction, updateOrder } from "../../actions/orderActions"; 6 | import { toast } from "react-toastify"; 7 | import { clearOrderUpdated, clearError } from "../../slices/orderSlice"; 8 | import { Link } from "react-router-dom"; 9 | 10 | export default function UpdateOrder () { 11 | 12 | 13 | const { loading, isOrderUpdated, error, orderDetail } = useSelector( state => state.orderState) 14 | const { user = {}, orderItems = [], shippingInfo = {}, totalPrice = 0, paymentInfo = {}} = orderDetail; 15 | const isPaid = paymentInfo.status === 'succeeded'? true: false; 16 | const [orderStatus, setOrderStatus] = useState("Processing"); 17 | const { id:orderId } = useParams(); 18 | 19 | 20 | const navigate = useNavigate(); 21 | const dispatch = useDispatch(); 22 | 23 | const submitHandler = (e) => { 24 | e.preventDefault(); 25 | const orderData = {}; 26 | orderData.orderStatus = orderStatus; 27 | dispatch(updateOrder(orderId, orderData)) 28 | } 29 | 30 | useEffect(() => { 31 | if(isOrderUpdated) { 32 | toast('Order Updated Succesfully!',{ 33 | type: 'success', 34 | position: toast.POSITION.BOTTOM_CENTER, 35 | onOpen: () => dispatch(clearOrderUpdated()) 36 | }) 37 | 38 | return; 39 | } 40 | 41 | if(error) { 42 | toast(error, { 43 | position: toast.POSITION.BOTTOM_CENTER, 44 | type: 'error', 45 | onOpen: ()=> { dispatch(clearError()) } 46 | }) 47 | return 48 | } 49 | 50 | dispatch(orderDetailAction(orderId)) 51 | }, [isOrderUpdated, error, dispatch]) 52 | 53 | 54 | useEffect(() => { 55 | if(orderDetail._id) { 56 | setOrderStatus(orderDetail.orderStatus); 57 | } 58 | },[orderDetail]) 59 | 60 | 61 | return ( 62 |
63 |
64 | 65 |
66 |
67 | 68 |
69 |
70 | 71 |

Order # {orderDetail._id}

72 | 73 |

Shipping Info

74 |

Name: {user.name}

75 |

Phone: {shippingInfo.phoneNo}

76 |

Address:{shippingInfo.address}, {shippingInfo.city}, {shippingInfo.postalCode}, {shippingInfo.state}, {shippingInfo.country}

77 |

Amount: ${totalPrice}

78 | 79 |
80 | 81 |

Payment

82 |

{isPaid ? 'PAID' : 'NOT PAID' }

83 | 84 | 85 |

Order Status:

86 |

{orderStatus}

87 | 88 | 89 |

Order Items:

90 | 91 |
92 |
93 | {orderItems && orderItems.map(item => ( 94 |
95 |
96 | {item.name} 97 |
98 | 99 |
100 | {item.name} 101 |
102 | 103 | 104 |
105 |

${item.price}

106 |
107 | 108 |
109 |

{item.quantity} Piece(s)

110 |
111 |
112 | ))} 113 | 114 |
115 |
116 |
117 |
118 |

Order Status

119 |
120 | 130 | 131 |
132 | 139 | 140 |
141 |
142 |
143 |
144 |
145 | 146 | ) 147 | } -------------------------------------------------------------------------------- /frontend/src/components/cart/Shipping.js: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import { Fragment, useState } from "react"; 3 | import {countries} from 'countries-list' 4 | import { saveShippingInfo } from "../../slices/cartSlice"; 5 | import { useNavigate } from "react-router-dom"; 6 | import CheckoutSteps from "./CheckoutStep"; 7 | import { toast } from "react-toastify"; 8 | 9 | export const validateShipping = (shippingInfo, navigate) => { 10 | 11 | if( 12 | !shippingInfo.address|| 13 | !shippingInfo.city|| 14 | !shippingInfo.state|| 15 | !shippingInfo.country|| 16 | !shippingInfo.phoneNo|| 17 | !shippingInfo.postalCode 18 | ) { 19 | toast.error('Please fill the shipping information',{position: toast.POSITION.BOTTOM_CENTER}) 20 | navigate('/shipping') 21 | } 22 | } 23 | 24 | 25 | export default function Shipping() { 26 | const {shippingInfo={} } = useSelector(state => state.cartState) 27 | 28 | const [address, setAddress] = useState(shippingInfo.address); 29 | const [city, setCity] = useState(shippingInfo.city); 30 | const [phoneNo, setPhoneNo] = useState(shippingInfo.phoneNo); 31 | const [postalCode, setPostalCode] = useState(shippingInfo.postalCode); 32 | const [country, setCountry] = useState(shippingInfo.country); 33 | const [state, setState] = useState(shippingInfo.state); 34 | const countryList = Object.values(countries); 35 | const dispatch = useDispatch(); 36 | const navigate = useNavigate(); 37 | 38 | const submitHandler = (e) => { 39 | e.preventDefault(); 40 | dispatch(saveShippingInfo({address, city, phoneNo, postalCode, country, state})) 41 | navigate('/order/confirm') 42 | } 43 | 44 | 45 | 46 | 47 | 48 | return ( 49 | 50 | 51 |
52 |
53 |
54 |

Shipping Info

55 |
56 | 57 | setAddress(e.target.value)} 63 | required 64 | /> 65 |
66 | 67 |
68 | 69 | setCity(e.target.value)} 75 | required 76 | /> 77 |
78 | 79 |
80 | 81 | setPhoneNo(e.target.value)} 87 | required 88 | /> 89 |
90 | 91 |
92 | 93 | setPostalCode(e.target.value)} 99 | required 100 | /> 101 |
102 | 103 |
104 | 105 | 120 |
121 |
122 | 123 | setState(e.target.value)} 129 | required 130 | /> 131 |
132 | 133 | 140 |
141 |
142 |
143 |
144 | ) 145 | } -------------------------------------------------------------------------------- /backend/controllers/productController.js: -------------------------------------------------------------------------------- 1 | const Product = require('../models/productModel'); 2 | const ErrorHandler = require('../utils/errorHandler') 3 | const catchAsyncError = require('../middlewares/catchAsyncError') 4 | const APIFeatures = require('../utils/apiFeatures'); 5 | 6 | //Get Products - /api/v1/products 7 | exports.getProducts = catchAsyncError(async (req, res, next)=>{ 8 | const resPerPage = 3; 9 | 10 | let buildQuery = () => { 11 | return new APIFeatures(Product.find(), req.query).search().filter() 12 | } 13 | 14 | const filteredProductsCount = await buildQuery().query.countDocuments({}) 15 | const totalProductsCount = await Product.countDocuments({}); 16 | let productsCount = totalProductsCount; 17 | 18 | if(filteredProductsCount !== totalProductsCount) { 19 | productsCount = filteredProductsCount; 20 | } 21 | 22 | const products = await buildQuery().paginate(resPerPage).query; 23 | 24 | res.status(200).json({ 25 | success : true, 26 | count: productsCount, 27 | resPerPage, 28 | products 29 | }) 30 | }) 31 | 32 | //Create Product - /api/v1/product/new 33 | exports.newProduct = catchAsyncError(async (req, res, next)=>{ 34 | let images = [] 35 | let BASE_URL = process.env.BACKEND_URL; 36 | if(process.env.NODE_ENV === "production"){ 37 | BASE_URL = `${req.protocol}://${req.get('host')}` 38 | } 39 | 40 | if(req.files.length > 0) { 41 | req.files.forEach( file => { 42 | let url = `${BASE_URL}/uploads/product/${file.originalname}`; 43 | images.push({ image: url }) 44 | }) 45 | } 46 | 47 | req.body.images = images; 48 | 49 | req.body.user = req.user.id; 50 | const product = await Product.create(req.body); 51 | res.status(201).json({ 52 | success: true, 53 | product 54 | }) 55 | }); 56 | 57 | //Get Single Product - api/v1/product/:id 58 | exports.getSingleProduct = catchAsyncError(async(req, res, next) => { 59 | const product = await Product.findById(req.params.id).populate('reviews.user','name email'); 60 | 61 | if(!product) { 62 | return next(new ErrorHandler('Product not found', 400)); 63 | } 64 | 65 | res.status(201).json({ 66 | success: true, 67 | product 68 | }) 69 | }) 70 | 71 | //Update Product - api/v1/product/:id 72 | exports.updateProduct = catchAsyncError(async (req, res, next) => { 73 | let product = await Product.findById(req.params.id); 74 | 75 | //uploading images 76 | let images = [] 77 | 78 | //if images not cleared we keep existing images 79 | if(req.body.imagesCleared === 'false' ) { 80 | images = product.images; 81 | } 82 | let BASE_URL = process.env.BACKEND_URL; 83 | if(process.env.NODE_ENV === "production"){ 84 | BASE_URL = `${req.protocol}://${req.get('host')}` 85 | } 86 | 87 | if(req.files.length > 0) { 88 | req.files.forEach( file => { 89 | let url = `${BASE_URL}/uploads/product/${file.originalname}`; 90 | images.push({ image: url }) 91 | }) 92 | } 93 | 94 | 95 | req.body.images = images; 96 | 97 | if(!product) { 98 | return res.status(404).json({ 99 | success: false, 100 | message: "Product not found" 101 | }); 102 | } 103 | 104 | product = await Product.findByIdAndUpdate(req.params.id, req.body, { 105 | new: true, 106 | runValidators: true 107 | }) 108 | 109 | res.status(200).json({ 110 | success: true, 111 | product 112 | }) 113 | 114 | }) 115 | 116 | //Delete Product - api/v1/product/:id 117 | exports.deleteProduct = catchAsyncError(async (req, res, next) =>{ 118 | const product = await Product.findById(req.params.id); 119 | 120 | if(!product) { 121 | return res.status(404).json({ 122 | success: false, 123 | message: "Product not found" 124 | }); 125 | } 126 | 127 | await product.remove(); 128 | 129 | res.status(200).json({ 130 | success: true, 131 | message: "Product Deleted!" 132 | }) 133 | 134 | }) 135 | 136 | //Create Review - api/v1/review 137 | exports.createReview = catchAsyncError(async (req, res, next) =>{ 138 | const { productId, rating, comment } = req.body; 139 | 140 | const review = { 141 | user : req.user.id, 142 | rating, 143 | comment 144 | } 145 | 146 | const product = await Product.findById(productId); 147 | //finding user review exists 148 | const isReviewed = product.reviews.find(review => { 149 | return review.user.toString() == req.user.id.toString() 150 | }) 151 | 152 | if(isReviewed){ 153 | //updating the review 154 | product.reviews.forEach(review => { 155 | if(review.user.toString() == req.user.id.toString()){ 156 | review.comment = comment 157 | review.rating = rating 158 | } 159 | 160 | }) 161 | 162 | }else{ 163 | //creating the review 164 | product.reviews.push(review); 165 | product.numOfReviews = product.reviews.length; 166 | } 167 | //find the average of the product reviews 168 | product.ratings = product.reviews.reduce((acc, review) => { 169 | return review.rating + acc; 170 | }, 0) / product.reviews.length; 171 | product.ratings = isNaN(product.ratings)?0:product.ratings; 172 | 173 | await product.save({validateBeforeSave: false}); 174 | 175 | res.status(200).json({ 176 | success: true 177 | }) 178 | 179 | 180 | }) 181 | 182 | //Get Reviews - api/v1/reviews?id={productId} 183 | exports.getReviews = catchAsyncError(async (req, res, next) =>{ 184 | const product = await Product.findById(req.query.id).populate('reviews.user','name email'); 185 | 186 | res.status(200).json({ 187 | success: true, 188 | reviews: product.reviews 189 | }) 190 | }) 191 | 192 | //Delete Review - api/v1/review 193 | exports.deleteReview = catchAsyncError(async (req, res, next) =>{ 194 | const product = await Product.findById(req.query.productId); 195 | 196 | //filtering the reviews which does match the deleting review id 197 | const reviews = product.reviews.filter(review => { 198 | return review._id.toString() !== req.query.id.toString() 199 | }); 200 | //number of reviews 201 | const numOfReviews = reviews.length; 202 | 203 | //finding the average with the filtered reviews 204 | let ratings = reviews.reduce((acc, review) => { 205 | return review.rating + acc; 206 | }, 0) / reviews.length; 207 | ratings = isNaN(ratings)?0:ratings; 208 | 209 | //save the product document 210 | await Product.findByIdAndUpdate(req.query.productId, { 211 | reviews, 212 | numOfReviews, 213 | ratings 214 | }) 215 | res.status(200).json({ 216 | success: true 217 | }) 218 | 219 | 220 | }); 221 | 222 | // get admin products - api/v1/admin/products 223 | exports.getAdminProducts = catchAsyncError(async (req, res, next) =>{ 224 | const products = await Product.find(); 225 | res.status(200).send({ 226 | success: true, 227 | products 228 | }) 229 | }); -------------------------------------------------------------------------------- /backend/controllers/authController.js: -------------------------------------------------------------------------------- 1 | const catchAsyncError = require('../middlewares/catchAsyncError'); 2 | const User = require('../models/userModel'); 3 | const sendEmail = require('../utils/email'); 4 | const ErrorHandler = require('../utils/errorHandler'); 5 | const sendToken = require('../utils/jwt'); 6 | const crypto = require('crypto') 7 | 8 | //Register User - /api/v1/register 9 | exports.registerUser = catchAsyncError(async (req, res, next) => { 10 | const {name, email, password } = req.body 11 | 12 | let avatar; 13 | 14 | let BASE_URL = process.env.BACKEND_URL; 15 | if(process.env.NODE_ENV === "production"){ 16 | BASE_URL = `${req.protocol}://${req.get('host')}` 17 | } 18 | 19 | if(req.file){ 20 | avatar = `${BASE_URL}/uploads/user/${req.file.originalname}` 21 | } 22 | 23 | const user = await User.create({ 24 | name, 25 | email, 26 | password, 27 | avatar 28 | }); 29 | 30 | sendToken(user, 201, res) 31 | 32 | }) 33 | 34 | //Login User - /api/v1/login 35 | exports.loginUser = catchAsyncError(async (req, res, next) => { 36 | const {email, password} = req.body 37 | 38 | if(!email || !password) { 39 | return next(new ErrorHandler('Please enter email & password', 400)) 40 | } 41 | 42 | //finding the user database 43 | const user = await User.findOne({email}).select('+password'); 44 | 45 | if(!user) { 46 | return next(new ErrorHandler('Invalid email or password', 401)) 47 | } 48 | 49 | if(!await user.isValidPassword(password)){ 50 | return next(new ErrorHandler('Invalid email or password', 401)) 51 | } 52 | 53 | sendToken(user, 201, res) 54 | 55 | }) 56 | 57 | //Logout - /api/v1/logout 58 | exports.logoutUser = (req, res, next) => { 59 | res.cookie('token',null, { 60 | expires: new Date(Date.now()), 61 | httpOnly: true 62 | }) 63 | .status(200) 64 | .json({ 65 | success: true, 66 | message: "Loggedout" 67 | }) 68 | 69 | } 70 | 71 | //Forgot Password - /api/v1/password/forgot 72 | exports.forgotPassword = catchAsyncError( async (req, res, next)=>{ 73 | const user = await User.findOne({email: req.body.email}); 74 | 75 | if(!user) { 76 | return next(new ErrorHandler('User not found with this email', 404)) 77 | } 78 | 79 | const resetToken = user.getResetToken(); 80 | await user.save({validateBeforeSave: false}) 81 | 82 | let BASE_URL = process.env.FRONTEND_URL; 83 | if(process.env.NODE_ENV === "production"){ 84 | BASE_URL = `${req.protocol}://${req.get('host')}` 85 | } 86 | 87 | 88 | //Create reset url 89 | const resetUrl = `${BASE_URL}/password/reset/${resetToken}`; 90 | 91 | const message = `Your password reset url is as follows \n\n 92 | ${resetUrl} \n\n If you have not requested this email, then ignore it.`; 93 | 94 | try{ 95 | sendEmail({ 96 | email: user.email, 97 | subject: "JVLcart Password Recovery", 98 | message 99 | }) 100 | 101 | res.status(200).json({ 102 | success: true, 103 | message: `Email sent to ${user.email}` 104 | }) 105 | 106 | }catch(error){ 107 | user.resetPasswordToken = undefined; 108 | user.resetPasswordTokenExpire = undefined; 109 | await user.save({validateBeforeSave: false}); 110 | return next(new ErrorHandler(error.message), 500) 111 | } 112 | 113 | }) 114 | 115 | //Reset Password - /api/v1/password/reset/:token 116 | exports.resetPassword = catchAsyncError( async (req, res, next) => { 117 | const resetPasswordToken = crypto.createHash('sha256').update(req.params.token).digest('hex'); 118 | 119 | const user = await User.findOne( { 120 | resetPasswordToken, 121 | resetPasswordTokenExpire: { 122 | $gt : Date.now() 123 | } 124 | } ) 125 | 126 | if(!user) { 127 | return next(new ErrorHandler('Password reset token is invalid or expired')); 128 | } 129 | 130 | if( req.body.password !== req.body.confirmPassword) { 131 | return next(new ErrorHandler('Password does not match')); 132 | } 133 | 134 | user.password = req.body.password; 135 | user.resetPasswordToken = undefined; 136 | user.resetPasswordTokenExpire = undefined; 137 | await user.save({validateBeforeSave: false}) 138 | sendToken(user, 201, res) 139 | 140 | }) 141 | 142 | //Get User Profile - /api/v1/myprofile 143 | exports.getUserProfile = catchAsyncError(async (req, res, next) => { 144 | const user = await User.findById(req.user.id) 145 | res.status(200).json({ 146 | success:true, 147 | user 148 | }) 149 | }) 150 | 151 | //Change Password - api/v1/password/change 152 | exports.changePassword = catchAsyncError(async (req, res, next) => { 153 | const user = await User.findById(req.user.id).select('+password'); 154 | //check old password 155 | if(!await user.isValidPassword(req.body.oldPassword)) { 156 | return next(new ErrorHandler('Old password is incorrect', 401)); 157 | } 158 | 159 | //assigning new password 160 | user.password = req.body.password; 161 | await user.save(); 162 | res.status(200).json({ 163 | success:true, 164 | }) 165 | }) 166 | 167 | //Update Profile - /api/v1/update 168 | exports.updateProfile = catchAsyncError(async (req, res, next) => { 169 | let newUserData = { 170 | name: req.body.name, 171 | email: req.body.email 172 | } 173 | 174 | let avatar; 175 | let BASE_URL = process.env.BACKEND_URL; 176 | if(process.env.NODE_ENV === "production"){ 177 | BASE_URL = `${req.protocol}://${req.get('host')}` 178 | } 179 | 180 | if(req.file){ 181 | avatar = `${BASE_URL}/uploads/user/${req.file.originalname}` 182 | newUserData = {...newUserData,avatar } 183 | } 184 | 185 | const user = await User.findByIdAndUpdate(req.user.id, newUserData, { 186 | new: true, 187 | runValidators: true, 188 | }) 189 | 190 | res.status(200).json({ 191 | success: true, 192 | user 193 | }) 194 | 195 | }) 196 | 197 | //Admin: Get All Users - /api/v1/admin/users 198 | exports.getAllUsers = catchAsyncError(async (req, res, next) => { 199 | const users = await User.find(); 200 | res.status(200).json({ 201 | success: true, 202 | users 203 | }) 204 | }) 205 | 206 | //Admin: Get Specific User - api/v1/admin/user/:id 207 | exports.getUser = catchAsyncError(async (req, res, next) => { 208 | const user = await User.findById(req.params.id); 209 | if(!user) { 210 | return next(new ErrorHandler(`User not found with this id ${req.params.id}`)) 211 | } 212 | res.status(200).json({ 213 | success: true, 214 | user 215 | }) 216 | }); 217 | 218 | //Admin: Update User - api/v1/admin/user/:id 219 | exports.updateUser = catchAsyncError(async (req, res, next) => { 220 | const newUserData = { 221 | name: req.body.name, 222 | email: req.body.email, 223 | role: req.body.role 224 | } 225 | 226 | const user = await User.findByIdAndUpdate(req.params.id, newUserData, { 227 | new: true, 228 | runValidators: true, 229 | }) 230 | 231 | res.status(200).json({ 232 | success: true, 233 | user 234 | }) 235 | }) 236 | 237 | //Admin: Delete User - api/v1/admin/user/:id 238 | exports.deleteUser = catchAsyncError(async (req, res, next) => { 239 | const user = await User.findById(req.params.id); 240 | if(!user) { 241 | return next(new ErrorHandler(`User not found with this id ${req.params.id}`)) 242 | } 243 | await user.remove(); 244 | res.status(200).json({ 245 | success: true, 246 | }) 247 | }) 248 | -------------------------------------------------------------------------------- /frontend/src/slices/productSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | 4 | const productSlice = createSlice({ 5 | name: 'product', 6 | initialState: { 7 | loading: false, 8 | product: {}, 9 | isReviewSubmitted: false, 10 | isProductCreated: false, 11 | isProductDeleted: false, 12 | isProductUpdated: false, 13 | isReviewDeleted: false, 14 | reviews: [] 15 | }, 16 | reducers: { 17 | productRequest(state, action){ 18 | return { 19 | ...state, 20 | loading: true 21 | } 22 | }, 23 | productSuccess(state, action){ 24 | return { 25 | ...state, 26 | loading: false, 27 | product: action.payload.product 28 | } 29 | }, 30 | productFail(state, action){ 31 | return { 32 | ...state, 33 | loading: false, 34 | error: action.payload 35 | } 36 | }, 37 | createReviewRequest(state, action){ 38 | return { 39 | ...state, 40 | loading: true 41 | } 42 | }, 43 | createReviewSuccess(state, action){ 44 | return { 45 | ...state, 46 | loading: false, 47 | isReviewSubmitted: true 48 | } 49 | }, 50 | createReviewFail(state, action){ 51 | return { 52 | ...state, 53 | loading: false, 54 | error: action.payload 55 | } 56 | }, 57 | clearReviewSubmitted(state, action) { 58 | return { 59 | ...state, 60 | isReviewSubmitted: false 61 | } 62 | }, 63 | clearError(state, action) { 64 | return{ ...state, 65 | error: null 66 | } 67 | }, 68 | clearProduct(state, action) { 69 | return{ ...state, 70 | product : {} 71 | } 72 | }, 73 | newProductRequest(state, action){ 74 | return { 75 | ...state, 76 | loading: true 77 | } 78 | }, 79 | newProductSuccess(state, action){ 80 | return { 81 | ...state, 82 | loading: false, 83 | product: action.payload.product, 84 | isProductCreated: true 85 | } 86 | }, 87 | newProductFail(state, action){ 88 | return { 89 | ...state, 90 | loading: false, 91 | error: action.payload, 92 | isProductCreated: false 93 | } 94 | }, 95 | clearProductCreated(state, action) { 96 | return { 97 | ...state, 98 | isProductCreated: false 99 | } 100 | }, 101 | newProductRequest(state, action){ 102 | return { 103 | ...state, 104 | loading: true 105 | } 106 | }, 107 | newProductSuccess(state, action){ 108 | return { 109 | ...state, 110 | loading: false, 111 | product: action.payload.product, 112 | isProductCreated: true 113 | } 114 | }, 115 | newProductFail(state, action){ 116 | return { 117 | ...state, 118 | loading: false, 119 | error: action.payload, 120 | isProductCreated: false 121 | } 122 | }, 123 | clearProductCreated(state, action) { 124 | return { 125 | ...state, 126 | isProductCreated: false 127 | } 128 | }, 129 | deleteProductRequest(state, action){ 130 | return { 131 | ...state, 132 | loading: true 133 | } 134 | }, 135 | deleteProductSuccess(state, action){ 136 | return { 137 | ...state, 138 | loading: false, 139 | isProductDeleted: true 140 | } 141 | }, 142 | deleteProductFail(state, action){ 143 | return { 144 | ...state, 145 | loading: false, 146 | error: action.payload, 147 | } 148 | }, 149 | clearProductDeleted(state, action) { 150 | return { 151 | ...state, 152 | isProductDeleted: false 153 | } 154 | }, 155 | 156 | updateProductRequest(state, action){ 157 | return { 158 | ...state, 159 | loading: true 160 | } 161 | }, 162 | updateProductSuccess(state, action){ 163 | return { 164 | ...state, 165 | loading: false, 166 | product: action.payload.product, 167 | isProductUpdated: true 168 | } 169 | }, 170 | updateProductFail(state, action){ 171 | return { 172 | ...state, 173 | loading: false, 174 | error: action.payload, 175 | } 176 | }, 177 | clearProductUpdated(state, action) { 178 | return { 179 | ...state, 180 | isProductUpdated: false 181 | } 182 | }, 183 | 184 | reviewsRequest(state, action){ 185 | return { 186 | ...state, 187 | loading: true 188 | } 189 | }, 190 | reviewsSuccess(state, action){ 191 | return { 192 | ...state, 193 | loading: false, 194 | reviews: action.payload.reviews 195 | } 196 | }, 197 | reviewsFail(state, action){ 198 | return { 199 | ...state, 200 | loading: false, 201 | error: action.payload 202 | } 203 | }, 204 | deleteReviewRequest(state, action){ 205 | return { 206 | ...state, 207 | loading: true 208 | } 209 | }, 210 | deleteReviewSuccess(state, action){ 211 | return { 212 | ...state, 213 | loading: false, 214 | isReviewDeleted: true 215 | } 216 | }, 217 | deleteReviewFail(state, action){ 218 | return { 219 | ...state, 220 | loading: false, 221 | error: action.payload, 222 | } 223 | }, 224 | clearReviewDeleted(state, action) { 225 | return { 226 | ...state, 227 | isReviewDeleted: false 228 | } 229 | }, 230 | 231 | } 232 | }); 233 | 234 | const { actions, reducer } = productSlice; 235 | 236 | export const { 237 | productRequest, 238 | productSuccess, 239 | productFail, 240 | createReviewFail, 241 | createReviewRequest, 242 | createReviewSuccess, 243 | clearError, 244 | clearReviewSubmitted, 245 | clearProduct, 246 | newProductFail, 247 | newProductSuccess, 248 | newProductRequest, 249 | clearProductCreated, 250 | deleteProductFail, 251 | deleteProductRequest, 252 | deleteProductSuccess, 253 | clearProductDeleted, 254 | updateProductFail, 255 | updateProductRequest, 256 | updateProductSuccess, 257 | clearProductUpdated, 258 | reviewsRequest, 259 | reviewsFail, 260 | reviewsSuccess, 261 | deleteReviewFail, 262 | deleteReviewRequest, 263 | deleteReviewSuccess, 264 | clearReviewDeleted 265 | } = actions; 266 | 267 | export default reducer; 268 | 269 | -------------------------------------------------------------------------------- /frontend/src/components/product/ProductSearch.js: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect, useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { getProducts } from "../../actions/productActions"; 4 | import Loader from ".././layouts/Loader"; 5 | import MetaData from ".././layouts/MetaData"; 6 | import Product from ".././product/Product"; 7 | import {toast} from 'react-toastify'; 8 | import Pagination from 'react-js-pagination'; 9 | import { useParams } from "react-router-dom"; 10 | import Slider from "rc-slider"; 11 | import Tooltip from 'rc-tooltip'; 12 | import 'rc-slider/assets/index.css'; 13 | import 'rc-tooltip/assets/bootstrap.css'; 14 | 15 | export default function ProductSearch(){ 16 | const dispatch = useDispatch(); 17 | const {products, loading, error, productsCount, resPerPage} = useSelector((state) => state.productsState) 18 | const [currentPage, setCurrentPage] = useState(1); 19 | const [price, setPrice] = useState([1,1000]); 20 | const [priceChanged, setPriceChanged] = useState(price); 21 | const [category, setCategory] = useState(null); 22 | const [rating, setRating] = useState(0); 23 | 24 | const { keyword } = useParams(); 25 | const categories = [ 26 | 'Electronics', 27 | 'Mobile Phones', 28 | 'Laptops', 29 | 'Accessories', 30 | 'Headphones', 31 | 'Food', 32 | 'Books', 33 | 'Clothes/Shoes', 34 | 'Beauty/Health', 35 | 'Sports', 36 | 'Outdoor', 37 | 'Home' 38 | ]; 39 | 40 | const setCurrentPageNo = (pageNo) =>{ 41 | 42 | setCurrentPage(pageNo) 43 | 44 | } 45 | 46 | useEffect(()=>{ 47 | if(error) { 48 | return toast.error(error,{ 49 | position: toast.POSITION.BOTTOM_CENTER 50 | }) 51 | } 52 | dispatch(getProducts(keyword, priceChanged, category, rating, currentPage)) 53 | }, [error, dispatch, currentPage, keyword, priceChanged, category, rating]) 54 | 55 | 56 | return ( 57 | 58 | {loading ? : 59 | 60 | 61 |

Search Products

62 |
63 |
64 |
65 | {/* Price Filter */} 66 |
setPriceChanged(price)}> 67 | { 79 | setPrice(price) 80 | }} 81 | handleRender={ 82 | renderProps => { 83 | return ( 84 | 85 |
86 |
87 | ) 88 | } 89 | } 90 | /> 91 |
92 |
93 | {/* Category Filter */} 94 |
95 |

Categories

96 |
    97 | {categories.map(category => 98 |
  • { 105 | setCategory(category) 106 | }} 107 | > 108 | {category} 109 |
  • 110 | 111 | )} 112 | 113 |
114 |
115 |
116 | {/* Ratings Filter */} 117 |
118 |

Ratings

119 |
    120 | {[5, 4, 3, 2, 1].map(star => 121 |
  • { 128 | setRating(star) 129 | }} 130 | > 131 |
    132 |
    138 | 139 |
    140 |
    141 |
  • 142 | 143 | )} 144 | 145 |
146 |
147 |
148 |
149 |
150 | { products && products.map(product => ( 151 | 152 | ))} 153 |
154 | 155 |
156 |
157 |
158 | {productsCount > 0 && productsCount > resPerPage? 159 |
160 | 171 |
: null } 172 |
173 | } 174 |
175 | ) 176 | } --------------------------------------------------------------------------------