├── .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 |
44 |
45 | )
46 | }
47 |
48 | export default App
49 |
--------------------------------------------------------------------------------
/frontend/src/actions/cartActions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { CART_ADD_ITEM, CART_REMOVE_ITEM, CART_SAVE_SHIPPING_ADDRESS, CART_SAVE_PAYMENT_METHOD } from '../constants/cartConstants'
3 |
4 | export const addToCart = (id, qty) => async (dispatch, getState) => {
5 | const { data } = await axios.get(`/api/products/${id}`)
6 | dispatch({
7 | type: CART_ADD_ITEM,
8 | payload: {
9 | product: data._id,
10 | name: data.name,
11 | image: data.image,
12 | price: data.price,
13 | countInStock: data.countInStock,
14 | qty,
15 | },
16 | })
17 | localStorage.setItem('cartItems', JSON.stringify(getState().cart.cartItems))
18 | }
19 |
20 | export const removeFromCart = (id) => (dispatch, getState) => {
21 | dispatch({
22 | type: CART_REMOVE_ITEM,
23 | payload: id,
24 | })
25 | localStorage.setItem('cartItems', JSON.stringify(getState().cart.cartItems))
26 | }
27 |
28 | export const saveShippingAddress = (data) => (dispatch) => {
29 | dispatch({
30 | type: CART_SAVE_SHIPPING_ADDRESS,
31 | payload: data,
32 | })
33 | localStorage.setItem('shippingAddress', JSON.stringify(data))
34 | }
35 |
36 | export const savePaymentMethod = (data) => (dispatch) => {
37 | dispatch({
38 | type: CART_SAVE_PAYMENT_METHOD,
39 | payload: data,
40 | })
41 | localStorage.setItem('paymentMethod', JSON.stringify(data))
42 | }
--------------------------------------------------------------------------------
/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 | ORDER_PAY_REQUEST,
9 | ORDER_PAY_SUCCESS,
10 | ORDER_PAY_FAIL,
11 | ORDER_LIST_USER_REQUEST,
12 | ORDER_LIST_USER_SUCCESS,
13 | ORDER_LIST_USER_FAIL,
14 | } from '../constants/orderConstants'
15 | import axios from 'axios'
16 |
17 | export const createOrder = (order) => async (dispatch, getState) => {
18 | try {
19 | dispatch({
20 | type: ORDER_CREATE_REQUEST,
21 | })
22 |
23 | // Get user login and user info
24 | const {
25 | userLogin: { userInfo },
26 | } = getState()
27 |
28 | // Header to send with the request
29 | const config = {
30 | headers: {
31 | 'Content-Type': 'application/json',
32 | Authorization: `Bearer ${userInfo.token}`,
33 | },
34 | }
35 |
36 | // Make request to server and get the response data
37 | const { data } = await axios.post(`/api/orders`, order, config)
38 |
39 | // Dispatch user order success after making the request
40 | dispatch({
41 | type: ORDER_CREATE_SUCCESS,
42 | payload: data,
43 | })
44 | } catch (error) {
45 | dispatch({
46 | type: ORDER_CREATE_FAIL,
47 | payload:
48 | error.response && error.response.data.message
49 | ? error.response.data.message
50 | : error.message,
51 | })
52 | }
53 | }
54 |
55 | export const getOrderDetails = (orderId) => async (dispatch, getState) => {
56 | try {
57 | dispatch({
58 | type: ORDER_DETAILS_REQUEST,
59 | })
60 |
61 | // Get user login to get the bearertoken
62 | const {
63 | userLogin: { userInfo },
64 | } = getState()
65 |
66 | // Header to send with the request
67 | const config = {
68 | headers: {
69 | Authorization: `Bearer ${userInfo.token}`,
70 | },
71 | }
72 |
73 | // Make request to server and get the response data
74 | const { data } = await axios.get(`/api/orders/${orderId}`, config)
75 |
76 | // Dispatch user order success after making the request
77 | dispatch({
78 | type: ORDER_DETAILS_SUCCESS,
79 | payload: data,
80 | })
81 | } catch (error) {
82 | dispatch({
83 | type: ORDER_DETAILS_FAIL,
84 | payload:
85 | error.response && error.response.data.message
86 | ? error.response.data.message
87 | : error.message,
88 | })
89 | }
90 | }
91 |
92 | export const payOrder = (orderId, paymentResult) => async (
93 | dispatch,
94 | getState
95 | ) => {
96 | try {
97 | dispatch({
98 | type: ORDER_PAY_REQUEST,
99 | })
100 |
101 | // Get user login to get the bearer token
102 | const {
103 | userLogin: { userInfo },
104 | } = getState()
105 |
106 | // Header to send with the request
107 | const config = {
108 | headers: {
109 | 'Content-Type': 'application/json',
110 | Authorization: `Bearer ${userInfo.token}`,
111 | },
112 | }
113 |
114 | // Make request to server and get the response data
115 | const { data } = await axios.put(
116 | `/api/orders/${orderId}/pay`,
117 | paymentResult,
118 | config
119 | )
120 |
121 | // Dispatch user order pay success after making the request
122 | dispatch({
123 | type: ORDER_PAY_SUCCESS,
124 | payload: data,
125 | })
126 | } catch (error) {
127 | dispatch({
128 | type: ORDER_PAY_FAIL,
129 | payload:
130 | error.response && error.response.data.message
131 | ? error.response.data.message
132 | : error.message,
133 | })
134 | }
135 | }
136 |
137 | export const listUserOrders = () => async (dispatch, getState) => {
138 | try {
139 | dispatch({
140 | type: ORDER_LIST_USER_REQUEST,
141 | })
142 |
143 | // Get user login to get the bearer token
144 | const {
145 | userLogin: { userInfo },
146 | } = getState()
147 |
148 | // Header to send with the request
149 | const config = {
150 | headers: {
151 | Authorization: `Bearer ${userInfo.token}`,
152 | },
153 | }
154 |
155 | // Make request to server and get the response data
156 | const { data } = await axios.get(`/api/orders/myorders`, config)
157 |
158 | // Dispatch user order pay success after making the request
159 | dispatch({
160 | type: ORDER_LIST_USER_SUCCESS,
161 | payload: data,
162 | })
163 | } catch (error) {
164 | dispatch({
165 | type: ORDER_LIST_USER_FAIL,
166 | payload:
167 | error.response && error.response.data.message
168 | ? error.response.data.message
169 | : error.message,
170 | })
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/frontend/src/actions/productActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | PRODUCT_LIST_REQUEST,
3 | PRODUCT_LIST_SUCCESS,
4 | PRODUCT_LIST_FAIL,
5 | PRODUCT_DETAILS_FAIL,
6 | PRODUCT_DETAILS_REQUEST,
7 | PRODUCT_DETAILS_SUCCESS,
8 | } from '../constants/productConstants'
9 | import axios from 'axios'
10 |
11 | export const listProducts = () => async (dispatch) => {
12 | try {
13 | dispatch({
14 | type: PRODUCT_LIST_REQUEST,
15 | })
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({
36 | type: PRODUCT_DETAILS_REQUEST,
37 | })
38 | const { data } = await axios.get(`/api/products/${id}`)
39 |
40 | dispatch({
41 | type: PRODUCT_DETAILS_SUCCESS,
42 | payload: data,
43 | })
44 | } catch (error) {
45 | dispatch({
46 | type: PRODUCT_DETAILS_FAIL,
47 | payload:
48 | error.response && error.response.data.message
49 | ? error.response.data.message
50 | : error.message,
51 | })
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/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 | USER_DETAILS_RESET,
16 | } from '../constants/userConstants'
17 | import { ORDER_LIST_USER_RESET } from '../constants/orderConstants'
18 | import axios from 'axios'
19 |
20 | export const login = (email, password) => async (dispatch) => {
21 | try {
22 | dispatch({
23 | type: USER_LOGIN_REQUEST,
24 | })
25 |
26 | // Header to send with the request
27 | const config = {
28 | headers: {
29 | 'Content-Type': 'application/json',
30 | },
31 | }
32 |
33 | // Make request to server and get the response data
34 | const { data } = await axios.post(
35 | '/api/users/login',
36 | { email, password },
37 | config
38 | )
39 |
40 | // Dispatch user login success after making the request
41 | dispatch({
42 | type: USER_LOGIN_SUCCESS,
43 | payload: data,
44 | })
45 |
46 | // Set user data to local storage
47 | localStorage.setItem('userInfo', JSON.stringify(data))
48 | } catch (error) {
49 | dispatch({
50 | type: USER_LOGIN_FAIL,
51 | payload:
52 | error.response && error.response.data.message
53 | ? error.response.data.message
54 | : error.message,
55 | })
56 | }
57 | }
58 |
59 | export const logout = () => (dispatch) => {
60 | localStorage.removeItem('userInfo')
61 | localStorage.removeItem('cartItems')
62 | localStorage.removeItem('shippingAddress')
63 | localStorage.removeItem('paymentMethod')
64 | dispatch({
65 | type: USER_LOGOUT,
66 | })
67 |
68 | dispatch({
69 | type: USER_DETAILS_RESET,
70 | })
71 |
72 | dispatch({
73 | type: ORDER_LIST_USER_RESET,
74 | })
75 | }
76 |
77 | export const register = (name, email, password) => async (dispatch) => {
78 | try {
79 | dispatch({
80 | type: USER_REGISTER_REQUEST,
81 | })
82 |
83 | // Header to send with the request
84 | const config = {
85 | headers: {
86 | 'Content-Type': 'application/json',
87 | },
88 | }
89 |
90 | // Make request to server and get the response data
91 | const { data } = await axios.post(
92 | '/api/users',
93 | { name, email, password },
94 | config
95 | )
96 |
97 | // Dispatch user register success after making the request
98 | dispatch({
99 | type: USER_REGISTER_SUCCESS,
100 | payload: data,
101 | })
102 | // Login in the user as well after registering
103 | dispatch({
104 | type: USER_LOGIN_SUCCESS,
105 | payload: data,
106 | })
107 |
108 | // Set user data to local storage
109 | localStorage.setItem('userInfo', JSON.stringify(data))
110 | } catch (error) {
111 | dispatch({
112 | type: USER_REGISTER_FAIL,
113 | payload:
114 | error.response && error.response.data.message
115 | ? error.response.data.message
116 | : error.message,
117 | })
118 | }
119 | }
120 |
121 | export const getUserDetails = (id) => async (dispatch, getState) => {
122 | try {
123 | dispatch({
124 | type: USER_DETAILS_REQUEST,
125 | })
126 |
127 | // Get user login and user info
128 | const {
129 | userLogin: { userInfo },
130 | } = getState()
131 |
132 | // Header to send with the request
133 | const config = {
134 | headers: {
135 | 'Content-Type': 'application/json',
136 | Authorization: `Bearer ${userInfo.token}`,
137 | },
138 | }
139 |
140 | // Make request to server and get the response data
141 | const { data } = await axios.get(`/api/users/${id}`, config)
142 |
143 | // Dispatch user register success after making the request
144 | dispatch({
145 | type: USER_DETAILS_SUCCESS,
146 | payload: data,
147 | })
148 | } catch (error) {
149 | dispatch({
150 | type: USER_DETAILS_FAIL,
151 | payload:
152 | error.response && error.response.data.message
153 | ? error.response.data.message
154 | : error.message,
155 | })
156 | }
157 | }
158 |
159 | export const updateUserProfile = (user) => async (dispatch, getState) => {
160 | try {
161 | dispatch({
162 | type: USER_UPDATE_PROFILE_REQUEST,
163 | })
164 |
165 | // Get user login and user info
166 | const {
167 | userLogin: { userInfo },
168 | } = getState()
169 |
170 | // Header to send with the request
171 | const config = {
172 | headers: {
173 | 'Content-Type': 'application/json',
174 | Authorization: `Bearer ${userInfo.token}`,
175 | },
176 | }
177 |
178 | // Make request to server and get the response data
179 | const { data } = await axios.put(`/api/users/profile`, user, config)
180 |
181 | // Dispatch user register success after making the request
182 | dispatch({
183 | type: USER_UPDATE_PROFILE_SUCCESS,
184 | payload: data,
185 | })
186 | } catch (error) {
187 | dispatch({
188 | type: USER_UPDATE_PROFILE_FAIL,
189 | payload:
190 | error.response && error.response.data.message
191 | ? error.response.data.message
192 | : error.message,
193 | })
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/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 |
45 | )
46 | }
47 |
48 | export default CheckoutSteps
49 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Container, Row } from 'react-bootstrap'
3 |
4 | const Footer = () => {
5 | return (
6 |
11 | )
12 | }
13 |
14 | export default Footer
15 |
--------------------------------------------------------------------------------
/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/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | // Bootstrap UI Components
3 | import { Navbar, Nav, Container, NavDropdown } from 'react-bootstrap'
4 | import { LinkContainer } from 'react-router-bootstrap'
5 | // React Redux
6 | import { useDispatch, useSelector } from 'react-redux'
7 | // Redux User Actions
8 | import { logout } from '../actions/userActions'
9 |
10 | const Header = () => {
11 | const dispatch = useDispatch()
12 |
13 | const userLogin = useSelector((state) => state.userLogin)
14 | const { userInfo } = userLogin
15 |
16 | const logoutHandler = () => {
17 | dispatch(logout())
18 | }
19 |
20 | return (
21 |
22 |
23 |
29 |
30 |
31 | React E-Commerce
32 |
33 |
34 |
35 |
67 |
68 |
69 |
70 |
71 |
72 | )
73 | }
74 |
75 | export default Header
76 |
--------------------------------------------------------------------------------
/frontend/src/components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Spinner } from 'react-bootstrap'
3 |
4 | const Loader = () => {
5 | return (
6 |
16 | Loading
17 |
18 | )
19 | }
20 |
21 | export default Loader
22 |
--------------------------------------------------------------------------------
/frontend/src/components/Message.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Alert } from 'react-bootstrap'
3 |
4 | const Message = ({ variant, children }) => {
5 | return {children}
6 | }
7 |
8 | Message.defaultProps = {
9 | variant: 'info',
10 | }
11 |
12 | export default Message
13 |
--------------------------------------------------------------------------------
/frontend/src/components/Product.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Card } from 'react-bootstrap'
3 | import Rating from './Rating'
4 |
5 | import { Link } from 'react-router-dom'
6 |
7 | const Product = ({ product }) => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {product.name}
17 |
18 |
19 |
20 |
24 |
25 | ${product.price}
26 |
27 |
28 | )
29 | }
30 |
31 | export default Product
32 |
--------------------------------------------------------------------------------
/frontend/src/components/Rating.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Rating = ({ value, text, color }) => {
5 | return (
6 |
7 |
8 | = 1
12 | ? 'fas fa-star'
13 | : value > 0.5
14 | ? 'fas fa-star-half-alt'
15 | : 'far fa-star'
16 | }
17 | >
18 |
19 |
20 | = 2
24 | ? 'fas fa-star'
25 | : value > 1.5
26 | ? 'fas fa-star-half-alt'
27 | : 'far fa-star'
28 | }
29 | >
30 |
31 |
32 | = 3
36 | ? 'fas fa-star'
37 | : value > 2.5
38 | ? 'fas fa-star-half-alt'
39 | : 'far fa-star'
40 | }
41 | >
42 |
43 |
44 | = 4
48 | ? 'fas fa-star'
49 | : value > 3.5
50 | ? 'fas fa-star-half-alt'
51 | : 'far fa-star'
52 | }
53 | >
54 |
55 |
56 | = 5
60 | ? 'fas fa-star'
61 | : value > 4.5
62 | ? 'fas fa-star-half-alt'
63 | : 'far fa-star'
64 | }
65 | >
66 |
67 | {text && text}
68 |
69 | )
70 | }
71 |
72 | // Default value for color
73 | Rating.defaultProps = {
74 | color: '#f8e825',
75 | }
76 |
77 | Rating.propTypes = {
78 | value: PropTypes.number.isRequired,
79 | text: PropTypes.string.isRequired,
80 | color: PropTypes.string,
81 | }
82 |
83 | export default Rating
84 |
--------------------------------------------------------------------------------
/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 |
5 | export const CART_SAVE_PAYMENT_METHOD = 'CART_SAVE_PAYMENT_METHOD'
--------------------------------------------------------------------------------
/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 | export const ORDER_CREATE_RESET = 'ORDER_CREATE_RESET'
5 |
6 | export const ORDER_DETAILS_REQUEST = 'ORDER_DETAILS_REQUEST'
7 | export const ORDER_DETAILS_SUCCESS = 'ORDER_DETAILS_SUCCESS'
8 | export const ORDER_DETAILS_FAIL = 'ORDER_DETAILS_FAIL'
9 |
10 | export const ORDER_PAY_REQUEST = 'ORDER_PAY_REQUEST'
11 | export const ORDER_PAY_SUCCESS = 'ORDER_PAY_SUCCESS'
12 | export const ORDER_PAY_FAIL = 'ORDER_PAY_FAIL'
13 | export const ORDER_PAY_RESET = 'ORDER_PAY_RESET'
14 |
15 | export const ORDER_LIST_USER_REQUEST = 'ORDER_LIST_USER_REQUEST'
16 | export const ORDER_LIST_USER_SUCCESS = 'ORDER_LIST_USER_SUCCESS'
17 | export const ORDER_LIST_USER_FAIL = 'ORDER_LIST_USER_FAIL'
18 | export const ORDER_LIST_USER_RESET = 'ORDER_LIST_USER_REST'
19 |
--------------------------------------------------------------------------------
/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/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 |
5 | export const USER_LOGOUT = 'USER_LOGOUT'
6 |
7 | export const USER_REGISTER_REQUEST = 'USER_REGISTER_REQUEST'
8 | export const USER_REGISTER_SUCCESS = 'USER_REGISTER_SUCCESS'
9 | export const USER_REGISTER_FAIL = 'USER_REGISTER_FAIL'
10 |
11 | export const USER_DETAILS_REQUEST = 'USER_DETAILS_REQUEST'
12 | export const USER_DETAILS_SUCCESS = 'USER_DETAILS_SUCCESS'
13 | export const USER_DETAILS_FAIL = 'USER_DETAILS_FAIL'
14 | export const USER_DETAILS_RESET = 'USER_DETAILS_RESET'
15 |
16 | export const USER_UPDATE_PROFILE_REQUEST = 'USER_DETAILS_PROFILE_REQUEST'
17 | export const USER_UPDATE_PROFILE_SUCCESS = 'USER_UPDATE_PROFILE_SUCCESS'
18 | export const USER_UPDATE_PROFILE_FAIL = 'USER_UPDATE_PROFILE_FAIL'
19 | export const USER_UPDATE_PROFILE_RESET = 'USER_UPDATE_PROFILE_RESET'
20 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | main {
2 | min-height: 80vh;
3 | }
4 |
5 | h3 {
6 | padding: 1rem 0;
7 | }
8 |
9 | h1 {
10 | font-size: 1.8rem;
11 | padding: 1rem 0;
12 | }
13 |
14 | h2 {
15 | font-size: 1.4rem;
16 | padding: 0.5rem 0;
17 | }
18 |
19 | .rating span {
20 | margin: 0.1rem;
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { Provider } from 'react-redux'
4 | import store from './store'
5 | import './bootstrap.min.css'
6 | import './index.css'
7 | import App from './App'
8 | import * as serviceWorker from './serviceWorker'
9 |
10 | ReactDOM.render(
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | )
16 |
17 | // If you want your app to work offline and load faster, you can change
18 | // unregister() to register() below. Note this comes with some pitfalls.
19 | // Learn more about service workers: https://bit.ly/CRA-PWA
20 | serviceWorker.unregister()
21 |
--------------------------------------------------------------------------------
/frontend/src/products.js:
--------------------------------------------------------------------------------
1 | const products = [
2 | {
3 | _id: '1',
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 | _id: '2',
17 | name: 'iPhone 11 Pro 256GB Memory',
18 | image: '/images/phone.jpg',
19 | description:
20 | 'Introducing the iPhone 11 Pro. A transformative triple-camera system that adds tons of capability without complexity. An unprecedented leap in battery life',
21 | brand: 'Apple',
22 | category: 'Electronics',
23 | price: 599.99,
24 | countInStock: 7,
25 | rating: 4.0,
26 | numReviews: 8,
27 | },
28 | {
29 | _id: '3',
30 | name: 'Cannon EOS 80D DSLR Camera',
31 | image: '/images/camera.jpg',
32 | description:
33 | 'Characterized by versatile imaging specs, the Canon EOS 80D further clarifies itself using a pair of robust focusing systems and an intuitive design',
34 | brand: 'Cannon',
35 | category: 'Electronics',
36 | price: 929.99,
37 | countInStock: 5,
38 | rating: 3,
39 | numReviews: 12,
40 | },
41 | {
42 | _id: '4',
43 | name: 'Sony Playstation 4 Pro White Version',
44 | image: '/images/playstation.jpg',
45 | description:
46 | 'The ultimate home entertainment center starts with PlayStation. Whether you are into gaming, HD movies, television, music',
47 | brand: 'Sony',
48 | category: 'Electronics',
49 | price: 399.99,
50 | countInStock: 11,
51 | rating: 5,
52 | numReviews: 12,
53 | },
54 | {
55 | _id: '5',
56 | name: 'Logitech G-Series Gaming Mouse',
57 | image: '/images/mouse.jpg',
58 | description:
59 | 'Get a better handle on your games with this Logitech LIGHTSYNC gaming mouse. The six programmable buttons allow customization for a smooth playing experience',
60 | brand: 'Logitech',
61 | category: 'Electronics',
62 | price: 49.99,
63 | countInStock: 7,
64 | rating: 3.5,
65 | numReviews: 10,
66 | },
67 | {
68 | _id: '6',
69 | name: 'Amazon Echo Dot 3rd Generation',
70 | image: '/images/alexa.jpg',
71 | description:
72 | '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',
73 | brand: 'Amazon',
74 | category: 'Electronics',
75 | price: 29.99,
76 | countInStock: 0,
77 | rating: 4,
78 | numReviews: 12,
79 | },
80 | ]
81 |
82 | export default products
83 |
--------------------------------------------------------------------------------
/frontend/src/reducers/cartReducers.js:
--------------------------------------------------------------------------------
1 | import { CART_ADD_ITEM, CART_REMOVE_ITEM, CART_SAVE_SHIPPING_ADDRESS, CART_SAVE_PAYMENT_METHOD } from '../constants/cartConstants'
2 |
3 | export const cartReducer = (state = { cartItems: [], shippingAddress: {} }, action) => {
4 | switch (action.type) {
5 | case CART_ADD_ITEM:
6 | const item = action.payload
7 | const existItem = state.cartItems.find(
8 | (x) => x.product === item.product
9 | )
10 |
11 | if (existItem) {
12 | return {
13 | ...state,
14 | cartItems: state.cartItems.map((x) =>
15 | x.product === existItem.product ? item : x
16 | ),
17 | }
18 | } else {
19 | return {
20 | ...state,
21 | cartItems: [...state.cartItems, item],
22 | }
23 | }
24 | case CART_REMOVE_ITEM:
25 | return {
26 | ...state,
27 | cartItems: state.cartItems.filter(
28 | (x) => x.product !== action.payload
29 | ),
30 | }
31 | case CART_SAVE_SHIPPING_ADDRESS:
32 | return {
33 | ...state,
34 | shippingAddress: action.payload,
35 | }
36 | case CART_SAVE_PAYMENT_METHOD:
37 | return {
38 | ...state,
39 | paymentMethod: action.payload
40 | }
41 | default:
42 | return state
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/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 | ORDER_PAY_REQUEST,
9 | ORDER_PAY_FAIL,
10 | ORDER_PAY_SUCCESS,
11 | ORDER_PAY_RESET,
12 | ORDER_LIST_USER_REQUEST,
13 | ORDER_LIST_USER_SUCCESS,
14 | ORDER_LIST_USER_FAIL,
15 | ORDER_LIST_USER_RESET,
16 | ORDER_CREATE_RESET,
17 | } from '../constants/orderConstants'
18 |
19 | export const createOrderReducer = (state = {}, action) => {
20 | switch (action.type) {
21 | case ORDER_CREATE_REQUEST:
22 | return {
23 | loading: true,
24 | }
25 | case ORDER_CREATE_SUCCESS:
26 | return {
27 | loading: false,
28 | success: true,
29 | order: action.payload,
30 | }
31 | case ORDER_CREATE_FAIL:
32 | return {
33 | loading: false,
34 | error: action.payload,
35 | }
36 | case ORDER_CREATE_RESET:
37 | return {}
38 | default:
39 | return state
40 | }
41 | }
42 |
43 | export const orderDetailsReducer = (
44 | state = { loading: true, orderItems: [], shippingAddress: {} },
45 | action
46 | ) => {
47 | switch (action.type) {
48 | case ORDER_DETAILS_REQUEST:
49 | return {
50 | ...state,
51 | loading: true,
52 | }
53 | case ORDER_DETAILS_SUCCESS:
54 | return {
55 | loading: false,
56 | order: action.payload,
57 | }
58 | case ORDER_DETAILS_FAIL:
59 | return {
60 | loading: false,
61 | error: action.payload,
62 | }
63 |
64 | default:
65 | return state
66 | }
67 | }
68 |
69 | export const orderPayReducer = (state = {}, action) => {
70 | switch (action.type) {
71 | case ORDER_PAY_REQUEST:
72 | return {
73 | ...state,
74 | loading: true,
75 | }
76 | case ORDER_PAY_SUCCESS:
77 | return {
78 | loading: false,
79 | success: true,
80 | order: action.payload,
81 | }
82 | case ORDER_PAY_FAIL:
83 | return {
84 | loading: false,
85 | error: action.payload,
86 | }
87 | case ORDER_PAY_RESET:
88 | return {}
89 | default:
90 | return state
91 | }
92 | }
93 |
94 | export const orderListUserReducer = (state = { orders: [] }, action) => {
95 | switch (action.type) {
96 | case ORDER_LIST_USER_REQUEST:
97 | return {
98 | ...state,
99 | loading: true,
100 | }
101 | case ORDER_LIST_USER_SUCCESS:
102 | return {
103 | loading: false,
104 | orders: action.payload,
105 | }
106 | case ORDER_LIST_USER_FAIL:
107 | return {
108 | loading: false,
109 | error: action.payload,
110 | }
111 | case ORDER_LIST_USER_RESET:
112 | return {
113 | orders: [],
114 | }
115 | default:
116 | return state
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/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 | // Getting products from server
11 | export const productListReducer = (state = { products: [] }, action) => {
12 | switch (action.type) {
13 | case PRODUCT_LIST_REQUEST:
14 | return {
15 | loading: true,
16 | products: [],
17 | }
18 | case PRODUCT_LIST_SUCCESS:
19 | return {
20 | loading: false,
21 | products: action.payload,
22 | }
23 | case PRODUCT_LIST_FAIL:
24 | return {
25 | loading: false,
26 | error: action.payload,
27 | }
28 | default:
29 | return state
30 | }
31 | }
32 |
33 | // Get products details
34 | export const productDetailsReducer = (
35 | state = { product: { reviews: [] } },
36 | action
37 | ) => {
38 | switch (action.type) {
39 | case PRODUCT_DETAILS_REQUEST:
40 | return {
41 | loading: true,
42 | ...state,
43 | }
44 | case PRODUCT_DETAILS_SUCCESS:
45 | return {
46 | loading: false,
47 | product: action.payload,
48 | }
49 | case PRODUCT_DETAILS_FAIL:
50 | return {
51 | loading: false,
52 | error: action.payload,
53 | }
54 | default:
55 | return state
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/frontend/src/reducers/userReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | USER_DETAILS_FAIL,
3 | USER_DETAILS_REQUEST,
4 | USER_DETAILS_RESET,
5 | USER_DETAILS_SUCCESS,
6 | USER_LOGIN_FAIL,
7 | USER_LOGIN_REQUEST,
8 | USER_LOGIN_SUCCESS,
9 | USER_LOGOUT,
10 | USER_REGISTER_FAIL,
11 | USER_REGISTER_REQUEST,
12 | USER_REGISTER_SUCCESS,
13 | USER_UPDATE_PROFILE_FAIL,
14 | USER_UPDATE_PROFILE_REQUEST,
15 | USER_UPDATE_PROFILE_SUCCESS,
16 | } from '../constants/userConstants'
17 |
18 | // Getting a user from server
19 | export const userLoginReducer = (state = {}, action) => {
20 | switch (action.type) {
21 | case USER_LOGIN_REQUEST:
22 | return {
23 | loading: true,
24 | }
25 | case USER_LOGIN_SUCCESS:
26 | return {
27 | loading: false,
28 | userInfo: action.payload,
29 | }
30 | case USER_LOGIN_FAIL:
31 | return {
32 | loading: false,
33 | error: action.payload,
34 | }
35 | case USER_LOGOUT:
36 | return {}
37 | default:
38 | return state
39 | }
40 | }
41 |
42 | export const userRegisterReducer = (state = {}, action) => {
43 | switch (action.type) {
44 | case USER_REGISTER_REQUEST:
45 | return {
46 | loading: true,
47 | }
48 | case USER_REGISTER_SUCCESS:
49 | return {
50 | loading: false,
51 | userInfo: action.payload,
52 | }
53 | case USER_REGISTER_FAIL:
54 | return {
55 | loading: false,
56 | error: action.payload,
57 | }
58 | case USER_LOGOUT:
59 | return {}
60 | default:
61 | return state
62 | }
63 | }
64 |
65 | export const userDetailsReducer = (state = { user: {} }, action) => {
66 | switch (action.type) {
67 | case USER_DETAILS_REQUEST:
68 | return {
69 | ...state,
70 | loading: true,
71 | }
72 | case USER_DETAILS_SUCCESS:
73 | return {
74 | loading: false,
75 | user: action.payload,
76 | }
77 | case USER_DETAILS_FAIL:
78 | return {
79 | loading: false,
80 | error: action.payload,
81 | }
82 | case USER_DETAILS_RESET:
83 | return {
84 | user: {},
85 | }
86 | default:
87 | return state
88 | }
89 | }
90 |
91 | export const userUpdateProfileReducer = (state = {}, action) => {
92 | switch (action.type) {
93 | case USER_UPDATE_PROFILE_REQUEST:
94 | return {
95 | loading: true,
96 | }
97 | case USER_UPDATE_PROFILE_SUCCESS:
98 | return {
99 | loading: false,
100 | success: true,
101 | userInfo: action.payload,
102 | }
103 | case USER_UPDATE_PROFILE_FAIL:
104 | return {
105 | loading: false,
106 | error: action.payload,
107 | }
108 | default:
109 | return state
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/frontend/src/screens/Cart.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | // React Bootstrap
4 | import { Row, Col, ListGroup, Image, Form, Button, Card } from 'react-bootstrap'
5 | // Redux state
6 | import { useDispatch, useSelector } from 'react-redux'
7 | // Components
8 | import Message from '../components/Message'
9 | // Redux Actions
10 | import { addToCart, removeFromCart } from '../actions/cartActions'
11 |
12 | const Cart = ({ match, location, history }) => {
13 | const productId = match.params.id
14 |
15 | const qty = location.search ? Number(location.search.split('=')[1]) : 1
16 |
17 | const dispatch = useDispatch()
18 |
19 | const cart = useSelector((state) => state.cart)
20 |
21 | const { cartItems } = cart
22 |
23 | useEffect(() => {
24 | if (productId) {
25 | dispatch(addToCart(productId, qty))
26 | }
27 | }, [dispatch, productId, qty])
28 |
29 | const removeFromCartHandler = (id) => {
30 | // console.log('remove')
31 | dispatch(removeFromCart(id))
32 | }
33 |
34 | const checkoutHandler = () => {
35 | history.push('/login?redirect=shipping')
36 | }
37 |
38 | return (
39 |
40 |
41 | Shopping Cart
42 | {cartItems.length === 0 ? (
43 |
44 | Your cart is empty Go back
45 |
46 | ) : (
47 |
48 | {cartItems.map((item) => (
49 |
50 |
51 |
52 |
58 |
59 |
60 |
61 | {item.name}
62 |
63 |
64 | ${item.price}
65 |
66 |
70 | dispatch(
71 | addToCart(
72 | item.product,
73 | Number(e.target.value)
74 | )
75 | )
76 | }
77 | >
78 | {[
79 | ...Array(
80 | item.countInStock
81 | ).keys(),
82 | ].map((p) => (
83 |
89 | ))}
90 |
91 |
92 |
93 |
104 |
105 |
106 |
107 | ))}
108 |
109 | )}
110 |
111 |
112 |
113 |
114 |
115 |
116 | Subtotal (
117 | {cartItems.reduce(
118 | (acc, item) => acc + item.qty,
119 | 0
120 | )}
121 | ) items
122 |
123 | $
124 | {cartItems
125 | .reduce(
126 | (acc, item) => acc + item.qty * item.price,
127 | 0
128 | )
129 | .toFixed(2)}
130 |
131 |
132 |
140 |
141 |
142 |
143 |
144 |
145 | )
146 | }
147 |
148 | export default Cart
149 |
--------------------------------------------------------------------------------
/frontend/src/screens/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useDispatch, useSelector } from 'react-redux'
3 | import { Row, Col } from 'react-bootstrap'
4 | // Components
5 | import Product from '../components/Product'
6 | import Message from '../components/Message'
7 | import Loader from '../components/Loader'
8 | // Redux
9 | import { listProducts } from '../actions/productActions'
10 |
11 | const Home = () => {
12 | const dispatch = useDispatch()
13 | // Grab the data from the state
14 | const productList = useSelector((state) => state.productList)
15 | const { loading, error, products } = productList
16 |
17 | // Whatever is put inside the useEffect function will run as soon as the component loads.
18 | useEffect(() => {
19 | // Dispatch the list products action and fill our state
20 | dispatch(listProducts())
21 | }, [dispatch])
22 |
23 | return (
24 | <>
25 | Latest products
26 | {loading ? (
27 |
28 | ) : error ? (
29 | {error}
30 | ) : (
31 |
32 | {products.map((product) => (
33 |
34 |
35 |
36 | ))}
37 |
38 | )}
39 | >
40 | )
41 | }
42 |
43 | export default Home
44 |
--------------------------------------------------------------------------------
/frontend/src/screens/Login.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | // Bootstrap Components
4 | import { Form, Button, Row, Col } from 'react-bootstrap'
5 | // Redux
6 | import { useDispatch, useSelector } from 'react-redux'
7 | // UI Components
8 | import Message from '../components/Message'
9 | import Loader from '../components/Loader'
10 | import FormContainer from '../components/FormContainer'
11 | // Redux Actions
12 | import { login } from '../actions/userActions'
13 |
14 | const Login = ({ location, history }) => {
15 | // State to hold email and password
16 | const [email, setEmail] = useState('')
17 | const [password, setPassword] = useState('')
18 |
19 | const dispatch = useDispatch()
20 |
21 | // Get user login info from Redux state
22 | const userLogin = useSelector((state) => state.userLogin)
23 | const { loading, error, userInfo } = userLogin
24 |
25 | const redirect = location.search ? location.search.split('=')[1] : '/'
26 |
27 | useEffect(() => {
28 | // If there is user info then redirect
29 | if (userInfo) {
30 | history.push(redirect)
31 | }
32 | }, [history, userInfo, redirect])
33 |
34 | // Handler that logs in the user
35 | const submitHandler = (e) => {
36 | e.preventDefault()
37 |
38 | // Dispatch login
39 | dispatch(login(email, password))
40 | }
41 |
42 | return (
43 |
44 | Sign In
45 | {error && {error}}
46 | {loading && }
47 |
49 | Email Address
50 | setEmail(e.target.value)}
55 | >
56 |
57 |
58 | Password
59 | setPassword(e.target.value)}
64 | >
65 |
66 |
69 |
70 |
71 |
72 | New Customer{' '}
73 |
80 | Register
81 |
82 |
83 |
84 |
85 | )
86 | }
87 |
88 | export default Login
89 |
--------------------------------------------------------------------------------
/frontend/src/screens/Order.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { Button, Row, Col, ListGroup, Image, Card } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import Message from '../components/Message'
5 | import Loader from '../components/Loader'
6 | import { Link } from 'react-router-dom'
7 | import axios from 'axios'
8 | import { PayPalButton } from 'react-paypal-button-v2'
9 | import { ORDER_PAY_RESET } from '../constants/orderConstants'
10 |
11 | // Get order details actions
12 | import { getOrderDetails, payOrder } from '../actions/orderActions'
13 |
14 | const Order = ({ match, history }) => {
15 | // Get order id parameter from URL
16 | const orderId = match.params.id
17 |
18 | // Boolean used to determine if the PayPal SDK has loaded
19 | const [sdkReady, setSdkReady] = useState(false)
20 |
21 | const dispatch = useDispatch()
22 |
23 | // Get order details from state
24 | const orderDetails = useSelector((state) => state.orderDetails)
25 | const { order, loading, error } = orderDetails
26 |
27 | // Get order payment status
28 | const orderPay = useSelector((state) => state.orderPay)
29 | const { loading: loadingPay, success: successPay } = orderPay
30 |
31 | if (!loading) {
32 | const addDecimals = (num) => {
33 | return (Math.round(num * 100) / 100).toFixed(2)
34 | }
35 |
36 | order.itemsPrice = addDecimals(
37 | order.orderItems.reduce(
38 | (acc, item) => acc + item.price * item.qty,
39 | 0
40 | )
41 | )
42 |
43 | order.shippingPrice = addDecimals(order.shippingPrice)
44 |
45 | order.taxPrice = addDecimals(order.taxPrice)
46 | }
47 |
48 | useEffect(() => {
49 | const addPayPalScript = async () => {
50 | const { data: clientId } = await axios.get('/api/config/paypal')
51 | const script = document.createElement('script')
52 | script.type = 'text/javascript'
53 | script.src = `https://www.paypal.com/sdk/js?client-id=${clientId}¤cy=USD`
54 | script.async = true
55 | script.onload = () => {
56 | setSdkReady(true)
57 | }
58 | document.body.appendChild(script)
59 | }
60 |
61 | if (!order || successPay || order._id !== orderId) {
62 | dispatch({ type: ORDER_PAY_RESET })
63 | dispatch(getOrderDetails(orderId))
64 | } else if (!order.isPaid) {
65 | if (!window.paypal) {
66 | addPayPalScript()
67 | } else {
68 | setSdkReady(true)
69 | }
70 | }
71 | }, [dispatch, orderId, order, successPay])
72 |
73 | const successPaymentHandler = (paymentResult) => {
74 | console.log(paymentResult)
75 | // Update status of order to paid
76 | dispatch(payOrder(orderId, paymentResult))
77 | }
78 |
79 | return loading ? (
80 |
81 | ) : error ? (
82 | {error}
83 | ) : (
84 | <>
85 | Order {orderId}
86 |
87 |
88 |
89 |
90 | Shipping
91 |
92 | Name:
93 | {order.user.name}
94 |
95 |
96 | Email:
97 | {order.user.email}
98 |
99 |
100 | Address:
101 | {order.shippingAddress.address},{' '}
102 | {order.shippingAddress.city}{' '}
103 | {order.shippingAddress.postalCode},{' '}
104 | {order.shippingAddress.country}
105 | {order.isDelivered ? (
106 |
107 | Delivered on {order.deliveredAt}
108 |
109 | ) : (
110 |
111 | Not Delivered
112 |
113 | )}
114 |
115 |
116 |
117 | Payment Method
118 |
119 | Method:
120 | {order.paymentMethod}
121 | {order.isPaid ? (
122 |
123 | Paid on {order.paidAt}
124 |
125 | ) : (
126 | Not Paid
127 | )}
128 |
129 |
130 |
131 | Order Items
132 | {order.orderItems.length === 0 ? (
133 | Order is empty
134 | ) : (
135 |
136 | {order.orderItems.map((item, index) => (
137 |
138 |
139 |
140 |
146 |
147 |
148 |
151 | {item.name}
152 |
153 |
154 |
155 | {item.qty} x $ {item.price}{' '}
156 | = $ {item.qty * item.price}
157 |
158 |
159 |
160 | ))}
161 |
162 | )}
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | Order Summary
171 |
172 |
173 |
174 | Items
175 | ${order.itemsPrice}
176 |
177 |
178 |
179 |
180 | Shipping
181 | ${order.shippingPrice}
182 |
183 |
184 |
185 |
186 | Tax
187 | ${order.taxPrice}
188 |
189 |
190 |
191 |
192 | Total
193 | ${order.totalPrice}
194 |
195 |
196 | {!order.isPaid && (
197 |
198 | {loadingPay && }
199 | {!sdkReady ? (
200 |
201 | ) : (
202 |
206 | )}
207 |
208 | )}
209 |
210 | {error && (
211 | {error}
212 | )}
213 |
214 |
215 |
216 |
217 |
218 | >
219 | )
220 | }
221 |
222 | export default Order
223 |
--------------------------------------------------------------------------------
/frontend/src/screens/Payment.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 |
6 | import { savePaymentMethod } from '../actions/cartActions'
7 |
8 | import CheckoutSteps from '../components/CheckoutSteps'
9 |
10 |
11 | const Payment = ({ history }) => {
12 |
13 | // Get shipping address from global state
14 | const cart = useSelector(state => state.cart)
15 | const { shippingAddress } = cart
16 |
17 | // If theres no shipping address redirect user to shipping
18 | if(!shippingAddress){
19 | history.push('/shipping')
20 | }
21 |
22 | const [paymentMethod, setPaymentMethod] = useState('PayPal')
23 |
24 | const dispatch = useDispatch()
25 |
26 | const submitHandler = (e) => {
27 | e.preventDefault()
28 | dispatch(savePaymentMethod(paymentMethod))
29 | // Move to the place order page
30 | history.push("/place-order")
31 | }
32 |
33 | return (
34 |
35 |
36 | Payment Method
37 |
39 | Select Method
40 |
41 | setPaymentMethod(e.target.value)}>
42 | {/* setPaymentMethod(e.target.value)}> */}
43 |
44 |
45 |
48 |
49 |
50 | )
51 | }
52 |
53 | export default Payment
54 |
--------------------------------------------------------------------------------
/frontend/src/screens/PlaceOrder.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { Button, Row, Col, ListGroup, Image, Card } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import Message from '../components/Message'
5 | import { Link } from 'react-router-dom'
6 |
7 | // Create order action
8 | import { createOrder } from '../actions/orderActions'
9 |
10 | import { ORDER_CREATE_RESET } from '../constants/orderConstants'
11 | import { USER_DETAILS_RESET } from '../constants/userConstants'
12 |
13 | import CheckoutSteps from '../components/CheckoutSteps'
14 |
15 | const PlaceOrder = ({ history }) => {
16 | const dispatch = useDispatch()
17 |
18 | // Get items from the cart
19 | const cart = useSelector((state) => state.cart)
20 |
21 | const addDecimals = (num) => {
22 | return (Math.round(num * 100) / 100).toFixed(2)
23 | }
24 |
25 | // Calculate prices
26 | cart.itemsPrice = addDecimals(
27 | cart.cartItems.reduce((acc, item) => acc + item.price * item.qty, 0)
28 | )
29 |
30 | // Calculate shipping prices
31 | cart.shippingPrice = addDecimals(cart.itemsPrice > 100 ? 0 : 10)
32 |
33 | // Calculate tax
34 | cart.taxPrice = addDecimals(Number((0.15 * cart.itemsPrice).toFixed(2)))
35 |
36 | // Total price
37 | cart.totalPrice = (
38 | Number(cart.itemsPrice) +
39 | Number(cart.shippingPrice) +
40 | Number(cart.taxPrice)
41 | ).toFixed(2)
42 |
43 | const orderCreate = useSelector((state) => state.createOrder)
44 | const { order, success, error } = orderCreate
45 |
46 | useEffect(() => {
47 | if (success) {
48 | history.push(`/order/${order._id}`)
49 | dispatch({ type: ORDER_CREATE_RESET })
50 | dispatch({ type: USER_DETAILS_RESET })
51 | }
52 | // eslint-disable-next-line
53 | }, [history, success])
54 |
55 | const placeOrderHandler = () => {
56 | console.log('Placed order')
57 | dispatch(
58 | createOrder({
59 | orderItems: cart.cartItems,
60 | shippingAddress: cart.shippingAddress,
61 | paymentMethod: cart.paymentMethod,
62 | itemsPrice: cart.itemsPrice,
63 | shippingPrice: cart.shippingPrice,
64 | taxPrice: cart.taxPrice,
65 | totalPrice: cart.totalPrice,
66 | })
67 | )
68 | }
69 |
70 | return (
71 | <>
72 |
73 |
74 |
75 |
76 |
77 | Shipping
78 |
79 | Address:
80 | {cart.shippingAddress.address},{' '}
81 | {cart.shippingAddress.city}{' '}
82 | {cart.shippingAddress.postalCode},{' '}
83 | {cart.shippingAddress.country}
84 |
85 |
86 |
87 | Payment Method
88 | Method:
89 | {cart.paymentMethod}
90 |
91 |
92 | Order Items
93 | {cart.cartItems.length < 0 ? (
94 | Your cart is empty
95 | ) : (
96 |
97 | {cart.cartItems.map((item, index) => (
98 |
99 |
100 |
101 |
107 |
108 |
109 |
112 | {item.name}
113 |
114 |
115 |
116 | {item.qty} x $ {item.price}{' '}
117 | = $ {item.qty * item.price}
118 |
119 |
120 |
121 | ))}
122 |
123 | )}
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | Order Summary
132 |
133 |
134 |
135 | Items
136 | ${cart.itemsPrice}
137 |
138 |
139 |
140 |
141 | Shipping
142 | ${cart.shippingPrice}
143 |
144 |
145 |
146 |
147 | Tax
148 | ${cart.taxPrice}
149 |
150 |
151 |
152 |
153 | Total
154 | ${cart.totalPrice}
155 |
156 |
157 |
158 | {error && (
159 | {error}
160 | )}
161 |
162 |
163 |
171 |
172 |
173 |
174 |
175 |
176 | >
177 | )
178 | }
179 |
180 | export default PlaceOrder
181 |
--------------------------------------------------------------------------------
/frontend/src/screens/Product.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | // Redux
4 | import { useDispatch, useSelector } from 'react-redux'
5 | // Bootstrap Components
6 | import { Row, Col, Image, ListGroup, Card, Button, Form } from 'react-bootstrap'
7 | // Components
8 | import Rating from '../components/Rating'
9 | // Redux Actions
10 | import { listProductDetails } from '../actions/productActions'
11 | import Loader from '../components/Loader'
12 | import Message from '../components/Message'
13 |
14 | const ProductScreen = ({ history, match }) => {
15 | const [qty, setQty] = useState(1)
16 |
17 | const dispatch = useDispatch()
18 | const productDetails = useSelector((state) => state.productDetails)
19 | const { loading, error, product } = productDetails
20 |
21 | useEffect(() => {
22 | dispatch(listProductDetails(match.params.id))
23 | }, [dispatch, match])
24 |
25 | const addToCartHandler = () => {
26 | history.push(`/cart/${match.params.id}?qty=${qty}`)
27 | }
28 |
29 | return (
30 | <>
31 |
32 | Go Back
33 |
34 | {loading ? (
35 |
36 | ) : error ? (
37 | {error}
38 | ) : (
39 |
40 |
41 |
46 |
47 |
48 |
49 |
50 | {product.name}
51 |
52 |
53 |
57 |
58 |
59 | Price: ${product.price}
60 |
61 |
62 | Description: {product.description}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | Price:
72 |
73 | ${product.price}
74 |
75 |
76 |
77 |
78 |
79 | Status:
80 |
81 | {product.countInStock > 0
82 | ? 'In Stock'
83 | : 'Out of Stock'}
84 |
85 |
86 |
87 | {product.countInStock > 0 && (
88 |
89 |
90 | Qty
91 |
92 |
96 | setQty(e.target.value)
97 | }
98 | >
99 | {[
100 | ...Array(
101 | product.countInStock
102 | ).keys(),
103 | ].map((p) => (
104 |
110 | ))}
111 |
112 |
113 |
114 |
115 | )}
116 |
117 |
125 |
126 |
127 |
128 |
129 |
130 | )}
131 | >
132 | )
133 | }
134 |
135 | export default ProductScreen
136 |
--------------------------------------------------------------------------------
/frontend/src/screens/Profile.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | // Bootstrap Components
3 | import { Form, Button, Row, Col, Table } from 'react-bootstrap'
4 | import { LinkContainer } from 'react-router-bootstrap'
5 | // Redux
6 | import { useDispatch, useSelector } from 'react-redux'
7 | // UI Components
8 | import Message from '../components/Message'
9 | import Loader from '../components/Loader'
10 | // Redux Actions
11 | import { getUserDetails, updateUserProfile } from '../actions/userActions'
12 | import { listUserOrders } from '../actions/orderActions'
13 |
14 | const Profile = ({ history }) => {
15 | // State to hold email and password
16 | const [name, setName] = useState('')
17 | const [email, setEmail] = useState('')
18 | const [password, setPassword] = useState('')
19 | const [confirmPassword, setConfirmPassword] = useState('')
20 | const [message, setMessage] = useState(null)
21 |
22 | const dispatch = useDispatch()
23 |
24 | // Get user details from Redux store
25 | const userDetails = useSelector((state) => state.userDetails)
26 | const { loading, error, user } = userDetails
27 |
28 | // Get user token from Redux store
29 | const userLogin = useSelector((state) => state.userLogin)
30 | const { userInfo } = userLogin
31 |
32 | // Get update status on user info
33 | const userUpdateProfile = useSelector((state) => state.userUpdateProfile)
34 | const { success } = userUpdateProfile
35 |
36 | // Get user orders from Redux store
37 | const orderListUser = useSelector((state) => state.orderListUser)
38 | const { loading: loadingOrders, error: errorOrders, orders } = orderListUser
39 |
40 | useEffect(() => {
41 | // If there is NO user info then redirect to login page
42 | if (!userInfo) {
43 | history.push('/login')
44 | } else {
45 | if (!user.name) {
46 | dispatch(getUserDetails('profile'))
47 | dispatch(listUserOrders())
48 | } else {
49 | setName(user.name)
50 | setEmail(user.email)
51 | }
52 | }
53 | }, [history, userInfo, dispatch, user])
54 |
55 | // Handler that logs in the user
56 | const submitHandler = (e) => {
57 | e.preventDefault()
58 | // Check if passwords are the same
59 | if (password !== confirmPassword) {
60 | setMessage('Passwords do not match')
61 | } else {
62 | // Dispatch update profile reducer
63 | dispatch(updateUserProfile({ id: user._id, name, email, password }))
64 | }
65 | }
66 |
67 | return (
68 |
69 |
70 | User Profile
71 | {error && {error}}
72 | {message && {message}}
73 | {success && (
74 | Profile Updated
75 | )}
76 | {loading && }
77 |
79 | Name
80 | setName(e.target.value)}
85 | >
86 |
87 |
88 | Email Address
89 | setEmail(e.target.value)}
94 | >
95 |
96 |
97 | Password
98 | setPassword(e.target.value)}
103 | >
104 |
105 |
106 | Confirm Password
107 | setConfirmPassword(e.target.value)}
112 | >
113 |
114 |
117 |
118 |
119 |
120 | My Orders
121 | {loadingOrders ? (
122 |
123 | ) : errorOrders ? (
124 | {errorOrders}
125 | ) : (
126 |
133 |
134 |
135 | ID |
136 | DATE |
137 | TOTAL |
138 | PAID |
139 | DELIVERED |
140 | DETAILS |
141 |
142 |
143 |
144 | {orders.map((order) => (
145 |
146 | {order._id} |
147 | {order.createdAt.substring(0, 10)} |
148 | ${order.totalPrice} |
149 |
150 | {order.isPaid ? (
151 | order.paidAt.substring(0, 10)
152 | ) : (
153 |
157 | )}
158 | |
159 |
160 | {order.isDelivered ? (
161 | order.deliveredAt.substring(0, 10)
162 | ) : (
163 |
167 | )}
168 | |
169 |
170 |
173 |
179 |
180 | |
181 |
182 | ))}
183 |
184 |
185 | )}
186 |
187 |
188 | )
189 | }
190 |
191 | export default Profile
192 |
--------------------------------------------------------------------------------
/frontend/src/screens/Register.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | // Bootstrap Components
4 | import { Form, Button, Row, Col } from 'react-bootstrap'
5 | // Redux
6 | import { useDispatch, useSelector } from 'react-redux'
7 | // UI Components
8 | import Message from '../components/Message'
9 | import Loader from '../components/Loader'
10 | import FormContainer from '../components/FormContainer'
11 | // Redux Actions
12 | import { register } from '../actions/userActions'
13 |
14 | const Register = ({ location, history }) => {
15 | // State to hold email and password
16 | const [name, setName] = useState('')
17 | const [email, setEmail] = useState('')
18 | const [password, setPassword] = useState('')
19 | const [confirmPassword, setConfirmPassword] = useState('')
20 | const [message, setMessage] = useState(null)
21 |
22 | const dispatch = useDispatch()
23 |
24 | // Get user login info from Redux state
25 | const userRegister = useSelector((state) => state.userRegister)
26 | const { loading, error, userInfo } = userRegister
27 |
28 | const redirect = location.search ? location.search.split('=')[1] : '/'
29 |
30 | useEffect(() => {
31 | // If there is user info then redirect
32 | if (userInfo) {
33 | history.push(redirect)
34 | }
35 | }, [history, userInfo, redirect])
36 |
37 | // Handler that logs in the user
38 | const submitHandler = (e) => {
39 | e.preventDefault()
40 | // Check if passwords are the same
41 | if (password !== confirmPassword) {
42 | setMessage('Passwords do not match')
43 | } else {
44 | // Dispatch Register
45 | dispatch(register(name, email, password))
46 | }
47 | }
48 |
49 | return (
50 |
51 | Sign Up
52 | {error && {error}}
53 | {message && {message}}
54 | {loading && }
55 |
57 | Name
58 | setName(e.target.value)}
63 | >
64 |
65 |
66 | Email Address
67 | setEmail(e.target.value)}
72 | >
73 |
74 |
75 | Password
76 | setPassword(e.target.value)}
81 | >
82 |
83 |
84 | Confirm Password
85 | setConfirmPassword(e.target.value)}
90 | >
91 |
92 |
95 |
96 |
97 |
98 | Have an Account?{' '}
99 |
102 | Login
103 |
104 |
105 |
106 |
107 | )
108 | }
109 |
110 | export default Register
111 |
--------------------------------------------------------------------------------
/frontend/src/screens/Shipping.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 |
6 | import { saveShippingAddress } from '../actions/cartActions'
7 |
8 | import CheckoutSteps from '../components/CheckoutSteps'
9 |
10 |
11 | const Shipping = ({ history }) => {
12 |
13 | // Get shipping address from global state
14 | const cart = useSelector(state => state.cart)
15 | const { shippingAddress } = cart
16 |
17 | const [address, setAddress] = useState(shippingAddress.address)
18 | const [city, setCity] = useState(shippingAddress.city)
19 | const [postalCode, setPostalCode] = useState(shippingAddress.postalCode)
20 | const [country, setCountry] = useState(shippingAddress.country)
21 |
22 | const dispatch = useDispatch()
23 |
24 | const submitHandler = (e) => {
25 | e.preventDefault()
26 | dispatch(saveShippingAddress({ address, city, postalCode, country }))
27 | // Move to the payments page
28 | history.push("/payment")
29 | }
30 |
31 | return (
32 |
33 |
34 | Shipping
35 |
37 | Address
38 | setAddress(e.target.value)}
43 | required
44 | >
45 |
46 |
47 | City
48 | setCity(e.target.value)}
53 | required
54 | >
55 |
56 |
57 | Postal Code
58 | setPostalCode(e.target.value)}
63 | required
64 | >
65 |
66 |
67 | Country
68 | setCountry(e.target.value)}
73 | required
74 | >
75 |
76 |
79 |
80 |
81 | )
82 | }
83 |
84 | export default Shipping
85 |
--------------------------------------------------------------------------------
/frontend/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/frontend/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import { composeWithDevTools } from 'redux-devtools-extension'
4 |
5 | // Reducers
6 | import {
7 | productListReducer,
8 | productDetailsReducer,
9 | } from './reducers/productReducers'
10 | import { cartReducer } from './reducers/cartReducers'
11 | import {
12 | userLoginReducer,
13 | userRegisterReducer,
14 | userDetailsReducer,
15 | userUpdateProfileReducer,
16 | } from './reducers/userReducers'
17 | import {
18 | createOrderReducer,
19 | orderDetailsReducer,
20 | orderListUserReducer,
21 | orderPayReducer,
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 | createOrder: createOrderReducer,
33 | orderDetails: orderDetailsReducer,
34 | orderPay: orderPayReducer,
35 | orderListUser: orderListUserReducer,
36 | })
37 |
38 | const cartItemsFromStorage = localStorage.getItem('cartItems')
39 | ? JSON.parse(localStorage.getItem('cartItems'))
40 | : []
41 |
42 | const userInfoFromStorage = localStorage.getItem('userInfo')
43 | ? JSON.parse(localStorage.getItem('userInfo'))
44 | : null
45 |
46 | const shippingAddressFromStorage = localStorage.getItem('shippingAddress')
47 | ? JSON.parse(localStorage.getItem('shippingAddress'))
48 | : {}
49 |
50 | // Load initial state when the application is loaded
51 | const initialState = {
52 | cart: {
53 | cartItems: cartItemsFromStorage,
54 | shippingAddress: shippingAddressFromStorage,
55 | },
56 | userLogin: { userInfo: userInfoFromStorage },
57 | }
58 |
59 | const middleware = [thunk]
60 |
61 | const store = createStore(
62 | reducer,
63 | initialState,
64 | composeWithDevTools(applyMiddleware(...middleware))
65 | )
66 |
67 | export default store
68 |
--------------------------------------------------------------------------------
/frontend/src/utils/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history'
2 | export default createBrowserHistory()
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "e-commerce-store",
3 | "version": "1.0.0",
4 | "description": "MERN e-commerce store",
5 | "main": "server.js",
6 | "type": "module",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "start": "node backend/server.js",
10 | "server": "nodemon backend/server",
11 | "client": "npm start --prefix frontend",
12 | "dev": "concurrently \"npm run server\" \"npm run client\" ",
13 | "data:import": "node backend/seeder",
14 | "data:destroy": "node backend/seeder -d"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/ramos07/React-Node-E-Commerce-Store.git"
19 | },
20 | "author": "Ricardo Ramos",
21 | "license": "ISC",
22 | "bugs": {
23 | "url": "https://github.com/ramos07/React-Node-E-Commerce-Store/issues"
24 | },
25 | "homepage": "https://github.com/ramos07/React-Node-E-Commerce-Store#readme",
26 | "dependencies": {
27 | "bcryptjs": "^2.4.3",
28 | "colors": "^1.4.0",
29 | "dotenv": "^8.2.0",
30 | "express": "^4.17.1",
31 | "express-async-handler": "^1.1.4",
32 | "jsonwebtoken": "^8.5.1",
33 | "mongoose": "^5.10.7"
34 | },
35 | "devDependencies": {
36 | "concurrently": "^5.3.0",
37 | "nodemon": "^2.0.4"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------