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