├── .gitignore ├── README.md ├── backend ├── config │ └── db.js ├── controllers │ ├── orderController.js │ ├── productController.js │ └── userController.js ├── data │ ├── products.js │ └── users.js ├── middleware │ ├── authMiddleware.js │ └── errorMiddleware.js ├── models │ ├── orderModel.js │ ├── productModel.js │ └── userModel.js ├── routes │ ├── orderRoutes.js │ ├── productRoutes.js │ └── userRoutes.js ├── seeder.js ├── server.js └── utils │ └── generateToken.js ├── frontend ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── images │ │ ├── airpods.jpg │ │ ├── alexa.jpg │ │ ├── camera.jpg │ │ ├── mouse.jpg │ │ ├── phone.jpg │ │ └── playstation.jpg │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.js │ ├── actions │ │ ├── cartActions.js │ │ ├── orderActions.js │ │ ├── productActions.js │ │ └── userActions.js │ ├── bootstrap.min.css │ ├── components │ │ ├── CheckoutSteps.js │ │ ├── Footer.js │ │ ├── FormContainer.js │ │ ├── Header.js │ │ ├── Loader.js │ │ ├── Message.js │ │ ├── Product.js │ │ └── Rating.js │ ├── constants │ │ ├── cartConstants.js │ │ ├── orderConstants.js │ │ ├── productConstants.js │ │ └── userConstants.js │ ├── index.css │ ├── index.js │ ├── products.js │ ├── reducers │ │ ├── cartReducers.js │ │ ├── orderReducers.js │ │ ├── productReducers.js │ │ └── userReducers.js │ ├── screens │ │ ├── Cart.js │ │ ├── Home.js │ │ ├── Login.js │ │ ├── Order.js │ │ ├── Payment.js │ │ ├── PlaceOrder.js │ │ ├── Product.js │ │ ├── Profile.js │ │ ├── Register.js │ │ └── Shipping.js │ ├── serviceWorker.js │ ├── store.js │ └── utils │ │ └── history.js └── yarn.lock ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .vscode 4 | # dependencies 5 | /node_modules 6 | node_modules/ 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MERN E-Commerce Store 2 | 3 | ## Tech 4 | 5 | - React 6 | - Mongo DB 7 | - Node.js 8 | 9 | ## Project Structure 10 | 11 | - `/backend` 12 | - Contains all the necessary operations required to process data from the store and operations to send and receive data from the front end. 13 | 14 | ## How to run 15 | 16 | - To run both the client and the server use the command `npm run dev` 17 | - To only run the server use the command `npm run server` 18 | - To only run the client use the command `npm run client` 19 | -------------------------------------------------------------------------------- /backend/config/db.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | /** 4 | * Method that connects our MongoDB database. 5 | */ 6 | const connectDB = async () => { 7 | try { 8 | const conn = await mongoose.connect(process.env.MONGO_URI, { 9 | useUnifiedTopology: true, 10 | useNewUrlParser: true, 11 | useCreateIndex: true, 12 | }) 13 | console.log(`MongoDB connected: ${conn.connection.host}`.cyan.underline) 14 | } catch (error) { 15 | console.error(`Error: ${error.message}`.red.underline.bold) 16 | process.exit(1) 17 | } 18 | } 19 | 20 | export default connectDB 21 | -------------------------------------------------------------------------------- /backend/controllers/orderController.js: -------------------------------------------------------------------------------- 1 | import Order from '../models/orderModel.js' 2 | import asyncHandler from 'express-async-handler' // Middleware to handle exceptions inside async express routes 3 | 4 | // @desc Create a new order 5 | // @route GET /api/orders 6 | // @access Private 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 17 | 18 | // Make sure order is not empty 19 | if (orderItems && orderItems.length === 0) { 20 | res.status(400) // Bad request 21 | throw new Error('No order items') 22 | return 23 | } else { 24 | const order = new Order({ 25 | orderItems, 26 | user: req.user._id, 27 | shippingAddress, 28 | paymentMethod, 29 | itemsPrice, 30 | taxPrice, 31 | shippingPrice, 32 | totalPrice, 33 | }) 34 | 35 | const createdOrder = await order.save() 36 | 37 | res.status(201).json(createdOrder) 38 | } 39 | }) 40 | 41 | // @desc Get an order by the id 42 | // @route GET /api/orders/:id 43 | // @access Private 44 | const getOrderById = asyncHandler(async (req, res) => { 45 | const order = await Order.findById(req.params.id).populate( 46 | 'user', 47 | 'name email' 48 | ) 49 | 50 | // if order exists else throw error 51 | if (order) { 52 | res.json(order) 53 | } else { 54 | res.status(404) 55 | throw new Error('Order not found') 56 | } 57 | }) 58 | 59 | // @desc Update order to paid 60 | // @route PUT /api/orders/:id/pay 61 | // @access Private 62 | const updateOrderToPaid = asyncHandler(async (req, res) => { 63 | // Get order from DB 64 | const order = await Order.findById(req.params.id) 65 | 66 | // if order exists update the fields else throw error 67 | if (order) { 68 | order.isPaid = true 69 | order.paidAt = Date.now() 70 | order.paymentResult = { 71 | id: req.body.id, 72 | status: req.body.status, 73 | update_time: req.body.update_time, 74 | email_address: req.body.payer.email_address, 75 | } 76 | 77 | // Save the updated order in the DB 78 | const updatedOrder = await order.save() 79 | 80 | // Send back updated order 81 | res.json(updatedOrder) 82 | } else { 83 | res.status(404) 84 | throw new Error('Could not update order') 85 | } 86 | }) 87 | 88 | // @desc Get logged in user orders 89 | // @route PUT /api/orders/myorders 90 | // @access Private 91 | const getUserOrders = asyncHandler(async (req, res) => { 92 | // Get orders from DB 93 | const orders = await Order.find({ user: req.user._id }) 94 | 95 | res.json(orders) 96 | }) 97 | 98 | export { addOrderItems, getOrderById, updateOrderToPaid, getUserOrders } 99 | -------------------------------------------------------------------------------- /backend/controllers/productController.js: -------------------------------------------------------------------------------- 1 | import Product from '../models/productModel.js' 2 | import asyncHandler from 'express-async-handler' // Middleware to handle exceptions inside async express routes 3 | 4 | // @desc Fetch all products 5 | // @route GET /api/products 6 | // @access Public 7 | const getProducts = asyncHandler(async (req, res) => { 8 | // Get all the products from MongoDB 9 | const products = await Product.find({}) 10 | res.json(products) 11 | }) 12 | 13 | // @desc 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) 18 | if (product) { 19 | res.json(product) 20 | } else { 21 | res.status(404) 22 | throw new Error('Product Not Found') 23 | } 24 | }) 25 | 26 | export { getProducts, getProductById } 27 | -------------------------------------------------------------------------------- /backend/controllers/userController.js: -------------------------------------------------------------------------------- 1 | import User from '../models/userModel.js' 2 | import asyncHandler from 'express-async-handler' // Middleware to handle exceptions inside async express routes 3 | import generateToken from '../utils/generateToken.js' 4 | 5 | // @desc 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 | // Find a user by email 11 | const user = await User.findOne({ email }) 12 | 13 | // If the user exists and the password matches the one store return the details with JSON web token signature 14 | if (user && (await user.matchPassword(password))) { 15 | res.json({ 16 | _id: user._id, 17 | name: user.name, 18 | email: user.email, 19 | isAdmin: user.isAdmin, 20 | token: generateToken(user._id), 21 | }) 22 | } else { 23 | res.status(401) 24 | throw new Error('Invalid email or password') 25 | } 26 | }) 27 | 28 | // @desc Register a new user 29 | // @route POST /api/users 30 | // @access Public 31 | const registerUser = asyncHandler(async (req, res) => { 32 | const { name, email, password } = req.body 33 | 34 | const userExists = await User.findOne({ email }) 35 | 36 | // If the user exists already 37 | if (userExists) { 38 | res.status(400) 39 | throw new Error('User already exists') 40 | } 41 | 42 | // Create a new user 43 | const user = await User.create({ 44 | name, 45 | email, 46 | password, 47 | }) 48 | 49 | // If the user is successfully created then send back user details in response 50 | if (user) { 51 | res.status(201).json({ 52 | _id: user._id, 53 | name: user.name, 54 | email: user.email, 55 | isAdmin: user.isAdmin, 56 | token: generateToken(user._id), 57 | }) 58 | } else { 59 | res.status(400) 60 | throw new Error('Invalid user data') 61 | } 62 | }) 63 | 64 | // @desc Get user profile 65 | // @route GET /api/users/profile 66 | // @access Private 67 | const getUserProfile = asyncHandler(async (req, res) => { 68 | const user = await User.findById(req.user._id) 69 | 70 | if (user) { 71 | res.json({ 72 | _id: user._id, 73 | name: user.name, 74 | email: user.email, 75 | isAdmin: user.isAdmin, 76 | }) 77 | } else { 78 | res.status(404) 79 | throw new Error('User not found') 80 | } 81 | }) 82 | 83 | // @desc Update user profile 84 | // @route PUT /api/users/profile 85 | // @access Private 86 | const updateUserProfile = asyncHandler(async (req, res) => { 87 | const user = await User.findById(req.user._id) 88 | 89 | if (user) { 90 | // Check which fields were sent in the request else just keep them the same 91 | user.name = req.body.name || user.name 92 | user.email = req.body.email || user.email 93 | // Check if password was sent with request 94 | if (req.body.password) { 95 | user.password = req.body.password 96 | } 97 | const updateUser = await user.save() 98 | 99 | res.json({ 100 | _id: updateUser._id, 101 | name: updateUser.name, 102 | email: updateUser.email, 103 | isAdmin: updateUser.isAdmin, 104 | token: generateToken(updateUser._id), 105 | }) 106 | } else { 107 | res.status(404) 108 | throw new Error('User not found') 109 | } 110 | }) 111 | 112 | export { authUser, getUserProfile, registerUser, updateUserProfile } 113 | -------------------------------------------------------------------------------- /backend/data/products.js: -------------------------------------------------------------------------------- 1 | const products = [ 2 | { 3 | name: 'Airpods Wireless Bluetooth Headphones', 4 | image: '/images/airpods.jpg', 5 | description: 6 | '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', 7 | brand: 'Apple', 8 | category: 'Electronics', 9 | price: 89.99, 10 | countInStock: 10, 11 | rating: 4.5, 12 | numReviews: 12, 13 | }, 14 | { 15 | name: 'iPhone 11 Pro 256GB Memory', 16 | image: '/images/phone.jpg', 17 | description: 18 | 'Introducing the iPhone 11 Pro. A transformative triple-camera system that adds tons of capability without complexity. An unprecedented leap in battery life', 19 | brand: 'Apple', 20 | category: 'Electronics', 21 | price: 599.99, 22 | countInStock: 7, 23 | rating: 4.0, 24 | numReviews: 8, 25 | }, 26 | { 27 | name: 'Cannon EOS 80D DSLR Camera', 28 | image: '/images/camera.jpg', 29 | description: 30 | 'Characterized by versatile imaging specs, the Canon EOS 80D further clarifies itself using a pair of robust focusing systems and an intuitive design', 31 | brand: 'Cannon', 32 | category: 'Electronics', 33 | price: 929.99, 34 | countInStock: 5, 35 | rating: 3, 36 | numReviews: 12, 37 | }, 38 | { 39 | name: 'Sony Playstation 4 Pro White Version', 40 | image: '/images/playstation.jpg', 41 | description: 42 | 'The ultimate home entertainment center starts with PlayStation. Whether you are into gaming, HD movies, television, music', 43 | brand: 'Sony', 44 | category: 'Electronics', 45 | price: 399.99, 46 | countInStock: 11, 47 | rating: 5, 48 | numReviews: 12, 49 | }, 50 | { 51 | name: 'Logitech G-Series Gaming Mouse', 52 | image: '/images/mouse.jpg', 53 | description: 54 | 'Get a better handle on your games with this Logitech LIGHTSYNC gaming mouse. The six programmable buttons allow customization for a smooth playing experience', 55 | brand: 'Logitech', 56 | category: 'Electronics', 57 | price: 49.99, 58 | countInStock: 7, 59 | rating: 3.5, 60 | numReviews: 10, 61 | }, 62 | { 63 | name: 'Amazon Echo Dot 3rd Generation', 64 | image: '/images/alexa.jpg', 65 | description: 66 | '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', 67 | brand: 'Amazon', 68 | category: 'Electronics', 69 | price: 29.99, 70 | countInStock: 0, 71 | rating: 4, 72 | numReviews: 12, 73 | }, 74 | ] 75 | 76 | export default products 77 | -------------------------------------------------------------------------------- /backend/data/users.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs' 2 | 3 | /** 4 | * Data to be used for seeder script 5 | */ 6 | const users = [ 7 | { 8 | name: 'Admin User', 9 | email: 'admin@example.com', 10 | password: bcrypt.hashSync('123456', 10), // Generate hash for password 11 | isAdmin: true, 12 | }, 13 | { 14 | name: 'John Wayne', 15 | email: 'jwayne@example.com', 16 | password: bcrypt.hashSync('123456', 10), 17 | }, 18 | { 19 | name: 'James Harden', 20 | email: 'jharden@example.com', 21 | password: bcrypt.hashSync('123456', 10), 22 | }, 23 | ] 24 | 25 | export default users 26 | -------------------------------------------------------------------------------- /backend/middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | import User from '../models/userModel.js' 3 | import asyncHandler from 'express-async-handler' // Middleware to handle exceptions inside async express routes 4 | 5 | // Middleware to protect routes 6 | const protect = asyncHandler(async (req, res, next) => { 7 | let token 8 | 9 | // Make sure Authorization headers exist that it contains the Bearer keyword 10 | if ( 11 | req.headers.authorization && 12 | req.headers.authorization.startsWith('Bearer') 13 | ) { 14 | // Try to decode the token 15 | try { 16 | // Split the Bearer from the actual token and get the actual token 17 | token = req.headers.authorization.split(' ')[1] 18 | 19 | // Decoded token with Header, Payload 20 | const decoded = jwt.verify(token, process.env.JWT_SECRET) 21 | 22 | // Fetch the user with the decoded id 23 | req.user = await User.findById(decoded.id).select('-password') 24 | 25 | next() 26 | } catch (error) { 27 | console.error(error) 28 | res.status(401) 29 | throw new Error('Not Authorized: Invalid Token') 30 | } 31 | } 32 | 33 | // Check if there is no token 34 | if (!token) { 35 | res.status(401) 36 | throw new Error('Not Authorized: No Token') 37 | } 38 | }) 39 | 40 | export { protect } 41 | -------------------------------------------------------------------------------- /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 | const errorHandler = (err, req, res, next) => { 8 | // If the the status code is 200 then make it a 500 else just just the 9 | const statusCode = res.statusCode === 200 ? 500 : res.statusCode 10 | res.status(statusCode) 11 | res.json({ 12 | message: err.message, 13 | stack: process.env.NODE_ENV === 'production' ? null : err.stack, 14 | }) 15 | } 16 | 17 | export { notFound, errorHandler } 18 | -------------------------------------------------------------------------------- /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: { 13 | type: String, 14 | required: true, 15 | }, 16 | qty: { 17 | type: Number, 18 | required: true, 19 | }, 20 | image: { 21 | type: String, 22 | required: true, 23 | }, 24 | price: { 25 | type: Number, 26 | required: true, 27 | }, 28 | product: { 29 | type: mongoose.Schema.Types.ObjectId, 30 | required: true, 31 | ref: 'Product', 32 | }, 33 | }, 34 | ], 35 | shippingAddress: { 36 | address: { 37 | type: String, 38 | required: true, 39 | }, 40 | city: { 41 | type: String, 42 | required: true, 43 | }, 44 | postalCode: { 45 | type: String, 46 | required: true, 47 | }, 48 | country: { 49 | type: String, 50 | required: true, 51 | }, 52 | }, 53 | paymentMethod: { 54 | type: String, 55 | required: true, 56 | }, 57 | paymentResult: { 58 | id: { 59 | type: String, 60 | }, 61 | status: { 62 | type: String, 63 | }, 64 | update_time: { 65 | type: String, 66 | }, 67 | email_address: { 68 | type: String, 69 | }, 70 | }, 71 | taxPrice: { 72 | type: Number, 73 | required: true, 74 | default: 0.0, 75 | }, 76 | shippingPrice: { 77 | type: Number, 78 | required: true, 79 | default: 0.0, 80 | }, 81 | totalPrice: { 82 | type: Number, 83 | required: true, 84 | default: 0.0, 85 | }, 86 | isPaid: { 87 | type: Boolean, 88 | required: true, 89 | default: false, 90 | }, 91 | paidAt: { 92 | type: Date, 93 | }, 94 | isDelivered: { 95 | type: Boolean, 96 | required: true, 97 | default: false, 98 | }, 99 | deliveredAt: { 100 | type: Date, 101 | }, 102 | }, 103 | { 104 | timestamps: true, 105 | } 106 | ) 107 | 108 | const Order = mongoose.model('Order', orderSchema) 109 | 110 | export default Order 111 | -------------------------------------------------------------------------------- /backend/models/productModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | const reviewSchema = mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: true, 8 | }, 9 | rating: { 10 | type: Number, 11 | required: true, 12 | }, 13 | comment: { 14 | type: String, 15 | required: true, 16 | }, 17 | }, 18 | { 19 | timestamps: true, 20 | } 21 | ) 22 | 23 | const productSchema = mongoose.Schema( 24 | { 25 | // ref is used to add relationship between the User and Product 26 | user: { 27 | type: mongoose.Schema.Types.ObjectId, 28 | required: true, 29 | ref: 'User', 30 | }, 31 | name: { 32 | type: String, 33 | required: true, 34 | }, 35 | image: { 36 | type: String, 37 | required: true, 38 | }, 39 | brand: { 40 | type: String, 41 | required: true, 42 | }, 43 | category: { 44 | type: String, 45 | required: true, 46 | }, 47 | description: { 48 | type: String, 49 | required: true, 50 | }, 51 | // Reviews attribute holds an array of reviews which are of the schema type 'reviewSchema' 52 | reviews: [reviewSchema], 53 | rating: { 54 | type: Number, 55 | required: true, 56 | default: 0, 57 | }, 58 | numReviews: { 59 | type: Number, 60 | required: true, 61 | default: 0, 62 | }, 63 | price: { 64 | type: Number, 65 | required: true, 66 | default: 0, 67 | }, 68 | countInStock: { 69 | type: Number, 70 | required: true, 71 | default: 0, 72 | }, 73 | }, 74 | { 75 | timestamps: true, 76 | } 77 | ) 78 | 79 | const Product = mongoose.model('Product', productSchema) 80 | 81 | export default Product 82 | -------------------------------------------------------------------------------- /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 | // We use the unique attribute to avoid having duplicate emails in the database 11 | email: { 12 | type: String, 13 | required: true, 14 | unique: true, 15 | }, 16 | password: { 17 | type: String, 18 | required: true, 19 | }, 20 | isAdmin: { 21 | type: Boolean, 22 | required: true, 23 | default: false, 24 | }, 25 | }, 26 | { 27 | timestamps: true, 28 | } 29 | ) 30 | 31 | userSchema.methods.matchPassword = async function (enteredPassword) { 32 | return await bcrypt.compare(enteredPassword, this.password) 33 | } 34 | 35 | // Before saving the password into the DB encrypt and hash the password 36 | userSchema.pre('save', async function (next) { 37 | // If the password has not been modified then just move on else just hash the password 38 | if (!this.isModified('password')) { 39 | next() 40 | } 41 | 42 | // Generate a salt with 10 rounds 43 | const salt = await bcrypt.genSalt(10) 44 | // Encrypt and hash password 45 | this.password = await bcrypt.hash(this.password, salt) 46 | }) 47 | 48 | const User = mongoose.model('User', userSchema) 49 | 50 | export default User 51 | -------------------------------------------------------------------------------- /backend/routes/orderRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { 3 | addOrderItems, 4 | getOrderById, 5 | updateOrderToPaid, 6 | getUserOrders, 7 | } from '../controllers/orderController.js' 8 | import { protect } from '../middleware/authMiddleware.js' 9 | 10 | const router = express.Router() 11 | 12 | router.route('/').post(protect, addOrderItems) 13 | router.route('/myorders').get(protect, getUserOrders) 14 | router.route('/:id').get(protect, getOrderById) 15 | router.route('/:id/pay').put(protect, updateOrderToPaid) 16 | 17 | export default router 18 | -------------------------------------------------------------------------------- /backend/routes/productRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { 3 | getProducts, 4 | getProductById, 5 | } from '../controllers/productController.js' 6 | 7 | const router = express.Router() 8 | 9 | 10 | router.route('/').get(getProducts) 11 | 12 | router.route('/:id').get(getProductById) 13 | 14 | export default router 15 | -------------------------------------------------------------------------------- /backend/routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { 3 | authUser, 4 | getUserProfile, 5 | registerUser, 6 | updateUserProfile, 7 | } from '../controllers/userController.js' 8 | import { protect } from '../middleware/authMiddleware.js' 9 | 10 | const router = express.Router() 11 | 12 | router.route('/').post(registerUser) 13 | router.post('/login', authUser) 14 | router 15 | .route('/profile') 16 | .get(protect, getUserProfile) 17 | .put(protect, updateUserProfile) 18 | 19 | export default router 20 | -------------------------------------------------------------------------------- /backend/seeder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Script to import data (not part of actual application) 3 | */ 4 | import mongoose from 'mongoose' 5 | import dotenv from 'dotenv' 6 | import colors from 'colors' 7 | 8 | // Users and product data 9 | import users from './data/users.js' 10 | import products from './data/products.js' 11 | 12 | // Mongoose models 13 | import Product from './models/productModel.js' 14 | import Order from './models/oderModel.js' 15 | import User from './models/userModel.js' 16 | 17 | // Mongoose connection 18 | import connectDB from './config/db.js' 19 | 20 | dotenv.config() 21 | 22 | connectDB() 23 | 24 | const importData = async () => { 25 | try { 26 | await Order.deleteMany() // Delete every order 27 | await Product.deleteMany() // Delete every product 28 | await User.deleteMany() // Delete every user 29 | 30 | // Array of created users 31 | const createdUsers = await User.insertMany(users) 32 | 33 | // Get admin user from array 34 | const adminUser = createdUsers[0].id 35 | 36 | // Get products with the admin user set 37 | const sampleProducts = products.map((product) => { 38 | return { ...product, user: adminUser } 39 | }) 40 | 41 | // Insert all product data with admin user 42 | await Product.insertMany(sampleProducts) 43 | 44 | console.log('Data Imported!'.green.inverse) 45 | process.exit() 46 | } catch (error) { 47 | console.error(`Error: ${error.message}`.red.inverse) 48 | process.exit(1) 49 | } 50 | } 51 | 52 | const destroyData = async () => { 53 | try { 54 | await Order.deleteMany() // Delete every order 55 | await Product.deleteMany() // Delete every product 56 | await User.deleteMany() // Delete every user 57 | 58 | console.log('Data Destroyed!'.red.inverse) 59 | process.exit() 60 | } catch (error) { 61 | console.error(`Error: ${error.message}`.red.inverse) 62 | process.exit(1) 63 | } 64 | } 65 | 66 | // If the script contains the -d flag then the data will be destroyed else it will be imported into MongoDB 67 | if (process.argv[2] == '-d') { 68 | destroyData() 69 | } else { 70 | importData() 71 | } 72 | -------------------------------------------------------------------------------- /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 productRoutes from './routes/productRoutes.js' 6 | import userRoutes from './routes/userRoutes.js' 7 | import orderRoutes from './routes/orderRoutes.js' 8 | import { notFound, errorHandler } from './middleware/errorMiddleware.js' 9 | 10 | // Initialize config file 11 | dotenv.config() 12 | 13 | // Establish connection with MongoDB database. 14 | connectDB() 15 | 16 | const app = express() 17 | 18 | // Body parser to accept JSON data 19 | app.use(express.json()) 20 | 21 | app.get('/', (req, res) => { 22 | res.send('API is running') 23 | }) 24 | 25 | // Product routes 26 | app.use('/api/products', productRoutes) 27 | // User routes 28 | app.use('/api/users', userRoutes) 29 | // Order routes 30 | app.use('/api/orders', orderRoutes) 31 | 32 | // Paypal routes 33 | app.use('/api/config/paypal', (req, res) => 34 | res.send(process.env.PAYPAL_CLIENT_ID) 35 | ) 36 | 37 | // Error handling middleware 38 | app.use(notFound) 39 | app.use(errorHandler) 40 | 41 | const PORT = process.env.PORT || 5000 42 | 43 | app.listen(PORT, () => { 44 | console.log( 45 | `Server running in ${process.env.NODE_ENV} mode on port ${PORT}`.green 46 | .bold 47 | ) 48 | }) 49 | -------------------------------------------------------------------------------- /backend/utils/generateToken.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a JSON web token 3 | */ 4 | import jwt from 'jsonwebtoken' 5 | 6 | const generateToken = (id) => { 7 | return jwt.sign({ id }, process.env.JWT_SECRET, { 8 | expiresIn: '30d', 9 | }) 10 | } 11 | 12 | export default generateToken 13 | -------------------------------------------------------------------------------- /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 | "axios": "^0.20.0", 8 | "cra-template": "1.0.3", 9 | "react": "^16.13.1", 10 | "react-bootstrap": "^1.3.0", 11 | "react-dom": "^16.13.1", 12 | "react-paypal-button-v2": "^2.6.2", 13 | "react-redux": "^7.2.1", 14 | "react-router-bootstrap": "^0.25.0", 15 | "react-router-dom": "^5.2.0", 16 | "react-scripts": "3.4.3", 17 | "redux": "^4.0.5", 18 | "redux-devtools-extension": "^2.13.8", 19 | "redux-thunk": "^2.3.0" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramos07/React-Node-E-Commerce-Store/4d498e9d1bdf3ef396af54a1d1c2b4c21286c9e1/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/images/airpods.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramos07/React-Node-E-Commerce-Store/4d498e9d1bdf3ef396af54a1d1c2b4c21286c9e1/frontend/public/images/airpods.jpg -------------------------------------------------------------------------------- /frontend/public/images/alexa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramos07/React-Node-E-Commerce-Store/4d498e9d1bdf3ef396af54a1d1c2b4c21286c9e1/frontend/public/images/alexa.jpg -------------------------------------------------------------------------------- /frontend/public/images/camera.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramos07/React-Node-E-Commerce-Store/4d498e9d1bdf3ef396af54a1d1c2b4c21286c9e1/frontend/public/images/camera.jpg -------------------------------------------------------------------------------- /frontend/public/images/mouse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramos07/React-Node-E-Commerce-Store/4d498e9d1bdf3ef396af54a1d1c2b4c21286c9e1/frontend/public/images/mouse.jpg -------------------------------------------------------------------------------- /frontend/public/images/phone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramos07/React-Node-E-Commerce-Store/4d498e9d1bdf3ef396af54a1d1c2b4c21286c9e1/frontend/public/images/phone.jpg -------------------------------------------------------------------------------- /frontend/public/images/playstation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramos07/React-Node-E-Commerce-Store/4d498e9d1bdf3ef396af54a1d1c2b4c21286c9e1/frontend/public/images/playstation.jpg -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 20 | 21 | React App 22 | 23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramos07/React-Node-E-Commerce-Store/4d498e9d1bdf3ef396af54a1d1c2b4c21286c9e1/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramos07/React-Node-E-Commerce-Store/4d498e9d1bdf3ef396af54a1d1c2b4c21286c9e1/frontend/public/logo512.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Container } from 'react-bootstrap' 3 | // Routing 4 | import { BrowserRouter as Router, Route } from 'react-router-dom' 5 | 6 | // Components 7 | import Header from './components/Header' 8 | import Footer from './components/Footer' 9 | 10 | // Screens 11 | import HomeScreen from './screens/Home' 12 | import ProductScreen from './screens/Product' 13 | import CartScreen from './screens/Cart' 14 | import LoginScreen from './screens/Login' 15 | import RegisterScreen from './screens/Register' 16 | import ProfileScreen from './screens/Profile' 17 | import ShippingScreen from './screens/Shipping' 18 | import PaymentScreen from './screens/Payment' 19 | import PlaceOrderScreen from './screens/PlaceOrder' 20 | import OrderScreen from './screens/Order' 21 | 22 | // History 23 | import history from './utils/history' 24 | 25 | const App = () => { 26 | return ( 27 | 28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |