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

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 |
11 |
by {review.user.name}
12 |
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 |

}
13 |
14 |
15 | {product.name}
16 |
17 |
18 |
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 |
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 |
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 | : 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 |
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 |
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 |
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 |

50 |
51 |
52 |
53 | {item.name}
54 |
55 |
56 |
57 |
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 |

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 |

42 |
43 |
44 |
45 | {item.name}
46 |
47 |
48 |
49 |
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 |
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 |
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 | )
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 |
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 |
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 |

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 |
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 |
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 | : null }
172 |
173 | }
174 |
175 | )
176 | }
--------------------------------------------------------------------------------