├── .vscode
└── settings.json
├── frontend
├── public
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── robots.txt
│ ├── images
│ │ ├── alexa.jpg
│ │ ├── mouse.jpg
│ │ ├── phone.jpg
│ │ ├── airpods.jpg
│ │ ├── camera.jpg
│ │ └── playstation.jpg
│ ├── manifest.json
│ └── index.html
├── src
│ ├── constants
│ │ ├── cartConstants.js
│ │ ├── orderConstants.js
│ │ ├── productConstants.js
│ │ └── userConstants.js
│ ├── components
│ │ ├── Message.js
│ │ ├── Footer.js
│ │ ├── FormContainer.js
│ │ ├── Loader.js
│ │ ├── Product.js
│ │ ├── CheckoutSteps.js
│ │ ├── Rating.js
│ │ └── Header.js
│ ├── reportWebVitals.js
│ ├── index.css
│ ├── index.js
│ ├── screens
│ │ ├── HomeScreen.js
│ │ ├── PaymentScreen.js
│ │ ├── LoginScreen.js
│ │ ├── ShippingScreen.js
│ │ ├── CartScreen.js
│ │ ├── RegisterScreen.js
│ │ ├── ProductScreen.js
│ │ ├── ProfileScreen.js
│ │ ├── OrderScreen.js
│ │ └── PlaceOrderScreen.js
│ ├── reducers
│ │ ├── cartReducers.js
│ │ ├── orderReducers.js
│ │ ├── productReducers.js
│ │ └── userReducers.js
│ ├── actions
│ │ ├── productActions.js
│ │ ├── cartActions.js
│ │ ├── orderActions.js
│ │ └── userActions.js
│ ├── App.js
│ └── store.js
├── debug.log
├── package.json
└── README.md
├── backend
├── utils
│ └── generateToken.js
├── routes
│ ├── productRoutes.js
│ ├── orderRoutes.js
│ └── userRoutes.js
├── data
│ ├── users.js
│ └── products.js
├── config
│ └── db.js
├── middleware
│ ├── errorMiddleware.js
│ └── authMiddleware.js
├── server.js
├── controllers
│ ├── productController.js
│ ├── orderController.js
│ └── userController.js
├── models
│ ├── userModel.js
│ ├── productModel.js
│ └── orderModel.js
└── seeder.js
├── .gitignore
├── README.md
└── package.json
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "git.ignoreLimitWarning": true
3 | }
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proghead00/Teterot/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proghead00/Teterot/HEAD/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proghead00/Teterot/HEAD/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/public/images/alexa.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proghead00/Teterot/HEAD/frontend/public/images/alexa.jpg
--------------------------------------------------------------------------------
/frontend/public/images/mouse.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proghead00/Teterot/HEAD/frontend/public/images/mouse.jpg
--------------------------------------------------------------------------------
/frontend/public/images/phone.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proghead00/Teterot/HEAD/frontend/public/images/phone.jpg
--------------------------------------------------------------------------------
/frontend/public/images/airpods.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proghead00/Teterot/HEAD/frontend/public/images/airpods.jpg
--------------------------------------------------------------------------------
/frontend/public/images/camera.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proghead00/Teterot/HEAD/frontend/public/images/camera.jpg
--------------------------------------------------------------------------------
/frontend/public/images/playstation.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proghead00/Teterot/HEAD/frontend/public/images/playstation.jpg
--------------------------------------------------------------------------------
/backend/utils/generateToken.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 |
3 | const generateToken = (id) => {
4 | return jwt.sign({ id }, process.env.JWT_SECRET, {
5 | expiresIn: "30d",
6 | });
7 | };
8 |
9 | export default generateToken;
10 |
--------------------------------------------------------------------------------
/frontend/src/constants/cartConstants.js:
--------------------------------------------------------------------------------
1 | export const CART_ADD_ITEM = "CART_ADD_ITEM";
2 | export const CART_REMOVE_ITEM = "CART_REMOVE_ITEM";
3 | export const CART_SAVE_SHIPPING_ADDRESS = "CART_SAVE_SHIPPING_ADDRESS";
4 | export const CART_SAVE_PAYMENT_METHOD = "CART_SAVE_PAYMENT_METHOD";
5 |
--------------------------------------------------------------------------------
/backend/routes/productRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | const router = express.Router();
3 | import {
4 | getProductById,
5 | getProducts,
6 | } from "../controllers/productController.js";
7 |
8 | router.route("/").get(getProducts);
9 |
10 | router.route("/:id").get(getProductById);
11 |
12 | export default router;
13 |
--------------------------------------------------------------------------------
/frontend/src/components/Message.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Alert } from "react-bootstrap";
3 |
4 | // children -> text that we want inside 👇
5 | const Message = ({ variant, children }) => {
6 | return {children} ;
7 | };
8 | Message.defaultProps = {
9 | variant: "info",
10 | };
11 | export default Message;
12 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import { Container, Row, Col } from "react-bootstrap";
2 |
3 | const Footer = () => {
4 | return (
5 |
6 |
7 |
8 | Copyright © Teterot
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | export default Footer;
16 |
--------------------------------------------------------------------------------
/frontend/src/constants/orderConstants.js:
--------------------------------------------------------------------------------
1 | export const ORDER_CREATE_REQUEST = "ORDER_CREATE_REQUEST";
2 | export const ORDER_CREATE_SUCCESS = "ORDER_CREATE_SUCCESS";
3 | export const ORDER_CREATE_FAIL = "ORDER_CREATE_FAIL";
4 |
5 | export const ORDER_DETAILS_REQUEST = "ORDER_DETAILS_REQUEST";
6 | export const ORDER_DETAILS_SUCCESS = "ORDER_DETAILS_SUCCESS";
7 | export const ORDER_DETAILS_FAIL = "ORDER_DETAILS_FAIL";
8 |
--------------------------------------------------------------------------------
/frontend/src/components/FormContainer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Row, Col } from "react-bootstrap";
3 |
4 | const FormContainer = ({ children }) => {
5 | return (
6 |
7 |
8 |
9 | {children}
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default FormContainer;
17 |
--------------------------------------------------------------------------------
/frontend/src/constants/productConstants.js:
--------------------------------------------------------------------------------
1 | export const PRODUCT_LIST_REQUEST = "PRODUCT_LIST_REQUEST";
2 | export const PRODUCT_LIST_SUCCESS = "PRODUCT_LIST_SUCCESS";
3 | export const PRODUCT_LIST_FAIL = "PRODUCT_LIST_FAIL";
4 |
5 | export const PRODUCT_DETAILS_REQUEST = "PRODUCT_DETAILS_REQUEST";
6 | export const PRODUCT_DETAILS_SUCCESS = "PRODUCT_DETAILS_SUCCESS";
7 | export const PRODUCT_DETAILS_FAIL = "PRODUCT_DETAILS_FAIL";
8 |
--------------------------------------------------------------------------------
/frontend/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | node_modules/
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
--------------------------------------------------------------------------------
/frontend/src/components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Spinner } from "react-bootstrap";
3 | const Loader = () => {
4 | return (
5 |
15 | Loading...
16 |
17 | );
18 | };
19 |
20 | export default Loader;
21 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Archivo&display=swap");
2 | * {
3 | margin: 0;
4 | padding: 0;
5 | font-family: "Archivo", sans-serif;
6 | }
7 |
8 | main {
9 | min-height: 80vh;
10 | }
11 |
12 | .rating span {
13 | margin: 0.1rem;
14 | }
15 |
16 | h3 {
17 | padding: 1rem 0;
18 | }
19 |
20 | h1 {
21 | font-size: 1.8rem;
22 | padding: 1rem 0;
23 | }
24 | h2 {
25 | font-size: 1.4rem;
26 | padding: 0.5rem 0;
27 | }
28 |
--------------------------------------------------------------------------------
/backend/routes/orderRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | const router = express.Router();
3 | import {
4 | addOrderItems,
5 | getOrderById,
6 | updateOrderToPaid,
7 | } from "../controllers/orderController.js";
8 | import { protect } from "../middleware/authMiddleware.js";
9 |
10 | router.route("/").post(protect, addOrderItems);
11 | router.route("/:id").get(protect, getOrderById);
12 | router.route("/:id/pay").put(protect, updateOrderToPaid);
13 |
14 | export default router;
15 |
--------------------------------------------------------------------------------
/backend/data/users.js:
--------------------------------------------------------------------------------
1 | import bcrypt from "bcryptjs";
2 |
3 | const users = [
4 | {
5 | name: "Admin User",
6 | email: "admin@example.com",
7 | password: bcrypt.hashSync("123456", 10),
8 | isAdmin: true,
9 | },
10 | {
11 | name: "John Doe",
12 | email: "john@example.com",
13 | password: bcrypt.hashSync("123456", 10),
14 | },
15 | {
16 | name: "Jane Doe",
17 | email: "jane@example.com",
18 | password: bcrypt.hashSync("123456", 10),
19 | },
20 | ];
21 |
22 | export default users;
23 |
--------------------------------------------------------------------------------
/backend/routes/userRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | const router = express.Router();
3 | import {
4 | authUser,
5 | registerUser,
6 | getUserProfile,
7 | updateUserProfile,
8 | } from "../controllers/userController.js";
9 | import { protect } from "../middleware/authMiddleware.js";
10 |
11 | router.route("/").post(registerUser);
12 | router.post("/login", authUser);
13 | router
14 | .route("/profile")
15 | .get(protect, getUserProfile)
16 | .put(protect, updateUserProfile);
17 |
18 | export default router;
19 |
--------------------------------------------------------------------------------
/backend/config/db.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const connectDB = async () => {
4 | try {
5 | const conn = await mongoose.connect(process.env.MONGO_URI, {
6 | // set of options:
7 | useUnifiedTopology: true,
8 | useNewUrlParser: true,
9 | useCreateIndex: true,
10 | });
11 |
12 | console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline);
13 | } catch (error) {
14 | console.error(`Error: ${error.message}`.red.underline.bold);
15 |
16 | process.exit(1); // exit with failure
17 | }
18 | };
19 |
20 | export default connectDB;
21 |
--------------------------------------------------------------------------------
/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/middleware/errorMiddleware.js:
--------------------------------------------------------------------------------
1 | const notFound = (req, res, next) => {
2 | const error = new Error(`Not found -> ${req.originalUrl}`);
3 | res.status(404);
4 | next(error);
5 | };
6 |
7 | // error middleware -> override the default error handler
8 | const errorHandler = (err, req, res, next) => {
9 | const statusCode = res.statusCode === 200 ? 500 : res.statusCode; // 500 -> server error
10 | res.status(statusCode);
11 | res.json({
12 | message: err.message,
13 |
14 | // stack trace
15 | stack: process.env.NODE_ENV === "production" ? null : err.stack,
16 | });
17 | };
18 |
19 | export { notFound, errorHandler };
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Teterot (under development)
2 | ## A MERN stack e-commerce project 🛒
3 |
4 | ## Features (including the ones to be added):
5 |
6 | - Full featured shopping cart
7 | - Product reviews and ratings - Registered users can post their reviews under the products
8 | - Top products carousel
9 | - Product pagination
10 | - Product search feature
11 | - User profile with orders list
12 | - Admin product management
13 | - Admin user management
14 | - Admin order details page
15 | - Mark orders as delivered option
16 | - Checkout process (shipping, payment method, etc)
17 | - PayPal / credit card integration
18 | - Database seeder (products & users)
19 |
--------------------------------------------------------------------------------
/frontend/debug.log:
--------------------------------------------------------------------------------
1 | [1115/134908.330:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
2 | [1120/111853.260:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
3 | [1120/202238.320:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
4 | [1121/185530.101:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
5 | [1122/185241.535:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
6 | [1123/201721.281:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
7 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./bootstrap.min.css";
4 | import { Provider } from "react-redux";
5 | import store from "./store";
6 | import "./index.css";
7 | import App from "./App";
8 | import reportWebVitals from "./reportWebVitals";
9 |
10 | ReactDOM.render(
11 | // Provider takes in store 👇
12 |
13 |
14 | ,
15 | document.getElementById("root")
16 | );
17 |
18 | // If you want to start measuring performance in your app, pass a function
19 | // to log results (for example: reportWebVitals(console.log))
20 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
21 | reportWebVitals();
22 |
--------------------------------------------------------------------------------
/frontend/src/components/Product.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Card } from "react-bootstrap";
3 | import Rating from "./Rating";
4 | import { Link } from "react-router-dom";
5 | const Product = ({ product }) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {product.name}
15 |
16 |
17 |
18 |
22 |
23 |
24 | ${product.price}
25 |
26 |
27 | );
28 | };
29 |
30 | export default Product;
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mern-ecommerce-app",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "type": "module",
7 | "scripts": {
8 | "start": "node backend/server",
9 | "server": "nodemon backend/server",
10 | "client": "npm start --prefix frontend",
11 | "dev": "concurrently \"npm run server\" \"npm run client\"",
12 | "data:import": "node backend/seeder",
13 | "data:destroy": "node backend/seeder -d"
14 | },
15 | "author": "Susnata Goswami",
16 | "license": "MIT",
17 | "dependencies": {
18 | "bcryptjs": "^2.4.3",
19 | "colors": "^1.4.0",
20 | "dotenv": "^8.2.0",
21 | "express": "^4.17.1",
22 | "express-async-handler": "^1.1.4",
23 | "jsonwebtoken": "^8.5.1",
24 | "mongoose": "^5.10.14"
25 | },
26 | "devDependencies": {
27 | "concurrently": "^5.3.0",
28 | "nodemon": "^2.0.6"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/src/constants/userConstants.js:
--------------------------------------------------------------------------------
1 | export const USER_LOGIN_REQUEST = " USER_LOGIN_REQUEST";
2 | export const USER_LOGIN_SUCCESS = " USER_LOGIN_SUCCESS";
3 | export const USER_LOGIN_FAIL = " USER_LOGIN_FAIL";
4 | export const USER_LOGOUT = " USER_LOGOUT";
5 |
6 | export const USER_REGISTER_REQUEST = " USER_REGISTER_REQUEST";
7 | export const USER_REGISTER_SUCCESS = " USER_REGISTER_SUCCESS";
8 | export const USER_REGISTER_FAIL = " USER_REGISTER_FAIL";
9 |
10 | export const USER_DETAILS_REQUEST = " USER_DETAILS_REQUEST";
11 | export const USER_DETAILS_SUCCESS = " USER_DETAILS_SUCCESS";
12 | export const USER_DETAILS_FAIL = " USER_DETAILS_FAIL";
13 |
14 | export const USER_UPDATE_PROFILE_REQUEST = " USER_UPDATE_PROFILE_REQUEST";
15 | export const USER_UPDATE_PROFILE_SUCCESS = " USER_UPDATE_PROFILE_SUCCESS";
16 | export const USER_UPDATE_PROFILE_FAIL = " USER_UPDATE_PROFILE_FAIL";
17 | export const USER_UPDATE_PROFILE_RESET = " USER_UPDATE_PROFILE_RESET";
18 |
--------------------------------------------------------------------------------
/backend/middleware/authMiddleware.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import User from "../models/userModel.js";
3 | import asyncHandler from "express-async-handler";
4 |
5 | // middleware
6 | const protect = asyncHandler(async (req, res, next) => {
7 | let token;
8 |
9 | if (
10 | req.headers.authorization &&
11 | req.headers.authorization.startsWith("Bearer")
12 | ) {
13 | try {
14 | token = req.headers.authorization.split(" ")[1];
15 |
16 | const decoded = jwt.verify(token, process.env.JWT_SECRET);
17 | // console.log(decoded);
18 |
19 | req.user = await User.findById(decoded.id).select("-password"); // don't return password
20 |
21 | next();
22 | } catch (error) {
23 | console.error(error);
24 | res.status(401);
25 | throw new Error("Not authorized, token failed");
26 | }
27 | }
28 |
29 | if (!token) {
30 | res.status(401);
31 | throw new Error("Not authorized, no token");
32 | }
33 | });
34 |
35 | export { protect };
36 |
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import dotenv from "dotenv";
3 | import connectDB from "./config/db.js";
4 | import colors from "colors";
5 | import { notFound, errorHandler } from "./middleware/errorMiddleware.js";
6 | import productRoutes from "./routes/productRoutes.js";
7 | import userRoutes from "./routes/userRoutes.js";
8 | import orderRoutes from "./routes/orderRoutes.js";
9 |
10 | dotenv.config();
11 |
12 | connectDB();
13 |
14 | const app = express();
15 |
16 | // middleware for req.body to parse (body-parser)
17 | app.use(express.json());
18 |
19 | app.get("/", (req, res) => {
20 | res.send("API RUNNING");
21 | });
22 |
23 | app.use("/api/products", productRoutes);
24 | app.use("/api/users", userRoutes);
25 | app.use("/api/orders", orderRoutes);
26 |
27 | app.use(notFound);
28 |
29 | app.use(errorHandler);
30 |
31 | const PORT = process.env.PORT || 5000;
32 |
33 | app.listen(
34 | PORT,
35 | console.log(`Server in ${process.env.NODE_ENV} on ${PORT}`.yellow.bold)
36 | );
37 |
--------------------------------------------------------------------------------
/backend/controllers/productController.js:
--------------------------------------------------------------------------------
1 | import Product from "../models/productModel.js";
2 | import asyncHandler from "express-async-handler";
3 |
4 | // @description Fetch all products
5 | // @route GET /api/products
6 | // @access Public
7 | const getProducts = asyncHandler(async (req, res) => {
8 | const products = await Product.find({}); // empty object gives everything
9 |
10 | res.json(products);
11 | });
12 |
13 | // @description Fetch single product
14 | // @route GET /api/product/:id
15 | // @access Public
16 | const getProductById = asyncHandler(async (req, res) => {
17 | const product = await Product.findById(req.params.id); // gives the id in url
18 |
19 | // checking if there's a product
20 | if (product) {
21 | res.json(product);
22 | } else {
23 | // to get the error -> has to be in FORMATTED ID but ain't in the DB
24 | res.status(404);
25 | throw new Error("Product not found");
26 | }
27 | });
28 |
29 | export { getProducts, getProductById };
30 |
--------------------------------------------------------------------------------
/backend/models/userModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import bcrypt from "bcryptjs";
3 |
4 | const userSchema = mongoose.Schema(
5 | {
6 | name: {
7 | type: String,
8 | required: true,
9 | },
10 | email: {
11 | type: String,
12 | required: true,
13 | unique: true,
14 | },
15 | password: {
16 | type: String,
17 | required: true,
18 | },
19 | isAdmin: {
20 | type: Boolean,
21 | required: true,
22 | default: false,
23 | },
24 | },
25 | {
26 | timestamps: true,
27 | }
28 | );
29 |
30 | userSchema.methods.matchPassword = async function (enteredPassword) {
31 | // compare plain text to encrypted password
32 | return await bcrypt.compare(enteredPassword, this.password);
33 | };
34 |
35 | // middleware
36 | userSchema.pre("save", async function (next) {
37 | // before saving, run the async function
38 | if (!this.isModified("password")) {
39 | // mongoose stuff-> .isModified
40 |
41 | next(); // move on
42 | }
43 |
44 | // encrypt password
45 | const salt = await bcrypt.genSalt(10);
46 | this.password = await bcrypt.hash(this.password, salt);
47 | });
48 | const User = mongoose.model("User", userSchema); // creating a model from this schema
49 |
50 | export default User;
51 |
--------------------------------------------------------------------------------
/frontend/src/screens/HomeScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Row, Col } from "react-bootstrap";
4 | import Product from "../components/Product";
5 | import Message from "../components/Message";
6 | import Loader from "../components/Loader";
7 | import { listProducts } from "../actions/productActions";
8 |
9 | const HomeScreen = () => {
10 | const dispatch = useDispatch();
11 |
12 | const productList = useSelector((state) => state.productList);
13 | //destructuring to get what we want from product list
14 | //coming from productReducers.js 👇
15 | const { loading, error, products } = productList;
16 |
17 | // firing off the action
18 | useEffect(() => {
19 | dispatch(listProducts());
20 | }, [dispatch]);
21 |
22 | return (
23 |
24 |
Latest Products
25 | {loading ? (
26 |
27 | ) : error ? (
28 |
{error}
29 | ) : (
30 |
31 | {products.map((product) => (
32 |
33 |
34 |
35 | ))}
36 |
37 | )}
38 |
39 | );
40 | };
41 |
42 | export default HomeScreen;
43 |
--------------------------------------------------------------------------------
/frontend/src/reducers/cartReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | CART_ADD_ITEM,
3 | CART_REMOVE_ITEM,
4 | CART_SAVE_SHIPPING_ADDRESS,
5 | CART_SAVE_PAYMENT_METHOD,
6 | } from "../constants/cartConstants";
7 |
8 | export const cartReducer = (
9 | state = { cartItems: [], shippingAddress: {} },
10 | action
11 | ) => {
12 | switch (action.type) {
13 | case CART_ADD_ITEM:
14 | const item = action.payload;
15 |
16 | const existItem = state.cartItems.find((x) => x.product === item.product);
17 |
18 | if (existItem) {
19 | return {
20 | ...state,
21 | cartItems: state.cartItems.map((x) =>
22 | x.product === existItem.product ? item : x
23 | ), // existItem.product is the id
24 | };
25 | } else {
26 | return {
27 | ...state,
28 | cartItems: [...state.cartItems, item],
29 | };
30 | }
31 | case CART_REMOVE_ITEM:
32 | return {
33 | ...state,
34 | cartItems: state.cartItems.filter((x) => x.product !== action.payload),
35 | };
36 |
37 | case CART_SAVE_SHIPPING_ADDRESS:
38 | return {
39 | ...state,
40 | shippingAddress: action.payload,
41 | };
42 | case CART_SAVE_PAYMENT_METHOD:
43 | return {
44 | ...state,
45 | paymentMethod: action.payload,
46 | };
47 |
48 | default:
49 | return state;
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/frontend/src/reducers/orderReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | ORDER_CREATE_REQUEST,
3 | ORDER_CREATE_SUCCESS,
4 | ORDER_CREATE_FAIL,
5 | ORDER_DETAILS_REQUEST,
6 | ORDER_DETAILS_SUCCESS,
7 | ORDER_DETAILS_FAIL,
8 | } from "../constants/orderConstants";
9 |
10 | export const orderCreateReducers = (state = {}, action) => {
11 | switch (action.type) {
12 | case ORDER_CREATE_REQUEST:
13 | return {
14 | loading: true,
15 | };
16 | case ORDER_CREATE_SUCCESS:
17 | return {
18 | loading: false,
19 | success: true,
20 | order: action.payload,
21 | };
22 | case ORDER_CREATE_FAIL:
23 | return {
24 | loading: false,
25 | error: action.payload,
26 | };
27 |
28 | default:
29 | return state;
30 | }
31 | };
32 |
33 | export const orderDetailsReducer = (
34 | state = { loading: true, orderItems: [], shippingAddress: {} },
35 | action
36 | ) => {
37 | switch (action.type) {
38 | case ORDER_DETAILS_REQUEST:
39 | return {
40 | ...state,
41 | loading: true,
42 | };
43 | case ORDER_DETAILS_SUCCESS:
44 | return {
45 | loading: false,
46 |
47 | order: action.payload,
48 | };
49 | case ORDER_DETAILS_FAIL:
50 | return {
51 | loading: false,
52 | error: action.payload,
53 | };
54 |
55 | default:
56 | return state;
57 | }
58 | };
59 |
--------------------------------------------------------------------------------
/frontend/src/reducers/productReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | PRODUCT_LIST_REQUEST,
3 | PRODUCT_LIST_SUCCESS,
4 | PRODUCT_LIST_FAIL,
5 | PRODUCT_DETAILS_REQUEST,
6 | PRODUCT_DETAILS_SUCCESS,
7 | PRODUCT_DETAILS_FAIL,
8 | } from "../constants/productConstants";
9 |
10 | // bunch of functions related to products 👇
11 |
12 | export const productListReducer = (state = { products: [] }, action) => {
13 | // evaluate the type in action object
14 | switch (action.type) {
15 | case PRODUCT_LIST_REQUEST:
16 | return { loading: true, products: [] };
17 |
18 | case PRODUCT_LIST_SUCCESS:
19 | return { loading: false, products: action.payload };
20 |
21 | case PRODUCT_LIST_FAIL:
22 | return { loading: false, error: action.payload };
23 |
24 | default:
25 | return state;
26 | }
27 | };
28 |
29 | export const productDetailsReducer = (
30 | state = { product: { reviews: [] } },
31 | action
32 | ) => {
33 | // evaluate the type in action object
34 | switch (action.type) {
35 | case PRODUCT_DETAILS_REQUEST:
36 | return { loading: true, ...state };
37 |
38 | case PRODUCT_DETAILS_SUCCESS:
39 | return { loading: false, product: action.payload };
40 |
41 | case PRODUCT_DETAILS_FAIL:
42 | return { loading: false, error: action.payload };
43 |
44 | default:
45 | return state;
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "proxy": "http://127.0.0.1:5000",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {
7 | "@testing-library/jest-dom": "^5.11.5",
8 | "@testing-library/react": "^11.1.2",
9 | "@testing-library/user-event": "^12.2.2",
10 | "axios": "^0.21.0",
11 | "react": "^17.0.1",
12 | "react-bootstrap": "^1.4.0",
13 | "react-dom": "^17.0.1",
14 | "react-redux": "^7.2.2",
15 | "react-router-bootstrap": "^0.25.0",
16 | "react-router-dom": "^5.2.0",
17 | "react-scripts": "4.0.0",
18 | "redux": "^4.0.5",
19 | "redux-devtools-extension": "^2.13.8",
20 | "redux-thunk": "^2.3.0",
21 | "web-vitals": "^0.2.4"
22 | },
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "test": "react-scripts test",
27 | "eject": "react-scripts eject"
28 | },
29 | "eslintConfig": {
30 | "extends": [
31 | "react-app",
32 | "react-app/jest"
33 | ]
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/frontend/src/components/CheckoutSteps.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Nav } from "react-bootstrap";
3 | import { LinkContainer } from "react-router-bootstrap";
4 |
5 | const CheckoutSteps = ({ step1, step2, step3, step4 }) => {
6 | return (
7 |
8 |
9 | {step1 ? (
10 |
11 | Sign In
12 |
13 | ) : (
14 | Sign In
15 | )}
16 |
17 |
18 |
19 | {step2 ? (
20 |
21 | Shipping
22 |
23 | ) : (
24 | Shipping
25 | )}
26 |
27 |
28 |
29 | {step3 ? (
30 |
31 | Payment
32 |
33 | ) : (
34 | Payment
35 | )}
36 |
37 |
38 |
39 | {step4 ? (
40 |
41 | Place order
42 |
43 | ) : (
44 | Place order
45 | )}
46 |
47 |
48 | );
49 | };
50 |
51 | export default CheckoutSteps;
52 |
--------------------------------------------------------------------------------
/frontend/src/actions/productActions.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | import {
4 | PRODUCT_LIST_REQUEST,
5 | PRODUCT_LIST_SUCCESS,
6 | PRODUCT_LIST_FAIL,
7 | PRODUCT_DETAILS_REQUEST,
8 | PRODUCT_DETAILS_SUCCESS,
9 | PRODUCT_DETAILS_FAIL,
10 | } from "../constants/productConstants";
11 |
12 | // actual actions; think of them as action creators 👇
13 | export const listProducts = () => async (dispatch) => {
14 | try {
15 | dispatch({ type: PRODUCT_LIST_REQUEST }); // this will call in the reducer -> set loading to true, and products will still be empty
16 | const { data } = await axios.get("/api/products");
17 |
18 | dispatch({
19 | type: PRODUCT_LIST_SUCCESS,
20 | payload: data,
21 | });
22 | } catch (error) {
23 | dispatch({
24 | type: PRODUCT_LIST_FAIL,
25 | payload:
26 | error.response && error.response.data.message
27 | ? error.response.data.message
28 | : error.message,
29 | });
30 | }
31 | };
32 |
33 | export const listProductDetails = (id) => async (dispatch) => {
34 | try {
35 | dispatch({ type: PRODUCT_DETAILS_REQUEST });
36 |
37 | const { data } = await axios.get(`/api/products/${id}`);
38 |
39 | dispatch({
40 | type: PRODUCT_DETAILS_SUCCESS,
41 | payload: data,
42 | });
43 | } catch (error) {
44 | dispatch({
45 | type: PRODUCT_DETAILS_FAIL,
46 | payload:
47 | error.response && error.response.data.message
48 | ? error.response.data.message
49 | : error.message,
50 | });
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/frontend/src/actions/cartActions.js:
--------------------------------------------------------------------------------
1 | import axios from "axios"; // when we add an item to the cart, we need to make request to api/products/:id to get the data for the particular product to add to the cart
2 | import {
3 | CART_ADD_ITEM,
4 | CART_REMOVE_ITEM,
5 | CART_SAVE_SHIPPING_ADDRESS,
6 | CART_SAVE_PAYMENT_METHOD,
7 | } from "../constants/cartConstants";
8 |
9 | export const addToCart = (id, qty) => async (dispatch, getState) => {
10 | // used thunk -> hence async(dispatch)
11 | const { data } = await axios.get(`/api/products/${id}`);
12 |
13 | dispatch({
14 | type: CART_ADD_ITEM,
15 | payload: {
16 | product: data._id,
17 | name: data.name,
18 | image: data.image,
19 | price: data.price,
20 | countInStock: data.countInStock,
21 | qty,
22 | },
23 | });
24 |
25 | localStorage.setItem("cartItems", JSON.stringify(getState().cart.cartItems));
26 | };
27 |
28 | export const removeFromCart = (id) => (dispatch, getState) => {
29 | dispatch({
30 | type: CART_REMOVE_ITEM,
31 | payload: id,
32 | });
33 | localStorage.setItem("cartItems", JSON.stringify(getState().cart.cartItems));
34 | };
35 |
36 | export const saveShippingAddress = (data) => (dispatch) => {
37 | dispatch({
38 | type: CART_SAVE_SHIPPING_ADDRESS,
39 | payload: data,
40 | });
41 | localStorage.setItem("shippingAddress", JSON.stringify(data));
42 | };
43 |
44 | export const savePaymentMethod = (data) => (dispatch) => {
45 | dispatch({
46 | type: CART_SAVE_PAYMENT_METHOD,
47 | payload: data,
48 | });
49 | localStorage.setItem("paymentMethod", JSON.stringify(data));
50 | };
51 |
--------------------------------------------------------------------------------
/frontend/src/components/Rating.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Rating = ({ value, text, color }) => {
4 | return (
5 |
6 |
7 | = 1
11 | ? "fas fa-star"
12 | : value >= 0.5
13 | ? "fas fa-star-half-alt"
14 | : "far fa-star"
15 | }
16 | >
17 |
18 |
19 | = 2
23 | ? "fas fa-star"
24 | : value >= 1.5
25 | ? "fas fa-star-half-alt"
26 | : "far fa-star"
27 | }
28 | >
29 |
30 |
31 | = 3
35 | ? "fas fa-star"
36 | : value >= 2.5
37 | ? "fas fa-star-half-alt"
38 | : "far fa-star"
39 | }
40 | >
41 |
42 |
43 | = 4
47 | ? "fas fa-star"
48 | : value >= 3.5
49 | ? "fas fa-star-half-alt"
50 | : "far fa-star"
51 | }
52 | >
53 |
54 |
55 | = 5
59 | ? "fas fa-star"
60 | : value >= 4.5
61 | ? "fas fa-star-half-alt"
62 | : "far fa-star"
63 | }
64 | >
65 |
66 | {text && text}
67 |
68 | );
69 | };
70 |
71 | Rating.defaultProps = {
72 | color: "#f1c40f",
73 | };
74 |
75 | export default Rating;
76 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import { BrowserRouter as Router, Route } from "react-router-dom";
2 |
3 | import Header from "./components/Header";
4 | import Footer from "./components/Footer";
5 | import { Container } from "react-bootstrap";
6 | import HomeScreen from "./screens/HomeScreen";
7 | import ProductScreen from "./screens/ProductScreen";
8 | import CartScreen from "./screens/CartScreen";
9 | import LoginScreen from "./screens/LoginScreen";
10 | import RegisterScreen from "./screens/RegisterScreen";
11 | import ProfileScreen from "./screens/ProfileScreen";
12 | import ShippingScreen from "./screens/ShippingScreen";
13 | import PaymentScreen from "./screens/PaymentScreen";
14 | import PlaceOrderScreen from "./screens/PlaceOrderScreen";
15 | import OrderScreen from "./screens/OrderScreen";
16 |
17 | function App() {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
41 | export default App;
42 |
--------------------------------------------------------------------------------
/backend/models/productModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const reviewSchema = mongoose.Schema(
4 | {
5 | name: { type: String, required: true },
6 | rating: { type: String, required: true }, // individual review rating
7 | comment: { type: String, required: true },
8 | },
9 | {
10 | timestamps: true,
11 | }
12 | );
13 |
14 | const productSchema = mongoose.Schema(
15 | {
16 | user: {
17 | type: mongoose.Schema.Types.ObjectId, // which admin has created which product
18 | required: true,
19 | ref: "User", // referencing a specific model -> Adds a relationship b/w product and the User
20 | },
21 | name: {
22 | type: String,
23 | required: true,
24 | },
25 | image: {
26 | type: String,
27 | required: true,
28 | },
29 |
30 | //brand of the product
31 | brand: {
32 | type: String,
33 | required: true,
34 | },
35 |
36 | category: {
37 | type: String,
38 | required: true,
39 | default: false,
40 | },
41 | description: {
42 | type: String,
43 | required: true,
44 | default: false,
45 | },
46 |
47 | reviews: [reviewSchema],
48 |
49 | // avg. rating of all the ratings in the reviews
50 | rating: {
51 | type: Number,
52 | required: true,
53 | default: 0,
54 | },
55 |
56 | // tracking the no. of reviews
57 | numReviews: {
58 | type: Number,
59 | required: true,
60 | default: 0,
61 | },
62 | price: {
63 | type: Number,
64 | required: true,
65 | default: 0,
66 | },
67 | countInStock: {
68 | type: Number,
69 | required: true,
70 | default: 0,
71 | },
72 | },
73 | {
74 | timestamps: true,
75 | }
76 | );
77 |
78 | const Product = mongoose.model("Product", productSchema); // creating a model from this schema
79 |
80 | export default Product;
81 |
--------------------------------------------------------------------------------
/frontend/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Navbar, Nav, Container, NavDropdown } from "react-bootstrap";
3 | import { LinkContainer } from "react-router-bootstrap";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import { logout } from "../actions/userActions";
6 |
7 | const Header = () => {
8 | const dispatch = useDispatch();
9 |
10 | const userLogin = useSelector((state) => state.userLogin);
11 | const { userInfo } = userLogin;
12 |
13 | const logoutHandler = () => {
14 | dispatch(logout());
15 | };
16 |
17 | return (
18 |
19 |
25 |
26 |
27 | Teterot
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {" "}
36 | Cart{" "}
37 |
38 |
39 | {userInfo ? (
40 |
41 |
42 | Profile
43 |
44 |
45 | Logout
46 |
47 |
48 | ) : (
49 |
50 |
51 |
52 | Sign In
53 |
54 |
55 | )}
56 |
57 |
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default Header;
65 |
--------------------------------------------------------------------------------
/frontend/src/store.js:
--------------------------------------------------------------------------------
1 | // connect all reducers, middlewares, etc
2 |
3 | import { createStore, combineReducers, applyMiddleware } from "redux";
4 | import thunk from "redux-thunk";
5 | import { composeWithDevTools } from "redux-devtools-extension";
6 | import {
7 | productListReducer,
8 | productDetailsReducer,
9 | } from "./reducers/productReducers";
10 |
11 | import { cartReducer } from "./reducers/cartReducers";
12 | import {
13 | userLoginReducer,
14 | userRegisterReducer,
15 | userDetailsReducer,
16 | userUpdateProfileReducer,
17 | } from "./reducers/userReducers";
18 |
19 | import {
20 | orderCreateReducers,
21 | orderDetailsReducer,
22 | } from "./reducers/orderReducers";
23 |
24 | const reducer = combineReducers({
25 | productList: productListReducer,
26 | productDetails: productDetailsReducer,
27 | cart: cartReducer,
28 | userLogin: userLoginReducer,
29 | userRegister: userRegisterReducer,
30 | userDetails: userDetailsReducer,
31 | userUpdateProfile: userUpdateProfileReducer,
32 | orderCreate: orderCreateReducers,
33 | orderDetails: orderDetailsReducer,
34 | });
35 |
36 | const cartItemsFromStorage = localStorage.getItem("cartItems")
37 | ? JSON.parse(localStorage.getItem("cartItems"))
38 | : [];
39 |
40 | const userInfoFromStorage = localStorage.getItem("userInfo")
41 | ? JSON.parse(localStorage.getItem("userInfo"))
42 | : [];
43 |
44 | const shippingAddressFromStorage = localStorage.getItem("shippingAddress")
45 | ? JSON.parse(localStorage.getItem("shippingAddress"))
46 | : {};
47 |
48 | const initialState = {
49 | cart: {
50 | cartItems: cartItemsFromStorage,
51 | shippingAddress: shippingAddressFromStorage,
52 | },
53 | userLogin: { userInfo: userInfoFromStorage },
54 | };
55 |
56 | const middleware = [thunk];
57 |
58 | const store = createStore(
59 | reducer,
60 | initialState,
61 | composeWithDevTools(applyMiddleware(...middleware))
62 | );
63 |
64 | export default store;
65 |
--------------------------------------------------------------------------------
/frontend/src/screens/PaymentScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Form, Button, Col } from "react-bootstrap";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import FormContainer from "../components/FormContainer";
5 | import CheckoutSteps from "../components/CheckoutSteps";
6 | import { savePaymentMethod } from "../actions/cartActions";
7 |
8 | const PaymentScreen = ({ history }) => {
9 | const cart = useSelector((state) => state.cart);
10 | const { shippingAddress } = cart;
11 |
12 | if (!shippingAddress.address) {
13 | history.push("/shipping");
14 | }
15 |
16 | const [paymentMethod, setPaymentMethod] = useState("PayPal");
17 |
18 | const dispatch = useDispatch();
19 |
20 | const submitHandler = (e) => {
21 | e.preventDefault();
22 | dispatch(savePaymentMethod(paymentMethod));
23 | history.push("/placeorder");
24 | };
25 |
26 | return (
27 |
28 |
29 | Payment Method
30 |
32 | Select Method
33 |
34 | setPaymentMethod(e.target.value)}
42 | >
43 | {/* setPaymentMethod(e.target.value)}
50 | > */}
51 |
52 |
53 |
54 |
55 | Continue
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | export default PaymentScreen;
63 |
--------------------------------------------------------------------------------
/backend/models/orderModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const orderSchema = mongoose.Schema(
4 | {
5 | user: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | required: true,
8 | ref: "User",
9 | },
10 | orderItems: [
11 | {
12 | name: { type: String, required: true }, // name of the product
13 | qty: { type: Number, required: true },
14 | image: { type: String, required: true },
15 | price: { type: Number, required: true },
16 | product: {
17 | type: mongoose.Schema.Types.ObjectId,
18 | required: true,
19 | ref: "Product",
20 | },
21 | },
22 | ],
23 |
24 | shippingAddress: {
25 | address: { type: String, required: true },
26 | city: { type: String, required: true },
27 | postalCode: { type: String, required: true },
28 | country: { type: String, required: true },
29 | },
30 |
31 | // scalable payment method(s)
32 | paymentMethod: {
33 | type: String,
34 | required: true,
35 | },
36 |
37 | // payment result comes from paypal -> when successful, we get some data back
38 | paymentResult: {
39 | id: { type: String },
40 | status: { type: String },
41 | updateTime: { type: String },
42 | emailAddress: { type: String }, // paypal email
43 | },
44 | taxPrice: {
45 | type: Number,
46 | required: true,
47 | default: 0.0,
48 | },
49 | shippingPrice: {
50 | type: Number,
51 | required: true,
52 | default: 0.0,
53 | },
54 | totalPrice: {
55 | type: Number,
56 | required: true,
57 | default: 0.0,
58 | },
59 | isPaid: {
60 | type: Boolean,
61 | required: true,
62 | default: false,
63 | },
64 |
65 | paidAt: {
66 | type: Date,
67 | },
68 | isDelivered: {
69 | type: Boolean,
70 | required: true,
71 | default: false,
72 | },
73 | deliveredAt: {
74 | type: Date,
75 | },
76 | },
77 | {
78 | timestamps: true,
79 | }
80 | );
81 |
82 | const Order = mongoose.model("Order", orderSchema); // creating a model from this schema
83 |
84 | export default Order;
85 |
--------------------------------------------------------------------------------
/frontend/src/actions/orderActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | ORDER_CREATE_REQUEST,
3 | ORDER_CREATE_SUCCESS,
4 | ORDER_CREATE_FAIL,
5 | ORDER_DETAILS_REQUEST,
6 | ORDER_DETAILS_SUCCESS,
7 | ORDER_DETAILS_FAIL,
8 | } from "../constants/orderConstants";
9 | import axios from "axios";
10 |
11 | export const createOrder = (order) => async (dispatch, getState) => {
12 | // getState is required because token is needed to be sent
13 | try {
14 | dispatch({
15 | type: ORDER_CREATE_REQUEST,
16 | });
17 |
18 | const {
19 | userLogin: { userInfo },
20 | } = getState(); // userInfo is in userLogin
21 |
22 | const config = {
23 | headers: {
24 | "Content-Type": "application/json",
25 |
26 | //pass token
27 | Authorization: `Bearer ${userInfo.token}`,
28 | },
29 | };
30 |
31 | const { data } = await axios.post(`/api/orders`, order, config);
32 |
33 | dispatch({
34 | type: ORDER_CREATE_SUCCESS,
35 | payload: data,
36 | });
37 | } catch (error) {
38 | dispatch({
39 | type: ORDER_CREATE_FAIL,
40 | payload:
41 | error.response && error.response.data.message
42 | ? error.response.data.message
43 | : error.message,
44 | });
45 | }
46 | };
47 |
48 | export const getOrderDetails = (id) => async (dispatch, getState) => {
49 | // getState is required because token is needed to be sent
50 | try {
51 | dispatch({
52 | type: ORDER_DETAILS_REQUEST,
53 | });
54 |
55 | const {
56 | userLogin: { userInfo },
57 | } = getState(); // userInfo is in userLogin
58 |
59 | const config = {
60 | headers: {
61 | //pass token
62 | Authorization: `Bearer ${userInfo.token}`,
63 | },
64 | };
65 |
66 | const { data } = await axios.get(`/api/orders/${id}`, config);
67 |
68 | dispatch({
69 | type: ORDER_DETAILS_SUCCESS,
70 | payload: data,
71 | });
72 | } catch (error) {
73 | dispatch({
74 | type: ORDER_DETAILS_FAIL,
75 | payload:
76 | error.response && error.response.data.message
77 | ? error.response.data.message
78 | : error.message,
79 | });
80 | }
81 | };
82 |
--------------------------------------------------------------------------------
/backend/seeder.js:
--------------------------------------------------------------------------------
1 | // seeder script helps to import data
2 |
3 | // 👉 THIS IS NOT AT ALL CONNECTED TO SERVER 👈
4 |
5 | import mongoose from "mongoose";
6 | import dotenv from "dotenv";
7 | import colors from "colors";
8 | import users from "./data/users.js";
9 | import products from "./data/products.js";
10 | import User from "./models/userModel.js";
11 | import Product from "./models/productModel.js";
12 | import Order from "./models/orderModel.js";
13 | import connectDB from "./config/db.js";
14 |
15 | dotenv.config();
16 |
17 | connectDB();
18 |
19 | const importData = async () => {
20 | try {
21 | await Order.deleteMany(); // if nothing is passed in paramters, it's gonna delete everything
22 | await Product.deleteMany(); // if nothing is passed in paramters, it's gonna delete everything
23 | await User.deleteMany(); // if nothing is passed in paramters, it's gonna delete everything
24 |
25 | // importing users
26 | const createdUsers = await User.insertMany(users);
27 |
28 | const adminUser = createdUsers[0]._id; // getting the ".id" field
29 |
30 | const sampleProducts = products.map((product) => {
31 | return { ...product, user: adminUser };
32 | });
33 |
34 | await Product.insertMany(sampleProducts); // sampleProducts = all product data including the admin user
35 |
36 | console.log("Data Imported!".green.inverse);
37 |
38 | process.exit();
39 | } catch (error) {
40 | console.error(`${error}`.red.inverse);
41 | process.exit(1);
42 | }
43 | };
44 |
45 | const destroyData = async () => {
46 | try {
47 | await Order.deleteMany(); // if nothing is passed in paramters, it's gonna delete everything
48 | await Product.deleteMany(); // if nothing is passed in paramters, it's gonna delete everything
49 | await User.deleteMany(); // if nothing is passed in paramters, it's gonna delete everything
50 |
51 | console.log("Data Destroyed!".red.inverse);
52 |
53 | process.exit();
54 | } catch (error) {
55 | console.error(`${error}`.red.trap);
56 | process.exit(1);
57 | }
58 | };
59 |
60 | if (process.argv[2] === "-d") {
61 | destroyData();
62 | } else {
63 | importData();
64 | }
65 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
23 |
24 |
33 | React App
34 |
35 |
36 | You need to enable JavaScript to run this app.
37 |
38 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/backend/controllers/orderController.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from "express-async-handler";
2 | import Order from "../models/orderModel.js";
3 |
4 | // @description Create new order
5 | // @route POST /api/orders
6 | // @access Pvt
7 | const addOrderItems = asyncHandler(async (req, res) => {
8 | const {
9 | orderItems,
10 | shippingAddress,
11 | paymentMethod,
12 | itemsPrice,
13 | taxPrice,
14 | shippingPrice,
15 | totalPrice,
16 | } = req.body; // coming from the body
17 |
18 | if (orderItems && orderItems.length === 0) {
19 | res.status(400);
20 | throw new Error("No order items");
21 | } else {
22 | const order = new Order({
23 | orderItems,
24 | user: req.user._id,
25 | shippingAddress,
26 | paymentMethod,
27 | itemsPrice,
28 | taxPrice,
29 | shippingPrice,
30 | totalPrice,
31 | });
32 |
33 | const createdOrder = await order.save();
34 |
35 | res.status(201).json(createdOrder);
36 | }
37 | });
38 |
39 | // @description Get order by ID
40 | // @route GET /api/orders/:id
41 | // @access Pvt
42 | const getOrderById = asyncHandler(async (req, res) => {
43 | const order = await Order.findById(req.params.id).populate(
44 | "user",
45 | "name email"
46 | );
47 |
48 | if (order) {
49 | res.json(order);
50 | } else {
51 | res.status(404);
52 | throw new Error("Order not found");
53 | }
54 | });
55 |
56 | // @description Update order to PAID
57 | // @route GET /api/orders/:id/pay
58 | // @access Pvt
59 | const updateOrderToPaid = asyncHandler(async (req, res) => {
60 | const order = await Order.findById(req.params.id);
61 |
62 | if (order) {
63 | order.isPaid = true;
64 | order.paidAt = Date.now();
65 | order.paymentResult = {
66 | id: req.body.id,
67 | status: req.body.status,
68 | update_time: req.body.update_time,
69 | email_address: req.body.payer.email_address,
70 | };
71 |
72 | const updatedOrder = await order.save();
73 |
74 | res.json(updatedOrder);
75 | } else {
76 | res.status(404);
77 | throw new Error("Order not found");
78 | }
79 | });
80 |
81 | export { addOrderItems, getOrderById, updateOrderToPaid };
82 |
--------------------------------------------------------------------------------
/frontend/src/reducers/userReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | USER_DETAILS_FAIL,
3 | USER_DETAILS_REQUEST,
4 | USER_DETAILS_SUCCESS,
5 | USER_LOGIN_FAIL,
6 | USER_LOGIN_REQUEST,
7 | USER_LOGIN_SUCCESS,
8 | USER_LOGOUT,
9 | USER_REGISTER_FAIL,
10 | USER_REGISTER_REQUEST,
11 | USER_REGISTER_SUCCESS,
12 | USER_UPDATE_PROFILE_FAIL,
13 | USER_UPDATE_PROFILE_REQUEST,
14 | USER_UPDATE_PROFILE_SUCCESS,
15 | } from "../constants/userConstants";
16 |
17 | export const userLoginReducer = (state = {}, action) => {
18 | // evaluate the type in action object
19 | switch (action.type) {
20 | case USER_LOGIN_REQUEST:
21 | return { loading: true };
22 |
23 | case USER_LOGIN_SUCCESS:
24 | return { loading: false, userInfo: action.payload };
25 |
26 | case USER_LOGIN_FAIL:
27 | return { loading: false, error: action.payload };
28 |
29 | case USER_LOGOUT:
30 | return {};
31 |
32 | default:
33 | return state;
34 | }
35 | };
36 |
37 | export const userRegisterReducer = (state = {}, action) => {
38 | // evaluate the type in action object
39 | switch (action.type) {
40 | case USER_REGISTER_REQUEST:
41 | return { loading: true };
42 |
43 | case USER_REGISTER_SUCCESS:
44 | return { loading: false, userInfo: action.payload };
45 |
46 | case USER_REGISTER_FAIL:
47 | return { loading: false, error: action.payload };
48 |
49 | default:
50 | return state;
51 | }
52 | };
53 |
54 | export const userDetailsReducer = (state = { user: {} }, action) => {
55 | // evaluate the type in action object
56 | switch (action.type) {
57 | case USER_DETAILS_REQUEST:
58 | return { ...state, loading: true };
59 |
60 | case USER_DETAILS_SUCCESS:
61 | return { loading: false, user: action.payload };
62 |
63 | case USER_DETAILS_FAIL:
64 | return { loading: false, error: action.payload };
65 |
66 | default:
67 | return state;
68 | }
69 | };
70 |
71 | export const userUpdateProfileReducer = (state = {}, action) => {
72 | // evaluate the type in action object
73 | switch (action.type) {
74 | case USER_UPDATE_PROFILE_REQUEST:
75 | return { loading: true };
76 |
77 | case USER_UPDATE_PROFILE_SUCCESS:
78 | return { loading: false, success: true, userInfo: action.payload };
79 |
80 | case USER_UPDATE_PROFILE_FAIL:
81 | return { loading: false, error: action.payload };
82 |
83 | default:
84 | return state;
85 | }
86 | };
87 |
--------------------------------------------------------------------------------
/frontend/src/screens/LoginScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Link } from "react-router-dom";
3 | import { Form, Button, Row, Col } from "react-bootstrap";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import Message from "../components/Message";
6 | import Loader from "../components/Loader";
7 | import FormContainer from "../components/FormContainer";
8 | import { login } from "../actions/userActions";
9 |
10 | const LoginScreen = ({ location, history }) => {
11 | const [email, setEmail] = useState("");
12 | const [password, setPassword] = useState("");
13 |
14 | const dispatch = useDispatch();
15 |
16 | const userLogin = useSelector((state) => state.userLogin); // get userLogin from the state
17 | const { loading, error, userInfo } = userLogin; // refrence -> userReducers.js
18 |
19 | const redirect = location.search ? location.search.split("=")[1] : "/";
20 |
21 | useEffect(() => {
22 | // if user is logged in -> then userInfo exists
23 | if (userInfo) {
24 | history.push(redirect);
25 | }
26 | }, [history, userInfo, redirect]);
27 |
28 | const submitHandler = (e) => {
29 | e.preventDefault(); //since it's a form
30 | dispatch(login(email, password));
31 | };
32 |
33 | return (
34 |
35 | Sign In
36 | {error && {error} }
37 | {loading && }
38 |
40 | Email Address
41 | setEmail(e.target.value)}
46 | >
47 |
48 |
49 |
50 | Password
51 | setPassword(e.target.value)}
56 | >
57 |
58 |
59 |
60 | Sign In
61 |
62 |
63 |
64 |
65 |
66 | New Customer?{" "}
67 |
68 | Register
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default LoginScreen;
77 |
--------------------------------------------------------------------------------
/backend/data/products.js:
--------------------------------------------------------------------------------
1 | const products = [
2 | // {
3 | //
4 | // name: "Airpods Wireless Bluetooth Headphones",
5 | // image: "/images/airpods.jpg",
6 | // description:
7 | // "Bluetooth technology lets you connect it with compatible devices wirelessly High-quality AAC audio offers immersive listening experience Built-in microphone allows you to take calls while working",
8 | // brand: "Apple",
9 | // category: "Electronics",
10 | // price: 89.99,
11 | // countInStock: 10,
12 | // rating: 4.5,
13 | // numReviews: 12,
14 | // },
15 | {
16 | name: "iPhone 11 Pro 256GB Memory",
17 | image: "/images/phone.jpg",
18 | description:
19 | "Introducing the iPhone 11 Pro. A transformative triple-camera system that adds tons of capability without complexity. An unprecedented leap in battery life",
20 | brand: "Apple",
21 | category: "Electronics",
22 | price: 599.99,
23 | countInStock: 7,
24 | rating: 4.0,
25 | numReviews: 8,
26 | },
27 | {
28 | name: "Cannon EOS 80D DSLR Camera",
29 | image: "/images/camera.jpg",
30 | description:
31 | "Characterized by versatile imaging specs, the Canon EOS 80D further clarifies itself using a pair of robust focusing systems and an intuitive design",
32 | brand: "Cannon",
33 | category: "Electronics",
34 | price: 929.99,
35 | countInStock: 5,
36 | rating: 3,
37 | numReviews: 12,
38 | },
39 | {
40 | name: "Sony Playstation 4 Pro White Version",
41 | image: "/images/playstation.jpg",
42 | description:
43 | "The ultimate home entertainment center starts with PlayStation. Whether you are into gaming, HD movies, television, music",
44 | brand: "Sony",
45 | category: "Electronics",
46 | price: 399.99,
47 | countInStock: 11,
48 | rating: 5,
49 | numReviews: 12,
50 | },
51 | {
52 | name: "Logitech G-Series Gaming Mouse",
53 | image: "/images/mouse.jpg",
54 | description:
55 | "Get a better handle on your games with this Logitech LIGHTSYNC gaming mouse. The six programmable buttons allow customization for a smooth playing experience",
56 | brand: "Logitech",
57 | category: "Electronics",
58 | price: 49.99,
59 | countInStock: 7,
60 | rating: 3.5,
61 | numReviews: 10,
62 | },
63 | {
64 | name: "Amazon Echo Dot 3rd Generation",
65 | image: "/images/alexa.jpg",
66 | description:
67 | "Meet Echo Dot - Our most popular smart speaker with a fabric design. It is our most compact smart speaker that fits perfectly into small space",
68 | brand: "Amazon",
69 | category: "Electronics",
70 | price: 29.99,
71 | countInStock: 0,
72 | rating: 4,
73 | numReviews: 12,
74 | },
75 | ];
76 |
77 | export default products;
78 |
--------------------------------------------------------------------------------
/frontend/src/screens/ShippingScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Form, Button } from "react-bootstrap";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import FormContainer from "../components/FormContainer";
5 | import CheckoutSteps from "../components/CheckoutSteps";
6 | import { saveShippingAddress } from "../actions/cartActions";
7 |
8 | const ShippingScreen = ({ history }) => {
9 | const cart = useSelector((state) => state.cart);
10 | const { shippingAddress } = cart;
11 |
12 | // if it's in localstorage, it will fill these out
13 | const [address, setAddress] = useState(shippingAddress.address);
14 | const [city, setCity] = useState(shippingAddress.city);
15 | const [postalCode, setPostalCode] = useState(shippingAddress.postalCode);
16 | const [country, setCountry] = useState(shippingAddress.country);
17 |
18 | const dispatch = useDispatch();
19 |
20 | const submitHandler = (e) => {
21 | e.preventDefault();
22 | dispatch(saveShippingAddress({ address, city, postalCode, country }));
23 | history.push("/payment"); // moving to the next page
24 | };
25 |
26 | return (
27 |
28 |
29 | Shipping
30 |
31 |
33 | Address
34 | setAddress(e.target.value)}
40 | >
41 |
42 |
43 |
44 | City
45 | setCity(e.target.value)}
51 | >
52 |
53 |
54 |
55 | Postal Code
56 | setPostalCode(e.target.value)}
62 | >
63 |
64 |
65 |
66 | Country
67 | setCountry(e.target.value)}
73 | >
74 |
75 |
76 |
77 | Continue
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | export default ShippingScreen;
85 |
--------------------------------------------------------------------------------
/backend/controllers/userController.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from "express-async-handler";
2 | import User from "../models/userModel.js";
3 | import generateToken from "../utils/generateToken.js";
4 |
5 | // @description Auth user & get token
6 | // @route POST /api/users/login
7 | // @access Public
8 | const authUser = asyncHandler(async (req, res) => {
9 | const { email, password } = req.body;
10 |
11 | const user = await User.findOne({ email });
12 |
13 | if (user && (await user.matchPassword(password))) {
14 | res.json({
15 | _id: user._id,
16 | name: user.name,
17 | email: user.email,
18 | isAdmin: user.isAdmin,
19 | token: generateToken(user._id),
20 | });
21 | } else {
22 | res.status(401);
23 | throw new Error("Invalid email or password");
24 | }
25 | });
26 |
27 | // @description Register a new user
28 | // @route POST /api/users
29 | // @access Public
30 | const registerUser = asyncHandler(async (req, res) => {
31 | const { name, email, password } = req.body;
32 |
33 | const userExists = await User.findOne({ email });
34 | if (userExists) {
35 | res.status(400); // bad req
36 | throw new Error("User already exists");
37 | }
38 |
39 | const user = await User.create({
40 | name,
41 | email,
42 | password,
43 | });
44 |
45 | // if user is created
46 | if (user) {
47 | res.status(201).json({
48 | _id: user._id,
49 | name: user.name,
50 | email: user.email,
51 | isAdmin: user.isAdmin,
52 | token: generateToken(user._id),
53 | });
54 | } else {
55 | res.status(400);
56 | throw new Error("Invalid user data");
57 | }
58 | });
59 |
60 | // @description Get user profile
61 | // @route GET /api/users/profile
62 | // @access Pvt
63 | const getUserProfile = asyncHandler(async (req, res) => {
64 | const user = await User.findById(req.user._id); // to get the logged in user's id
65 |
66 | if (user) {
67 | res.json({
68 | _id: user._id,
69 | name: user.name,
70 | email: user.email,
71 | isAdmin: user.isAdmin,
72 | });
73 | } else {
74 | res.status(404);
75 | throw new Error("User not found");
76 | }
77 | });
78 |
79 | // @desc Update user profile
80 | // @route PUT /api/users/profile
81 | // @access Pvt
82 | const updateUserProfile = asyncHandler(async (req, res) => {
83 | const user = await User.findById(req.user._id);
84 |
85 | if (user) {
86 | user.name = req.body.name || user.name;
87 | user.email = req.body.email || user.email;
88 | if (req.body.password) {
89 | user.password = req.body.password;
90 | }
91 |
92 | const updatedUser = await user.save();
93 |
94 | res.json({
95 | _id: updatedUser._id,
96 | name: updatedUser.name,
97 | email: updatedUser.email,
98 | isAdmin: updatedUser.isAdmin,
99 | token: generateToken(updatedUser._id),
100 | });
101 | } else {
102 | res.status(404);
103 | throw new Error("User not found");
104 | }
105 | });
106 |
107 | export { authUser, registerUser, getUserProfile, updateUserProfile };
108 |
--------------------------------------------------------------------------------
/frontend/src/screens/CartScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { Link } from "react-router-dom";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import {
5 | Row,
6 | Col,
7 | ListGroup,
8 | Image,
9 | Form,
10 | Button,
11 | Card,
12 | } from "react-bootstrap";
13 | import Message from "../components/Message";
14 | import { addToCart, removeFromCart } from "../actions/cartActions";
15 |
16 | const CartScreen = ({ match, location, history }) => {
17 | const productId = match.params.id;
18 |
19 | const qty = location.search ? Number(location.search.split("=")[1]) : 1;
20 |
21 | const dispatch = useDispatch();
22 |
23 | const cart = useSelector((state) => state.cart);
24 | const { cartItems } = cart;
25 |
26 | useEffect(() => {
27 | if (productId) {
28 | dispatch(addToCart(productId, qty));
29 | }
30 | }, [dispatch, productId, qty]);
31 |
32 | const removeFromCartHandler = (id) => {
33 | dispatch(removeFromCart(id));
34 | };
35 |
36 | const checkoutHandler = () => {
37 | history.push("/login?redirect=shipping");
38 | };
39 |
40 | return (
41 |
42 |
43 | Shopping Cart
44 | {cartItems.length === 0 ? (
45 |
46 | Your cart is empty Go Back
47 |
48 | ) : (
49 |
50 | {cartItems.map((item) => (
51 |
52 |
53 |
54 |
55 |
56 |
57 | {item.name}
58 |
59 | ${item.price}
60 |
61 |
65 | dispatch(
66 | addToCart(item.product, Number(e.target.value))
67 | )
68 | }
69 | >
70 | {[...Array(item.countInStock).keys()].map((x) => (
71 |
72 | {x + 1}
73 |
74 | ))}
75 |
76 |
77 |
78 | removeFromCartHandler(item.product)}
82 | >
83 |
84 |
85 |
86 |
87 |
88 | ))}
89 |
90 | )}
91 |
92 |
93 |
94 |
95 |
96 |
97 | Subtotal ({cartItems.reduce((acc, item) => acc + item.qty, 0)})
98 | items
99 |
100 | $
101 | {cartItems
102 | .reduce((acc, item) => acc + item.qty * item.price, 0)
103 | .toFixed(2)}
104 |
105 |
106 |
112 | Proceed To Checkout
113 |
114 |
115 |
116 |
117 |
118 |
119 | );
120 | };
121 |
122 | export default CartScreen;
123 |
--------------------------------------------------------------------------------
/frontend/src/screens/RegisterScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Link } from "react-router-dom";
3 | import { Form, Button, Row, Col } from "react-bootstrap";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import Message from "../components/Message";
6 | import Loader from "../components/Loader";
7 | import FormContainer from "../components/FormContainer";
8 | import { register } from "../actions/userActions";
9 |
10 | const RegisterScreen = ({ location, history }) => {
11 | const [name, setName] = useState("");
12 | const [email, setEmail] = useState("");
13 | const [password, setPassword] = useState("");
14 | const [confirmPassword, setConfirmPassword] = useState("");
15 | const [message, setMessage] = useState(null);
16 |
17 | const dispatch = useDispatch();
18 |
19 | const userRegister = useSelector((state) => state.userRegister); // get userLogin from the state
20 | const { loading, error, userInfo } = userRegister; // refrence -> userReducers.js
21 |
22 | const redirect = location.search ? location.search.split("=")[1] : "/";
23 |
24 | useEffect(() => {
25 | // if user is logged in -> then userInfo exists
26 | if (userInfo) {
27 | history.push(redirect);
28 | }
29 | }, [history, userInfo, redirect]);
30 |
31 | const submitHandler = (e) => {
32 | e.preventDefault(); //since it's a form
33 |
34 | if (password !== confirmPassword) {
35 | setMessage("Passwords do not match");
36 | } else {
37 | //dispatch the register
38 | dispatch(register(name, email, password));
39 | }
40 | };
41 |
42 | return (
43 |
44 | Sign Up
45 | {message && {message} }
46 |
47 | {error && {error} }
48 | {loading && }
49 |
51 | Name
52 | setName(e.target.value)}
57 | >
58 |
59 |
60 |
61 | Email Address
62 | setEmail(e.target.value)}
67 | >
68 |
69 |
70 |
71 | Password
72 | setPassword(e.target.value)}
77 | >
78 |
79 |
80 |
81 | Confirm password
82 | setConfirmPassword(e.target.value)}
87 | >
88 |
89 |
90 |
91 | Register
92 |
93 |
94 |
95 |
96 |
97 | Already a user?
98 |
99 | Log In
100 |
101 |
102 |
103 |
104 | );
105 | };
106 |
107 | export default RegisterScreen;
108 |
--------------------------------------------------------------------------------
/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 the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will 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/screens/ProductScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Link } from "react-router-dom";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import {
5 | Row,
6 | Col,
7 | Image,
8 | ListGroup,
9 | Card,
10 | Button,
11 | Form,
12 | } from "react-bootstrap";
13 | import Rating from "../components/Rating";
14 | import { listProductDetails } from "../actions/productActions";
15 | import Loader from "../components/Loader";
16 | import Message from "../components/Message";
17 |
18 | const ProductScreen = ({ match, history }) => {
19 | const [qty, setQty] = useState(1);
20 |
21 | const dispatch = useDispatch();
22 |
23 | const productDetails = useSelector((state) => state.productDetails);
24 |
25 | const { loading, error, product } = productDetails;
26 |
27 | useEffect(() => {
28 | dispatch(listProductDetails(match.params.id));
29 | }, [dispatch, match]);
30 |
31 | const addToCartHandler = () => {
32 | history.push(`/cart/${match.params.id}?qty=${qty}`);
33 | };
34 |
35 | return (
36 |
37 |
38 | Go Back
39 |
40 | {loading ? (
41 |
42 | ) : error ? (
43 | {error}
44 | ) : (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | {product.name}
54 |
55 |
56 |
60 |
61 | Price: ${product.price}
62 |
63 | Description: {product.description}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | Price:
74 |
75 |
76 | ${product.price}
77 |
78 |
79 |
80 |
81 |
82 |
83 | Status:
84 |
85 |
86 | {product.countInStock > 0 ? "In Stock" : "Out of Stock"}
87 |
88 |
89 |
90 |
91 | {product.countInStock > 0 && (
92 |
93 |
94 | Qty
95 |
96 | setQty(e.target.value)}
100 | >
101 | {[...Array(product.countInStock).keys()].map((x) => (
102 |
103 | {x + 1}
104 |
105 | ))}
106 |
107 |
108 |
109 |
110 | )}
111 |
112 |
113 |
119 | Add to Cart
120 |
121 |
122 |
123 |
124 |
125 |
126 | )}
127 |
128 | );
129 | };
130 |
131 | export default ProductScreen;
132 |
--------------------------------------------------------------------------------
/frontend/src/screens/ProfileScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Form, Button, Row, Col } from "react-bootstrap";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import Message from "../components/Message";
5 | import Loader from "../components/Loader";
6 | import { getUserDetails, updateUserProfile } from "../actions/userActions";
7 |
8 | const ProfileScreen = ({ location, history }) => {
9 | const [name, setName] = useState("");
10 | const [email, setEmail] = useState("");
11 | const [password, setPassword] = useState("");
12 | const [confirmPassword, setConfirmPassword] = useState("");
13 | const [message, setMessage] = useState(null);
14 |
15 | const dispatch = useDispatch();
16 |
17 | const userDetails = useSelector((state) => state.userDetails); // get userLogin from the state
18 | const { loading, error, user } = userDetails; // refrence -> userReducers.js
19 |
20 | const userLogin = useSelector((state) => state.userLogin); // get userLogin from the state
21 | const { userInfo } = userLogin; // refrence -> userReducers.js
22 |
23 | const userUpdateProfile = useSelector((state) => state.userUpdateProfile);
24 | const { success } = userUpdateProfile;
25 |
26 | useEffect(() => {
27 | // if user is not logged in -> then redirect to /login
28 | if (!userInfo) {
29 | history.push("/login");
30 | } else {
31 | if (!user.name) {
32 | dispatch(getUserDetails("profile")); // "profile" gets passed in the {id} at userActions.js
33 | } else {
34 | // if user is there
35 | setName(user.name);
36 | setEmail(user.email);
37 | }
38 | }
39 | }, [dispatch, history, userInfo, user]);
40 |
41 | const submitHandler = (e) => {
42 | e.preventDefault(); //since it's a form
43 |
44 | if (password !== confirmPassword) {
45 | setMessage("Passwords do not match");
46 | } else {
47 | //dispatch the updated profile
48 | dispatch(updateUserProfile({ id: user._id, name, email, password }));
49 | }
50 | };
51 |
52 | return (
53 |
54 |
55 | User Profile
56 | {message && {message} }
57 |
58 | {error && {error} }
59 | {success && Profile updated }
60 |
61 | {loading && }
62 |
63 |
65 | Name
66 | setName(e.target.value)}
71 | >
72 |
73 |
74 |
75 | Email Address
76 | setEmail(e.target.value)}
81 | >
82 |
83 |
84 |
85 | Password
86 | setPassword(e.target.value)}
91 | >
92 |
93 |
94 |
95 | Confirm password
96 | setConfirmPassword(e.target.value)}
101 | >
102 |
103 |
104 |
105 | Update
106 |
107 |
108 |
109 |
110 |
111 | My orders
112 |
113 |
114 | );
115 | };
116 |
117 | export default ProfileScreen;
118 |
--------------------------------------------------------------------------------
/frontend/src/actions/userActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | USER_DETAILS_FAIL,
3 | USER_DETAILS_REQUEST,
4 | USER_DETAILS_SUCCESS,
5 | USER_LOGIN_FAIL,
6 | USER_LOGIN_REQUEST,
7 | USER_LOGIN_SUCCESS,
8 | USER_LOGOUT,
9 | USER_REGISTER_FAIL,
10 | USER_REGISTER_REQUEST,
11 | USER_REGISTER_SUCCESS,
12 | USER_UPDATE_PROFILE_REQUEST,
13 | USER_UPDATE_PROFILE_SUCCESS,
14 | USER_UPDATE_PROFILE_FAIL,
15 | } from "../constants/userConstants";
16 | import axios from "axios";
17 |
18 | export const login = (email, password) => async (dispatch) => {
19 | try {
20 | dispatch({
21 | type: USER_LOGIN_REQUEST,
22 | });
23 | const config = {
24 | headers: {
25 | "Content-Type": "application/json",
26 | },
27 | };
28 |
29 | const { data } = await axios.post(
30 | "/api/users/login",
31 | { email, password },
32 | config
33 | );
34 |
35 | // after request, dispatch the user login success
36 | dispatch({
37 | type: USER_LOGIN_SUCCESS,
38 | payload: data,
39 | });
40 |
41 | localStorage.setItem("userInfo", JSON.stringify(data));
42 | } catch (error) {
43 | dispatch({
44 | type: USER_LOGIN_FAIL,
45 | payload:
46 | error.response && error.response.data.message
47 | ? error.response.data.message
48 | : error.message,
49 | });
50 | }
51 | };
52 |
53 | export const logout = () => (dispatch) => {
54 | // remove
55 | localStorage.removeItem("userInfo");
56 | dispatch({ type: USER_LOGOUT });
57 | };
58 |
59 | export const register = (name, email, password) => async (dispatch) => {
60 | try {
61 | dispatch({
62 | type: USER_REGISTER_REQUEST,
63 | });
64 | const config = {
65 | headers: {
66 | "Content-Type": "application/json",
67 | },
68 | };
69 |
70 | const { data } = await axios.post(
71 | "/api/users",
72 | { name, email, password },
73 | config
74 | );
75 |
76 | dispatch({
77 | type: USER_REGISTER_SUCCESS,
78 | payload: data,
79 | });
80 |
81 | // after request, dispatch the user login success
82 | dispatch({
83 | type: USER_LOGIN_SUCCESS,
84 | payload: data,
85 | });
86 |
87 | localStorage.setItem("userInfo", JSON.stringify(data));
88 | } catch (error) {
89 | dispatch({
90 | type: USER_REGISTER_FAIL,
91 | payload:
92 | error.response && error.response.data.message
93 | ? error.response.data.message
94 | : error.message,
95 | });
96 | }
97 | };
98 |
99 | export const getUserDetails = (id) => async (dispatch, getState) => {
100 | try {
101 | dispatch({
102 | type: USER_DETAILS_REQUEST,
103 | });
104 |
105 | const {
106 | userLogin: { userInfo },
107 | } = getState(); // userInfo is in userLogin
108 |
109 | const config = {
110 | headers: {
111 | "Content-Type": "application/json",
112 |
113 | //pass token
114 | Authorization: `Bearer ${userInfo.token}`,
115 | },
116 | };
117 |
118 | const { data } = await axios.get(`/api/users/${id}`, config);
119 |
120 | dispatch({
121 | type: USER_DETAILS_SUCCESS,
122 | payload: data,
123 | });
124 | } catch (error) {
125 | dispatch({
126 | type: USER_DETAILS_FAIL,
127 | payload:
128 | error.response && error.response.data.message
129 | ? error.response.data.message
130 | : error.message,
131 | });
132 | }
133 | };
134 |
135 | export const updateUserProfile = (user) => async (dispatch, getState) => {
136 | // getState is required because token is needed to be sent
137 | try {
138 | dispatch({
139 | type: USER_UPDATE_PROFILE_REQUEST,
140 | });
141 |
142 | const {
143 | userLogin: { userInfo },
144 | } = getState(); // userInfo is in userLogin
145 |
146 | const config = {
147 | headers: {
148 | "Content-Type": "application/json",
149 |
150 | //pass token
151 | Authorization: `Bearer ${userInfo.token}`,
152 | },
153 | };
154 |
155 | const { data } = await axios.put(`/api/users/profile`, user, config);
156 |
157 | dispatch({
158 | type: USER_UPDATE_PROFILE_SUCCESS,
159 | payload: data,
160 | });
161 | } catch (error) {
162 | dispatch({
163 | type: USER_UPDATE_PROFILE_FAIL,
164 | payload:
165 | error.response && error.response.data.message
166 | ? error.response.data.message
167 | : error.message,
168 | });
169 | }
170 | };
171 |
--------------------------------------------------------------------------------
/frontend/src/screens/OrderScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { Link } from "react-router-dom";
3 | import { Row, Col, ListGroup, Image, Card } from "react-bootstrap";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import Message from "../components/Message";
6 | import Loader from "../components/Loader";
7 | import { getOrderDetails } from "../actions/orderActions";
8 |
9 | const OrderScreen = ({ match }) => {
10 | const orderId = match.params.id;
11 |
12 | const dispatch = useDispatch();
13 |
14 | const orderDetails = useSelector((state) => state.orderDetails);
15 | const { order, loading, error } = orderDetails;
16 |
17 | if (!loading) {
18 | // Calculate prices
19 | const addDecimals = (num) => {
20 | return (Math.round(num * 100) / 100).toFixed(2);
21 | };
22 |
23 | order.itemsPrice = addDecimals(
24 | order.orderItems.reduce((acc, item) => acc + item.price * item.qty, 0)
25 | );
26 | // order.itemsPrice = addDecimals(
27 | // order.orderItems.reduce((acc, item) => acc + item.price * item.qty, 0)
28 | // );
29 | }
30 |
31 | useEffect(() => {
32 | dispatch(getOrderDetails(orderId));
33 | }, []);
34 |
35 | return loading ? (
36 |
37 | ) : error ? (
38 | {error}
39 | ) : (
40 |
41 |
Order {order._id}
42 |
43 |
44 |
45 |
46 | Shipping
47 |
48 |
49 | Name: {order.user.name}
50 |
51 |
52 | Email: {" "}
53 | {order.user.email}
54 |
55 |
56 |
57 | Address:
58 | {order.shippingAddress.address}, {order.shippingAddress.city}{" "}
59 | {order.shippingAddress.postalCode},{" "}
60 | {order.shippingAddress.country}
61 |
62 |
63 | {order.isDelivered ? (
64 |
65 | {" "}
66 | Delivered on {order.deliveredAt}{" "}
67 |
68 | ) : (
69 | Not delivered
70 | )}
71 |
72 |
73 |
74 | Payment method
75 |
76 | Method:
77 | {order.paymentMethod}
78 |
79 | {order.isPaid ? (
80 | Paid on {order.paidAt}
81 | ) : (
82 | Not paid
83 | )}
84 |
85 |
86 |
87 | Order items
88 | {order.orderItems.length === 0 ? (
89 | Order is empty
90 | ) : (
91 |
92 | {order.orderItems.map((item, index) => (
93 |
94 |
95 |
96 |
102 |
103 |
104 |
105 | {item.name}
106 |
107 |
108 |
109 | {item.qty} x ${item.price} = ${item.qty * item.price}
110 |
111 |
112 |
113 | ))}
114 |
115 | )}
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | Order summary
124 |
125 |
126 |
127 | Items
128 | ${order.itemsPrice}
129 |
130 |
131 |
132 |
133 | Shipping
134 | ${order.shippingPrice}
135 |
136 |
137 |
138 |
139 | Tax
140 | ${order.taxPrice}
141 |
142 |
143 |
144 |
145 | Total
146 | ${order.totalPrice}
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | );
155 | };
156 |
157 | export default OrderScreen;
158 |
--------------------------------------------------------------------------------
/frontend/src/screens/PlaceOrderScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { Link } from "react-router-dom";
3 | import { Button, Row, Col, ListGroup, Image, Card } from "react-bootstrap";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import Message from "../components/Message";
6 | import CheckoutSteps from "../components/CheckoutSteps";
7 | import { createOrder } from "../actions/orderActions";
8 | // import { ORDER_CREATE_RESET } from "../constants/orderConstants";
9 | // import { USER_DETAILS_RESET } from "../constants/userConstants";
10 |
11 | const PlaceOrderScreen = ({ history }) => {
12 | const dispatch = useDispatch();
13 |
14 | const cart = useSelector((state) => state.cart);
15 |
16 | if (!cart.shippingAddress.address) {
17 | history.push("/shipping");
18 | } else if (!cart.paymentMethod) {
19 | history.push("/payment");
20 | }
21 |
22 | // Calculate prices
23 | const addDecimals = (num) => {
24 | return (Math.round(num * 100) / 100).toFixed(2);
25 | };
26 |
27 | cart.itemsPrice = addDecimals(
28 | cart.cartItems.reduce((acc, item) => acc + item.price * item.qty, 0)
29 | );
30 |
31 | cart.shippingPrice = addDecimals(cart.itemsPrice > 100 ? 0 : 100);
32 | cart.taxPrice = addDecimals(Number((0.15 * cart.itemsPrice).toFixed(2)));
33 | cart.totalPrice = (
34 | Number(cart.itemsPrice) +
35 | Number(cart.shippingPrice) +
36 | Number(cart.taxPrice)
37 | ).toFixed(2);
38 |
39 | const orderCreate = useSelector((state) => state.orderCreate);
40 | const { order, success, error } = orderCreate;
41 |
42 | useEffect(() => {
43 | if (success) {
44 | history.push(`/order/${order._id}`);
45 | // dispatch({ type: USER_DETAILS_RESET });
46 | // dispatch({ type: ORDER_CREATE_RESET });
47 | }
48 | // eslint-disable-next-line
49 | }, [history, success]);
50 |
51 | const placeOrderHandler = () => {
52 | dispatch(
53 | createOrder({
54 | orderItems: cart.cartItems,
55 | shippingAddress: cart.shippingAddress,
56 | paymentMethod: cart.paymentMethod,
57 | itemsPrice: cart.itemsPrice,
58 | shippingPrice: cart.shippingPrice,
59 | taxPrice: cart.taxPrice,
60 | totalPrice: cart.totalPrice,
61 | })
62 | );
63 | };
64 |
65 | return (
66 |
67 |
68 |
69 |
70 |
71 |
72 | Shipping
73 |
74 | Address:
75 | {cart.shippingAddress.address}, {cart.shippingAddress.city}{" "}
76 | {cart.shippingAddress.postalCode},{" "}
77 | {cart.shippingAddress.country}
78 |
79 |
80 |
81 |
82 | Payment Method
83 | Method:
84 | {cart.paymentMethod}
85 |
86 |
87 |
88 | Order Items
89 | {cart.cartItems.length === 0 ? (
90 | Your cart is empty
91 | ) : (
92 |
93 | {cart.cartItems.map((item, index) => (
94 |
95 |
96 |
97 |
103 |
104 |
105 |
106 | {item.name}
107 |
108 |
109 |
110 | {item.qty} x ${item.price} = ${item.qty * item.price}
111 |
112 |
113 |
114 | ))}
115 |
116 | )}
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | Order summary
125 |
126 |
127 |
128 | Items
129 | ${cart.itemsPrice}
130 |
131 |
132 |
133 |
134 | Shipping
135 | ${cart.shippingPrice}
136 |
137 |
138 |
139 |
140 | Tax
141 | ${cart.taxPrice}
142 |
143 |
144 |
145 |
146 | Total
147 | ${cart.totalPrice}
148 |
149 |
150 |
151 | {error && {error} }
152 |
153 |
154 |
160 | Place order
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 | );
169 | };
170 |
171 | export default PlaceOrderScreen;
172 |
--------------------------------------------------------------------------------