├── uploads
├── file.txt
└── Screen Shot 2020-09-29 at 5.50.52 PM.png
├── Procfile
├── frontend
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── images
│ │ ├── alexa.jpg
│ │ ├── camera.jpg
│ │ ├── mouse.jpg
│ │ ├── phone.jpg
│ │ ├── sample.jpg
│ │ ├── airpods.jpg
│ │ └── playstation.jpg
│ ├── manifest.json
│ └── index.html
├── src
│ ├── components
│ │ ├── Message.js
│ │ ├── Footer.js
│ │ ├── FormContainer.js
│ │ ├── Loader.js
│ │ ├── Meta.js
│ │ ├── Paginate.js
│ │ ├── SearchBox.js
│ │ ├── Product.js
│ │ ├── ProductCarousel.js
│ │ ├── CheckoutSteps.js
│ │ ├── Rating.js
│ │ └── Header.js
│ ├── constants
│ │ ├── cartConstants.js
│ │ ├── orderConstants.js
│ │ ├── userConstants.js
│ │ └── productConstants.js
│ ├── index.js
│ ├── index.css
│ ├── actions
│ │ ├── cartActions.js
│ │ ├── productActions.js
│ │ ├── orderActions.js
│ │ └── userActions.js
│ ├── reducers
│ │ ├── cartReducers.js
│ │ ├── userReducers.js
│ │ ├── orderReducers.js
│ │ └── productReducers.js
│ ├── screens
│ │ ├── HomeScreen.js
│ │ ├── PaymentScreen.js
│ │ ├── LoginScreen.js
│ │ ├── OrderListScreen.js
│ │ ├── ShippingScreen.js
│ │ ├── UserListScreen.js
│ │ ├── RegisterScreen.js
│ │ ├── UserEditScreen.js
│ │ ├── CartScreen.js
│ │ ├── ProductListScreen.js
│ │ ├── PlaceOrderScreen.js
│ │ ├── ProfileScreen.js
│ │ ├── ProductEditScreen.js
│ │ ├── OrderScreen.js
│ │ └── ProductScreen.js
│ ├── store.js
│ ├── App.js
│ └── serviceWorker.js
├── package.json
└── README.md
├── backend
├── utils
│ └── generateToken.js
├── middleware
│ ├── errorMiddleware.js
│ └── authMiddleware.js
├── data
│ ├── users.js
│ └── products.js
├── config
│ └── db.js
├── routes
│ ├── orderRoutes.js
│ ├── productRoutes.js
│ ├── userRoutes.js
│ └── uploadRoutes.js
├── models
│ ├── userModel.js
│ ├── productModel.js
│ └── orderModel.js
├── seeder.js
├── server.js
└── controllers
│ ├── orderController.js
│ ├── productController.js
│ └── userController.js
├── .gitignore
├── package.json
└── README.md
/uploads/file.txt:
--------------------------------------------------------------------------------
1 | Add to git repo
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: node backend/server.js
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/proshop_mern/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/proshop_mern/HEAD/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/proshop_mern/HEAD/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/public/images/alexa.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/proshop_mern/HEAD/frontend/public/images/alexa.jpg
--------------------------------------------------------------------------------
/frontend/public/images/camera.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/proshop_mern/HEAD/frontend/public/images/camera.jpg
--------------------------------------------------------------------------------
/frontend/public/images/mouse.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/proshop_mern/HEAD/frontend/public/images/mouse.jpg
--------------------------------------------------------------------------------
/frontend/public/images/phone.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/proshop_mern/HEAD/frontend/public/images/phone.jpg
--------------------------------------------------------------------------------
/frontend/public/images/sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/proshop_mern/HEAD/frontend/public/images/sample.jpg
--------------------------------------------------------------------------------
/frontend/public/images/airpods.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/proshop_mern/HEAD/frontend/public/images/airpods.jpg
--------------------------------------------------------------------------------
/frontend/public/images/playstation.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/proshop_mern/HEAD/frontend/public/images/playstation.jpg
--------------------------------------------------------------------------------
/uploads/Screen Shot 2020-09-29 at 5.50.52 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/proshop_mern/HEAD/uploads/Screen Shot 2020-09-29 at 5.50.52 PM.png
--------------------------------------------------------------------------------
/backend/utils/generateToken.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken'
2 |
3 | const generateToken = (id) => {
4 | return jwt.sign({ id }, process.env.JWT_SECRET, {
5 | expiresIn: '30d',
6 | })
7 | }
8 |
9 | export default generateToken
10 |
--------------------------------------------------------------------------------
/frontend/src/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/constants/cartConstants.js:
--------------------------------------------------------------------------------
1 | export const CART_ADD_ITEM = 'CART_ADD_ITEM'
2 | export const CART_CLEAR_ITEMS = 'CART_RESET'
3 | export const CART_REMOVE_ITEM = 'CART_REMOVE_ITEM'
4 | export const CART_SAVE_SHIPPING_ADDRESS = 'CART_SAVE_SHIPPING_ADDRESS'
5 | export const CART_SAVE_PAYMENT_METHOD = 'CART_SAVE_PAYMENT_METHOD'
6 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Container, Row, Col } from 'react-bootstrap'
3 |
4 | const Footer = () => {
5 | return (
6 |
13 | )
14 | }
15 |
16 | export default Footer
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | node_modules/
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /frontend/build
14 |
15 | # misc
16 | .DS_Store
17 | .env
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .vscode/settings.json
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | const statusCode = res.statusCode === 200 ? 500 : res.statusCode
9 | res.status(statusCode)
10 | res.json({
11 | message: err.message,
12 | stack: process.env.NODE_ENV === 'production' ? null : err.stack,
13 | })
14 | }
15 |
16 | export { notFound, errorHandler }
17 |
--------------------------------------------------------------------------------
/backend/data/users.js:
--------------------------------------------------------------------------------
1 | import bcrypt from 'bcryptjs'
2 |
3 | const users = [
4 | {
5 | name: 'Admin User',
6 | email: 'admin@example.com',
7 | password: bcrypt.hashSync('123456', 10),
8 | isAdmin: true,
9 | },
10 | {
11 | name: 'John Doe',
12 | email: 'john@example.com',
13 | password: bcrypt.hashSync('123456', 10),
14 | },
15 | {
16 | name: 'Jane Doe',
17 | email: 'jane@example.com',
18 | password: bcrypt.hashSync('123456', 10),
19 | },
20 | ]
21 |
22 | export default users
23 |
--------------------------------------------------------------------------------
/backend/config/db.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 |
3 | const connectDB = async () => {
4 | try {
5 | const conn = await mongoose.connect(process.env.MONGO_URI, {
6 | useUnifiedTopology: true,
7 | useNewUrlParser: true,
8 | useCreateIndex: true,
9 | })
10 |
11 | console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline)
12 | } catch (error) {
13 | console.error(`Error: ${error.message}`.red.underline.bold)
14 | process.exit(1)
15 | }
16 | }
17 |
18 | export default connectDB
19 |
--------------------------------------------------------------------------------
/frontend/src/components/Meta.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Helmet } from 'react-helmet'
3 |
4 | const Meta = ({ title, description, keywords }) => {
5 | return (
6 |
7 | {title}
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | Meta.defaultProps = {
15 | title: 'Welcome To ProShop',
16 | description: 'We sell the best products for cheap',
17 | keywords: 'electronics, buy electronics, cheap electroincs',
18 | }
19 |
20 | export default Meta
21 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/backend/routes/orderRoutes.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | const router = express.Router()
3 | import {
4 | addOrderItems,
5 | getOrderById,
6 | updateOrderToPaid,
7 | updateOrderToDelivered,
8 | getMyOrders,
9 | getOrders,
10 | } from '../controllers/orderController.js'
11 | import { protect, admin } from '../middleware/authMiddleware.js'
12 |
13 | router.route('/').post(protect, addOrderItems).get(protect, admin, getOrders)
14 | router.route('/myorders').get(protect, getMyOrders)
15 | router.route('/:id').get(protect, getOrderById)
16 | router.route('/:id/pay').put(protect, updateOrderToPaid)
17 | router.route('/:id/deliver').put(protect, admin, updateOrderToDelivered)
18 |
19 | export default router
20 |
--------------------------------------------------------------------------------
/backend/routes/productRoutes.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | const router = express.Router()
3 | import {
4 | getProducts,
5 | getProductById,
6 | deleteProduct,
7 | createProduct,
8 | updateProduct,
9 | createProductReview,
10 | getTopProducts,
11 | } from '../controllers/productController.js'
12 | import { protect, admin } from '../middleware/authMiddleware.js'
13 |
14 | router.route('/').get(getProducts).post(protect, admin, createProduct)
15 | router.route('/:id/reviews').post(protect, createProductReview)
16 | router.get('/top', getTopProducts)
17 | router
18 | .route('/:id')
19 | .get(getProductById)
20 | .delete(protect, admin, deleteProduct)
21 | .put(protect, admin, updateProduct)
22 |
23 | export default router
24 |
--------------------------------------------------------------------------------
/backend/routes/userRoutes.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | const router = express.Router()
3 | import {
4 | authUser,
5 | registerUser,
6 | getUserProfile,
7 | updateUserProfile,
8 | getUsers,
9 | deleteUser,
10 | getUserById,
11 | updateUser,
12 | } from '../controllers/userController.js'
13 | import { protect, admin } from '../middleware/authMiddleware.js'
14 |
15 | router.route('/').post(registerUser).get(protect, admin, getUsers)
16 | router.post('/login', authUser)
17 | router
18 | .route('/profile')
19 | .get(protect, getUserProfile)
20 | .put(protect, updateUserProfile)
21 | router
22 | .route('/:id')
23 | .delete(protect, admin, deleteUser)
24 | .get(protect, admin, getUserById)
25 | .put(protect, admin, updateUser)
26 |
27 | export default router
28 |
--------------------------------------------------------------------------------
/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 |
23 | /* carousel */
24 | .carousel-item-next,
25 | .carousel-item-prev,
26 | .carousel-item.active {
27 | display: flex;
28 | }
29 | .carousel-caption {
30 | position: absolute;
31 | top: 0;
32 | }
33 |
34 | .carousel-caption h2 {
35 | color: #fff;
36 | }
37 |
38 | .carousel img {
39 | height: 300px;
40 | padding: 30px;
41 | margin: 40px;
42 | border-radius: 50%;
43 | margin-left: auto;
44 | margin-right: auto;
45 | }
46 | .carousel a {
47 | margin: 0 auto;
48 | }
49 | @media (max-width: 900px) {
50 | .carousel-caption h2 {
51 | font-size: 2.5vw;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/src/components/Paginate.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Pagination } from 'react-bootstrap'
3 | import { LinkContainer } from 'react-router-bootstrap'
4 |
5 | const Paginate = ({ pages, page, isAdmin = false, keyword = '' }) => {
6 | return (
7 | pages > 1 && (
8 |
9 | {[...Array(pages).keys()].map((x) => (
10 |
20 | {x + 1}
21 |
22 | ))}
23 |
24 | )
25 | )
26 | }
27 |
28 | export default Paginate
29 |
--------------------------------------------------------------------------------
/frontend/src/components/SearchBox.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Form, Button } from 'react-bootstrap'
3 |
4 | const SearchBox = ({ history }) => {
5 | const [keyword, setKeyword] = useState('')
6 |
7 | const submitHandler = (e) => {
8 | e.preventDefault()
9 | if (keyword.trim()) {
10 | history.push(`/search/${keyword}`)
11 | } else {
12 | history.push('/')
13 | }
14 | }
15 |
16 | return (
17 |
setKeyword(e.target.value)}
22 | placeholder='Search Products...'
23 | className='mr-sm-2 ml-sm-5'
24 | >
25 |
28 |
29 | )
30 | }
31 |
32 | export default SearchBox
33 |
--------------------------------------------------------------------------------
/frontend/src/components/Product.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { Card } from 'react-bootstrap'
4 | import Rating from './Rating'
5 |
6 | const Product = ({ product }) => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {product.name}
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 | ${product.price}
28 |
29 |
30 | )
31 | }
32 |
33 | export default Product
34 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
20 | Welcome To ProShop
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/backend/models/userModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 | import bcrypt from 'bcryptjs'
3 |
4 | const userSchema = mongoose.Schema(
5 | {
6 | name: {
7 | type: String,
8 | required: true,
9 | },
10 | email: {
11 | type: String,
12 | required: true,
13 | unique: true,
14 | },
15 | password: {
16 | type: String,
17 | required: true,
18 | },
19 | isAdmin: {
20 | type: Boolean,
21 | required: true,
22 | default: false,
23 | },
24 | },
25 | {
26 | timestamps: true,
27 | }
28 | )
29 |
30 | userSchema.methods.matchPassword = async function (enteredPassword) {
31 | return await bcrypt.compare(enteredPassword, this.password)
32 | }
33 |
34 | userSchema.pre('save', async function (next) {
35 | if (!this.isModified('password')) {
36 | next()
37 | }
38 |
39 | const salt = await bcrypt.genSalt(10)
40 | this.password = await bcrypt.hash(this.password, salt)
41 | })
42 |
43 | const User = mongoose.model('User', userSchema)
44 |
45 | export default User
46 |
--------------------------------------------------------------------------------
/backend/routes/uploadRoutes.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import express from 'express'
3 | import multer from 'multer'
4 | const router = express.Router()
5 |
6 | const storage = multer.diskStorage({
7 | destination(req, file, cb) {
8 | cb(null, 'uploads/')
9 | },
10 | filename(req, file, cb) {
11 | cb(
12 | null,
13 | `${file.fieldname}-${Date.now()}${path.extname(file.originalname)}`
14 | )
15 | },
16 | })
17 |
18 | function checkFileType(file, cb) {
19 | const filetypes = /jpg|jpeg|png/
20 | const extname = filetypes.test(path.extname(file.originalname).toLowerCase())
21 | const mimetype = filetypes.test(file.mimetype)
22 |
23 | if (extname && mimetype) {
24 | return cb(null, true)
25 | } else {
26 | cb('Images only!')
27 | }
28 | }
29 |
30 | const upload = multer({
31 | storage,
32 | fileFilter: function (req, file, cb) {
33 | checkFileType(file, cb)
34 | },
35 | })
36 |
37 | router.post('/', upload.single('image'), (req, res) => {
38 | res.send(`/${req.file.path}`)
39 | })
40 |
41 | export default router
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "proshop",
3 | "version": "1.0.0",
4 | "description": "MERN shopping cart app",
5 | "main": "server.js",
6 | "type": "module",
7 | "scripts": {
8 | "start": "node backend/server",
9 | "server": "nodemon backend/server",
10 | "client": "npm start --prefix frontend",
11 | "dev": "concurrently \"npm run server\" \"npm run client\"",
12 | "data:import": "node backend/seeder",
13 | "data:destroy": "node backend/seeder -d",
14 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix frontend && npm run build --prefix frontend"
15 | },
16 | "author": "Brad Traversy",
17 | "license": "MIT",
18 | "dependencies": {
19 | "bcryptjs": "^2.4.3",
20 | "colors": "^1.4.0",
21 | "dotenv": "^8.2.0",
22 | "express": "^4.17.1",
23 | "express-async-handler": "^1.1.4",
24 | "jsonwebtoken": "^8.5.1",
25 | "mongoose": "^5.10.6",
26 | "morgan": "^1.10.0",
27 | "multer": "^1.4.2"
28 | },
29 | "devDependencies": {
30 | "concurrently": "^5.3.0",
31 | "nodemon": "^2.0.4"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/backend/middleware/authMiddleware.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken'
2 | import asyncHandler from 'express-async-handler'
3 | import User from '../models/userModel.js'
4 |
5 | const protect = asyncHandler(async (req, res, next) => {
6 | let token
7 |
8 | if (
9 | req.headers.authorization &&
10 | req.headers.authorization.startsWith('Bearer')
11 | ) {
12 | try {
13 | token = req.headers.authorization.split(' ')[1]
14 |
15 | const decoded = jwt.verify(token, process.env.JWT_SECRET)
16 |
17 | req.user = await User.findById(decoded.id).select('-password')
18 |
19 | next()
20 | } catch (error) {
21 | console.error(error)
22 | res.status(401)
23 | throw new Error('Not authorized, token failed')
24 | }
25 | }
26 |
27 | if (!token) {
28 | res.status(401)
29 | throw new Error('Not authorized, no token')
30 | }
31 | })
32 |
33 | const admin = (req, res, next) => {
34 | if (req.user && req.user.isAdmin) {
35 | next()
36 | } else {
37 | res.status(401)
38 | throw new Error('Not authorized as an admin')
39 | }
40 | }
41 |
42 | export { protect, admin }
43 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "proxy": "http://127.0.0.1:5000",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {
7 | "@testing-library/jest-dom": "^4.2.4",
8 | "@testing-library/react": "^9.5.0",
9 | "@testing-library/user-event": "^7.2.1",
10 | "axios": "^0.20.0",
11 | "react": "^16.13.1",
12 | "react-bootstrap": "^1.3.0",
13 | "react-dom": "^16.13.1",
14 | "react-helmet": "^6.1.0",
15 | "react-paypal-button-v2": "^2.6.2",
16 | "react-redux": "^7.2.1",
17 | "react-router-bootstrap": "^0.25.0",
18 | "react-router-dom": "^5.2.0",
19 | "react-scripts": "3.4.3",
20 | "redux": "^4.0.5",
21 | "redux-devtools-extension": "^2.13.8",
22 | "redux-thunk": "^2.3.0"
23 | },
24 | "scripts": {
25 | "start": "react-scripts start",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test",
28 | "eject": "react-scripts eject"
29 | },
30 | "eslintConfig": {
31 | "extends": "react-app"
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/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_MY_REQUEST = 'ORDER_LIST_MY_REQUEST'
16 | export const ORDER_LIST_MY_SUCCESS = 'ORDER_LIST_MY_SUCCESS'
17 | export const ORDER_LIST_MY_FAIL = 'ORDER_LIST_MY_FAIL'
18 | export const ORDER_LIST_MY_RESET = 'ORDER_LIST_MY_RESET'
19 |
20 | export const ORDER_LIST_REQUEST = 'ORDER_LIST_REQUEST'
21 | export const ORDER_LIST_SUCCESS = 'ORDER_LIST_SUCCESS'
22 | export const ORDER_LIST_FAIL = 'ORDER_LIST_FAIL'
23 |
24 | export const ORDER_DELIVER_REQUEST = 'ORDER_DELIVER_REQUEST'
25 | export const ORDER_DELIVER_SUCCESS = 'ORDER_DELIVER_SUCCESS'
26 | export const ORDER_DELIVER_FAIL = 'ORDER_DELIVER_FAIL'
27 | export const ORDER_DELIVER_RESET = 'ORDER_DELIVER_RESET'
28 |
--------------------------------------------------------------------------------
/frontend/src/components/ProductCarousel.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { Carousel, Image } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Loader from './Loader'
6 | import Message from './Message'
7 | import { listTopProducts } from '../actions/productActions'
8 |
9 | const ProductCarousel = () => {
10 | const dispatch = useDispatch()
11 |
12 | const productTopRated = useSelector((state) => state.productTopRated)
13 | const { loading, error, products } = productTopRated
14 |
15 | useEffect(() => {
16 | dispatch(listTopProducts())
17 | }, [dispatch])
18 |
19 | return loading ? (
20 |
21 | ) : error ? (
22 | {error}
23 | ) : (
24 |
25 | {products.map((product) => (
26 |
27 |
28 |
29 |
30 |
31 | {product.name} (${product.price})
32 |
33 |
34 |
35 |
36 | ))}
37 |
38 | )
39 | }
40 |
41 | export default ProductCarousel
42 |
--------------------------------------------------------------------------------
/frontend/src/actions/cartActions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {
3 | CART_ADD_ITEM,
4 | CART_REMOVE_ITEM,
5 | CART_SAVE_SHIPPING_ADDRESS,
6 | CART_SAVE_PAYMENT_METHOD,
7 | } from '../constants/cartConstants'
8 |
9 | export const addToCart = (id, qty) => async (dispatch, getState) => {
10 | const { data } = await axios.get(`/api/products/${id}`)
11 |
12 | dispatch({
13 | type: CART_ADD_ITEM,
14 | payload: {
15 | product: data._id,
16 | name: data.name,
17 | image: data.image,
18 | price: data.price,
19 | countInStock: data.countInStock,
20 | qty,
21 | },
22 | })
23 |
24 | localStorage.setItem('cartItems', JSON.stringify(getState().cart.cartItems))
25 | }
26 |
27 | export const removeFromCart = (id) => (dispatch, getState) => {
28 | dispatch({
29 | type: CART_REMOVE_ITEM,
30 | payload: id,
31 | })
32 |
33 | localStorage.setItem('cartItems', JSON.stringify(getState().cart.cartItems))
34 | }
35 |
36 | export const saveShippingAddress = (data) => (dispatch) => {
37 | dispatch({
38 | type: CART_SAVE_SHIPPING_ADDRESS,
39 | payload: data,
40 | })
41 |
42 | localStorage.setItem('shippingAddress', JSON.stringify(data))
43 | }
44 |
45 | export const savePaymentMethod = (data) => (dispatch) => {
46 | dispatch({
47 | type: CART_SAVE_PAYMENT_METHOD,
48 | payload: data,
49 | })
50 |
51 | localStorage.setItem('paymentMethod', JSON.stringify(data))
52 | }
53 |
--------------------------------------------------------------------------------
/frontend/src/components/CheckoutSteps.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Nav } from 'react-bootstrap'
3 | import { LinkContainer } from 'react-router-bootstrap'
4 |
5 | const CheckoutSteps = ({ step1, step2, step3, step4 }) => {
6 | return (
7 |
48 | )
49 | }
50 |
51 | export default CheckoutSteps
52 |
--------------------------------------------------------------------------------
/frontend/src/reducers/cartReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | CART_ADD_ITEM,
3 | CART_REMOVE_ITEM,
4 | CART_SAVE_SHIPPING_ADDRESS,
5 | CART_SAVE_PAYMENT_METHOD,
6 | CART_CLEAR_ITEMS,
7 | } from '../constants/cartConstants'
8 |
9 | export const cartReducer = (
10 | state = { cartItems: [], shippingAddress: {} },
11 | action
12 | ) => {
13 | switch (action.type) {
14 | case CART_ADD_ITEM:
15 | const item = action.payload
16 |
17 | const existItem = state.cartItems.find((x) => x.product === item.product)
18 |
19 | if (existItem) {
20 | return {
21 | ...state,
22 | cartItems: state.cartItems.map((x) =>
23 | x.product === existItem.product ? item : x
24 | ),
25 | }
26 | } else {
27 | return {
28 | ...state,
29 | cartItems: [...state.cartItems, item],
30 | }
31 | }
32 | case CART_REMOVE_ITEM:
33 | return {
34 | ...state,
35 | cartItems: state.cartItems.filter((x) => x.product !== action.payload),
36 | }
37 | case CART_SAVE_SHIPPING_ADDRESS:
38 | return {
39 | ...state,
40 | shippingAddress: action.payload,
41 | }
42 | case CART_SAVE_PAYMENT_METHOD:
43 | return {
44 | ...state,
45 | paymentMethod: action.payload,
46 | }
47 | case CART_CLEAR_ITEMS:
48 | return {
49 | ...state,
50 | cartItems: [],
51 | }
52 | default:
53 | return state
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/backend/seeder.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 | import dotenv from 'dotenv'
3 | import colors from 'colors'
4 | import users from './data/users.js'
5 | import products from './data/products.js'
6 | import User from './models/userModel.js'
7 | import Product from './models/productModel.js'
8 | import Order from './models/orderModel.js'
9 | import connectDB from './config/db.js'
10 |
11 | dotenv.config()
12 |
13 | connectDB()
14 |
15 | const importData = async () => {
16 | try {
17 | await Order.deleteMany()
18 | await Product.deleteMany()
19 | await User.deleteMany()
20 |
21 | const createdUsers = await User.insertMany(users)
22 |
23 | const adminUser = createdUsers[0]._id
24 |
25 | const sampleProducts = products.map((product) => {
26 | return { ...product, user: adminUser }
27 | })
28 |
29 | await Product.insertMany(sampleProducts)
30 |
31 | console.log('Data Imported!'.green.inverse)
32 | process.exit()
33 | } catch (error) {
34 | console.error(`${error}`.red.inverse)
35 | process.exit(1)
36 | }
37 | }
38 |
39 | const destroyData = async () => {
40 | try {
41 | await Order.deleteMany()
42 | await Product.deleteMany()
43 | await User.deleteMany()
44 |
45 | console.log('Data Destroyed!'.red.inverse)
46 | process.exit()
47 | } catch (error) {
48 | console.error(`${error}`.red.inverse)
49 | process.exit(1)
50 | }
51 | }
52 |
53 | if (process.argv[2] === '-d') {
54 | destroyData()
55 | } else {
56 | importData()
57 | }
58 |
--------------------------------------------------------------------------------
/frontend/src/constants/userConstants.js:
--------------------------------------------------------------------------------
1 | export const USER_LOGIN_REQUEST = 'USER_LOGIN_REQUEST'
2 | export const USER_LOGIN_SUCCESS = 'USER_LOGIN_SUCCESS'
3 | export const USER_LOGIN_FAIL = 'USER_LOGIN_FAIL'
4 | export const USER_LOGOUT = 'USER_LOGOUT'
5 |
6 | export const USER_REGISTER_REQUEST = 'USER_REGISTER_REQUEST'
7 | export const USER_REGISTER_SUCCESS = 'USER_REGISTER_SUCCESS'
8 | export const USER_REGISTER_FAIL = 'USER_REGISTER_FAIL'
9 |
10 | export const USER_DETAILS_REQUEST = 'USER_DETAILS_REQUEST'
11 | export const USER_DETAILS_SUCCESS = 'USER_DETAILS_SUCCESS'
12 | export const USER_DETAILS_FAIL = 'USER_DETAILS_FAIL'
13 | export const USER_DETAILS_RESET = 'USER_DETAILS_RESET'
14 |
15 | export const USER_UPDATE_PROFILE_REQUEST = 'USER_UPDATE_PROFILE_REQUEST'
16 | export const USER_UPDATE_PROFILE_SUCCESS = 'USER_UPDATE_PROFILE_SUCCESS'
17 | export const USER_UPDATE_PROFILE_FAIL = 'USER_UPDATE_PROFILE_FAIL'
18 | export const USER_UPDATE_PROFILE_RESET = 'USER_UPDATE_PROFILE_RESET'
19 |
20 | export const USER_LIST_REQUEST = 'USER_LIST_REQUEST'
21 | export const USER_LIST_SUCCESS = 'USER_LIST_SUCCESS'
22 | export const USER_LIST_FAIL = 'USER_LIST_FAIL'
23 | export const USER_LIST_RESET = 'USER_LIST_RESET'
24 |
25 | export const USER_DELETE_REQUEST = 'USER_DELETE_REQUEST'
26 | export const USER_DELETE_SUCCESS = 'USER_DELETE_SUCCESS'
27 | export const USER_DELETE_FAIL = 'USER_DELETE_FAIL'
28 |
29 | export const USER_UPDATE_REQUEST = 'USER_UPDATE_REQUEST'
30 | export const USER_UPDATE_SUCCESS = 'USER_UPDATE_SUCCESS'
31 | export const USER_UPDATE_FAIL = 'USER_UPDATE_FAIL'
32 | export const USER_UPDATE_RESET = 'USER_UPDATE_RESET'
33 |
--------------------------------------------------------------------------------
/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 |
9 | export const PRODUCT_DELETE_REQUEST = 'PRODUCT_DELETE_REQUEST'
10 | export const PRODUCT_DELETE_SUCCESS = 'PRODUCT_DELETE_SUCCESS'
11 | export const PRODUCT_DELETE_FAIL = 'PRODUCT_DELETE_FAIL'
12 |
13 | export const PRODUCT_CREATE_REQUEST = 'PRODUCT_CREATE_REQUEST'
14 | export const PRODUCT_CREATE_SUCCESS = 'PRODUCT_CREATE_SUCCESS'
15 | export const PRODUCT_CREATE_FAIL = 'PRODUCT_CREATE_FAIL'
16 | export const PRODUCT_CREATE_RESET = 'PRODUCT_CREATE_RESET'
17 |
18 | export const PRODUCT_UPDATE_REQUEST = 'PRODUCT_UPDATE_REQUEST'
19 | export const PRODUCT_UPDATE_SUCCESS = 'PRODUCT_UPDATE_SUCCESS'
20 | export const PRODUCT_UPDATE_FAIL = 'PRODUCT_UPDATE_FAIL'
21 | export const PRODUCT_UPDATE_RESET = 'PRODUCT_UPDATE_RESET'
22 |
23 | export const PRODUCT_CREATE_REVIEW_REQUEST = 'PRODUCT_CREATE_REVIEW_REQUEST'
24 | export const PRODUCT_CREATE_REVIEW_SUCCESS = 'PRODUCT_CREATE_REVIEW_SUCCESS'
25 | export const PRODUCT_CREATE_REVIEW_FAIL = 'PRODUCT_CREATE_REVIEW_FAIL'
26 | export const PRODUCT_CREATE_REVIEW_RESET = 'PRODUCT_CREATE_REVIEW_RESET'
27 |
28 | export const PRODUCT_TOP_REQUEST = 'PRODUCT_TOP_REQUEST'
29 | export const PRODUCT_TOP_SUCCESS = 'PRODUCT_TOP_SUCCESS'
30 | export const PRODUCT_TOP_FAIL = 'PRODUCT_TOP_FAIL'
31 |
--------------------------------------------------------------------------------
/backend/models/productModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 |
3 | const reviewSchema = mongoose.Schema(
4 | {
5 | name: { type: String, required: true },
6 | rating: { type: Number, required: true },
7 | comment: { type: String, required: true },
8 | user: {
9 | type: mongoose.Schema.Types.ObjectId,
10 | required: true,
11 | ref: 'User',
12 | },
13 | },
14 | {
15 | timestamps: true,
16 | }
17 | )
18 |
19 | const productSchema = mongoose.Schema(
20 | {
21 | user: {
22 | type: mongoose.Schema.Types.ObjectId,
23 | required: true,
24 | ref: 'User',
25 | },
26 | name: {
27 | type: String,
28 | required: true,
29 | },
30 | image: {
31 | type: String,
32 | required: true,
33 | },
34 | brand: {
35 | type: String,
36 | required: true,
37 | },
38 | category: {
39 | type: String,
40 | required: true,
41 | },
42 | description: {
43 | type: String,
44 | required: true,
45 | },
46 | reviews: [reviewSchema],
47 | rating: {
48 | type: Number,
49 | required: true,
50 | default: 0,
51 | },
52 | numReviews: {
53 | type: Number,
54 | required: true,
55 | default: 0,
56 | },
57 | price: {
58 | type: Number,
59 | required: true,
60 | default: 0,
61 | },
62 | countInStock: {
63 | type: Number,
64 | required: true,
65 | default: 0,
66 | },
67 | },
68 | {
69 | timestamps: true,
70 | }
71 | )
72 |
73 | const Product = mongoose.model('Product', productSchema)
74 |
75 | export default Product
76 |
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import express from 'express'
3 | import dotenv from 'dotenv'
4 | import colors from 'colors'
5 | import morgan from 'morgan'
6 | import { notFound, errorHandler } from './middleware/errorMiddleware.js'
7 | import connectDB from './config/db.js'
8 |
9 | import productRoutes from './routes/productRoutes.js'
10 | import userRoutes from './routes/userRoutes.js'
11 | import orderRoutes from './routes/orderRoutes.js'
12 | import uploadRoutes from './routes/uploadRoutes.js'
13 |
14 | dotenv.config()
15 |
16 | connectDB()
17 |
18 | const app = express()
19 |
20 | if (process.env.NODE_ENV === 'development') {
21 | app.use(morgan('dev'))
22 | }
23 |
24 | app.use(express.json())
25 |
26 | app.use('/api/products', productRoutes)
27 | app.use('/api/users', userRoutes)
28 | app.use('/api/orders', orderRoutes)
29 | app.use('/api/upload', uploadRoutes)
30 |
31 | app.get('/api/config/paypal', (req, res) =>
32 | res.send(process.env.PAYPAL_CLIENT_ID)
33 | )
34 |
35 | const __dirname = path.resolve()
36 | app.use('/uploads', express.static(path.join(__dirname, '/uploads')))
37 |
38 | if (process.env.NODE_ENV === 'production') {
39 | app.use(express.static(path.join(__dirname, '/frontend/build')))
40 |
41 | app.get('*', (req, res) =>
42 | res.sendFile(path.resolve(__dirname, 'frontend', 'build', 'index.html'))
43 | )
44 | } else {
45 | app.get('/', (req, res) => {
46 | res.send('API is running....')
47 | })
48 | }
49 |
50 | app.use(notFound)
51 | app.use(errorHandler)
52 |
53 | const PORT = process.env.PORT || 5000
54 |
55 | app.listen(
56 | PORT,
57 | console.log(
58 | `Server running in ${process.env.NODE_ENV} mode on port ${PORT}`.yellow.bold
59 | )
60 | )
61 |
--------------------------------------------------------------------------------
/frontend/src/components/Rating.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Rating = ({ value, text, color }) => {
4 | return (
5 |
6 |
7 | = 1
11 | ? 'fas fa-star'
12 | : value >= 0.5
13 | ? 'fas fa-star-half-alt'
14 | : 'far fa-star'
15 | }
16 | >
17 |
18 |
19 | = 2
23 | ? 'fas fa-star'
24 | : value >= 1.5
25 | ? 'fas fa-star-half-alt'
26 | : 'far fa-star'
27 | }
28 | >
29 |
30 |
31 | = 3
35 | ? 'fas fa-star'
36 | : value >= 2.5
37 | ? 'fas fa-star-half-alt'
38 | : 'far fa-star'
39 | }
40 | >
41 |
42 |
43 | = 4
47 | ? 'fas fa-star'
48 | : value >= 3.5
49 | ? 'fas fa-star-half-alt'
50 | : 'far fa-star'
51 | }
52 | >
53 |
54 |
55 | = 5
59 | ? 'fas fa-star'
60 | : value >= 4.5
61 | ? 'fas fa-star-half-alt'
62 | : 'far fa-star'
63 | }
64 | >
65 |
66 | {text && text}
67 |
68 | )
69 | }
70 |
71 | Rating.defaultProps = {
72 | color: '#f8e825',
73 | }
74 |
75 | export default Rating
76 |
--------------------------------------------------------------------------------
/frontend/src/screens/HomeScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { Row, Col } from 'react-bootstrap'
5 | import Product from '../components/Product'
6 | import Message from '../components/Message'
7 | import Loader from '../components/Loader'
8 | import Paginate from '../components/Paginate'
9 | import ProductCarousel from '../components/ProductCarousel'
10 | import Meta from '../components/Meta'
11 | import { listProducts } from '../actions/productActions'
12 |
13 | const HomeScreen = ({ match }) => {
14 | const keyword = match.params.keyword
15 |
16 | const pageNumber = match.params.pageNumber || 1
17 |
18 | const dispatch = useDispatch()
19 |
20 | const productList = useSelector((state) => state.productList)
21 | const { loading, error, products, page, pages } = productList
22 |
23 | useEffect(() => {
24 | dispatch(listProducts(keyword, pageNumber))
25 | }, [dispatch, keyword, pageNumber])
26 |
27 | return (
28 | <>
29 |
30 | {!keyword ? (
31 |
32 | ) : (
33 |
34 | Go Back
35 |
36 | )}
37 | Latest Products
38 | {loading ? (
39 |
40 | ) : error ? (
41 | {error}
42 | ) : (
43 | <>
44 |
45 | {products.map((product) => (
46 |
47 |
48 |
49 | ))}
50 |
51 |
56 | >
57 | )}
58 | >
59 | )
60 | }
61 |
62 | export default HomeScreen
63 |
--------------------------------------------------------------------------------
/backend/models/orderModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 |
3 | const orderSchema = mongoose.Schema(
4 | {
5 | user: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | required: true,
8 | ref: 'User',
9 | },
10 | orderItems: [
11 | {
12 | name: { type: String, required: true },
13 | qty: { type: Number, required: true },
14 | image: { type: String, required: true },
15 | price: { type: Number, required: true },
16 | product: {
17 | type: mongoose.Schema.Types.ObjectId,
18 | required: true,
19 | ref: 'Product',
20 | },
21 | },
22 | ],
23 | shippingAddress: {
24 | address: { type: String, required: true },
25 | city: { type: String, required: true },
26 | postalCode: { type: String, required: true },
27 | country: { type: String, required: true },
28 | },
29 | paymentMethod: {
30 | type: String,
31 | required: true,
32 | },
33 | paymentResult: {
34 | id: { type: String },
35 | status: { type: String },
36 | update_time: { type: String },
37 | email_address: { type: String },
38 | },
39 | taxPrice: {
40 | type: Number,
41 | required: true,
42 | default: 0.0,
43 | },
44 | shippingPrice: {
45 | type: Number,
46 | required: true,
47 | default: 0.0,
48 | },
49 | totalPrice: {
50 | type: Number,
51 | required: true,
52 | default: 0.0,
53 | },
54 | isPaid: {
55 | type: Boolean,
56 | required: true,
57 | default: false,
58 | },
59 | paidAt: {
60 | type: Date,
61 | },
62 | isDelivered: {
63 | type: Boolean,
64 | required: true,
65 | default: false,
66 | },
67 | deliveredAt: {
68 | type: Date,
69 | },
70 | },
71 | {
72 | timestamps: true,
73 | }
74 | )
75 |
76 | const Order = mongoose.model('Order', orderSchema)
77 |
78 | export default Order
79 |
--------------------------------------------------------------------------------
/frontend/src/screens/PaymentScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Form, Button, Col } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import FormContainer from '../components/FormContainer'
5 | import CheckoutSteps from '../components/CheckoutSteps'
6 | import { savePaymentMethod } from '../actions/cartActions'
7 |
8 | const PaymentScreen = ({ history }) => {
9 | const cart = useSelector((state) => state.cart)
10 | const { shippingAddress } = cart
11 |
12 | if (!shippingAddress.address) {
13 | history.push('/shipping')
14 | }
15 |
16 | const [paymentMethod, setPaymentMethod] = useState('PayPal')
17 |
18 | const dispatch = useDispatch()
19 |
20 | const submitHandler = (e) => {
21 | e.preventDefault()
22 | dispatch(savePaymentMethod(paymentMethod))
23 | history.push('/placeorder')
24 | }
25 |
26 | return (
27 |
28 |
29 | Payment Method
30 |
32 | Select Method
33 |
34 | setPaymentMethod(e.target.value)}
42 | >
43 | {/* setPaymentMethod(e.target.value)}
50 | > */}
51 |
52 |
53 |
54 |
57 |
58 |
59 | )
60 | }
61 |
62 | export default PaymentScreen
63 |
--------------------------------------------------------------------------------
/frontend/src/screens/LoginScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { Form, Button, Row, Col } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import Loader from '../components/Loader'
7 | import FormContainer from '../components/FormContainer'
8 | import { login } from '../actions/userActions'
9 |
10 | const LoginScreen = ({ location, history }) => {
11 | const [email, setEmail] = useState('')
12 | const [password, setPassword] = useState('')
13 |
14 | const dispatch = useDispatch()
15 |
16 | const userLogin = useSelector((state) => state.userLogin)
17 | const { loading, error, userInfo } = userLogin
18 |
19 | const redirect = location.search ? location.search.split('=')[1] : '/'
20 |
21 | useEffect(() => {
22 | if (userInfo) {
23 | history.push(redirect)
24 | }
25 | }, [history, userInfo, redirect])
26 |
27 | const submitHandler = (e) => {
28 | e.preventDefault()
29 | dispatch(login(email, password))
30 | }
31 |
32 | return (
33 |
34 | Sign In
35 | {error && {error}}
36 | {loading && }
37 |
39 | Email Address
40 | setEmail(e.target.value)}
45 | >
46 |
47 |
48 |
49 | Password
50 | setPassword(e.target.value)}
55 | >
56 |
57 |
58 |
61 |
62 |
63 |
64 |
65 | New Customer?{' '}
66 |
67 | Register
68 |
69 |
70 |
71 |
72 | )
73 | }
74 |
75 | export default LoginScreen
76 |
--------------------------------------------------------------------------------
/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 | import {
5 | productListReducer,
6 | productDetailsReducer,
7 | productDeleteReducer,
8 | productCreateReducer,
9 | productUpdateReducer,
10 | productReviewCreateReducer,
11 | productTopRatedReducer,
12 | } from './reducers/productReducers'
13 | import { cartReducer } from './reducers/cartReducers'
14 | import {
15 | userLoginReducer,
16 | userRegisterReducer,
17 | userDetailsReducer,
18 | userUpdateProfileReducer,
19 | userListReducer,
20 | userDeleteReducer,
21 | userUpdateReducer,
22 | } from './reducers/userReducers'
23 | import {
24 | orderCreateReducer,
25 | orderDetailsReducer,
26 | orderPayReducer,
27 | orderDeliverReducer,
28 | orderListMyReducer,
29 | orderListReducer,
30 | } from './reducers/orderReducers'
31 |
32 | const reducer = combineReducers({
33 | productList: productListReducer,
34 | productDetails: productDetailsReducer,
35 | productDelete: productDeleteReducer,
36 | productCreate: productCreateReducer,
37 | productUpdate: productUpdateReducer,
38 | productReviewCreate: productReviewCreateReducer,
39 | productTopRated: productTopRatedReducer,
40 | cart: cartReducer,
41 | userLogin: userLoginReducer,
42 | userRegister: userRegisterReducer,
43 | userDetails: userDetailsReducer,
44 | userUpdateProfile: userUpdateProfileReducer,
45 | userList: userListReducer,
46 | userDelete: userDeleteReducer,
47 | userUpdate: userUpdateReducer,
48 | orderCreate: orderCreateReducer,
49 | orderDetails: orderDetailsReducer,
50 | orderPay: orderPayReducer,
51 | orderDeliver: orderDeliverReducer,
52 | orderListMy: orderListMyReducer,
53 | orderList: orderListReducer,
54 | })
55 |
56 | const cartItemsFromStorage = localStorage.getItem('cartItems')
57 | ? JSON.parse(localStorage.getItem('cartItems'))
58 | : []
59 |
60 | const userInfoFromStorage = localStorage.getItem('userInfo')
61 | ? JSON.parse(localStorage.getItem('userInfo'))
62 | : null
63 |
64 | const shippingAddressFromStorage = localStorage.getItem('shippingAddress')
65 | ? JSON.parse(localStorage.getItem('shippingAddress'))
66 | : {}
67 |
68 | const initialState = {
69 | cart: {
70 | cartItems: cartItemsFromStorage,
71 | shippingAddress: shippingAddressFromStorage,
72 | },
73 | userLogin: { userInfo: userInfoFromStorage },
74 | }
75 |
76 | const middleware = [thunk]
77 |
78 | const store = createStore(
79 | reducer,
80 | initialState,
81 | composeWithDevTools(applyMiddleware(...middleware))
82 | )
83 |
84 | export default store
85 |
--------------------------------------------------------------------------------
/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: 3,
11 | rating: 0,
12 | numReviews: 0,
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: 10,
23 | rating: 0,
24 | numReviews: 0,
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: 0,
35 | rating: 0,
36 | numReviews: 0,
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: 10,
47 | rating: 0,
48 | numReviews: 0,
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: 0,
60 | numReviews: 0,
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: 0,
72 | numReviews: 0,
73 | },
74 | ]
75 |
76 | export default products
77 |
--------------------------------------------------------------------------------
/frontend/src/screens/OrderListScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { LinkContainer } from 'react-router-bootstrap'
3 | import { Table, Button } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import Loader from '../components/Loader'
7 | import { listOrders } from '../actions/orderActions'
8 |
9 | const OrderListScreen = ({ history }) => {
10 | const dispatch = useDispatch()
11 |
12 | const orderList = useSelector((state) => state.orderList)
13 | const { loading, error, orders } = orderList
14 |
15 | const userLogin = useSelector((state) => state.userLogin)
16 | const { userInfo } = userLogin
17 |
18 | useEffect(() => {
19 | if (userInfo && userInfo.isAdmin) {
20 | dispatch(listOrders())
21 | } else {
22 | history.push('/login')
23 | }
24 | }, [dispatch, history, userInfo])
25 |
26 | return (
27 | <>
28 | Orders
29 | {loading ? (
30 |
31 | ) : error ? (
32 | {error}
33 | ) : (
34 |
35 |
36 |
37 | | ID |
38 | USER |
39 | DATE |
40 | TOTAL |
41 | PAID |
42 | DELIVERED |
43 | |
44 |
45 |
46 |
47 | {orders.map((order) => (
48 |
49 | | {order._id} |
50 | {order.user && order.user.name} |
51 | {order.createdAt.substring(0, 10)} |
52 | ${order.totalPrice} |
53 |
54 | {order.isPaid ? (
55 | order.paidAt.substring(0, 10)
56 | ) : (
57 |
58 | )}
59 | |
60 |
61 | {order.isDelivered ? (
62 | order.deliveredAt.substring(0, 10)
63 | ) : (
64 |
65 | )}
66 | |
67 |
68 |
69 |
72 |
73 | |
74 |
75 | ))}
76 |
77 |
78 | )}
79 | >
80 | )
81 | }
82 |
83 | export default OrderListScreen
84 |
--------------------------------------------------------------------------------
/frontend/src/screens/ShippingScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Form, Button } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import FormContainer from '../components/FormContainer'
5 | import CheckoutSteps from '../components/CheckoutSteps'
6 | import { saveShippingAddress } from '../actions/cartActions'
7 |
8 | const ShippingScreen = ({ history }) => {
9 | const cart = useSelector((state) => state.cart)
10 | const { shippingAddress } = cart
11 |
12 | const [address, setAddress] = useState(shippingAddress.address)
13 | const [city, setCity] = useState(shippingAddress.city)
14 | const [postalCode, setPostalCode] = useState(shippingAddress.postalCode)
15 | const [country, setCountry] = useState(shippingAddress.country)
16 |
17 | const dispatch = useDispatch()
18 |
19 | const submitHandler = (e) => {
20 | e.preventDefault()
21 | dispatch(saveShippingAddress({ address, city, postalCode, country }))
22 | history.push('/payment')
23 | }
24 |
25 | return (
26 |
27 |
28 | Shipping
29 |
31 | Address
32 | setAddress(e.target.value)}
38 | >
39 |
40 |
41 |
42 | City
43 | setCity(e.target.value)}
49 | >
50 |
51 |
52 |
53 | Postal Code
54 | setPostalCode(e.target.value)}
60 | >
61 |
62 |
63 |
64 | Country
65 | setCountry(e.target.value)}
71 | >
72 |
73 |
74 |
77 |
78 |
79 | )
80 | }
81 |
82 | export default ShippingScreen
83 |
--------------------------------------------------------------------------------
/frontend/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route } from 'react-router-dom'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { LinkContainer } from 'react-router-bootstrap'
5 | import { Navbar, Nav, Container, NavDropdown } from 'react-bootstrap'
6 | import SearchBox from './SearchBox'
7 | import { logout } from '../actions/userActions'
8 |
9 | const Header = () => {
10 | const dispatch = useDispatch()
11 |
12 | const userLogin = useSelector((state) => state.userLogin)
13 | const { userInfo } = userLogin
14 |
15 | const logoutHandler = () => {
16 | dispatch(logout())
17 | }
18 |
19 | return (
20 |
21 |
22 |
23 |
24 | ProShop
25 |
26 |
27 |
28 | } />
29 |
65 |
66 |
67 |
68 |
69 | )
70 | }
71 |
72 | export default Header
73 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { BrowserRouter as Router, Route } from 'react-router-dom'
3 | import { Container } from 'react-bootstrap'
4 | import Header from './components/Header'
5 | import Footer from './components/Footer'
6 | import HomeScreen from './screens/HomeScreen'
7 | import ProductScreen from './screens/ProductScreen'
8 | import CartScreen from './screens/CartScreen'
9 | import LoginScreen from './screens/LoginScreen'
10 | import RegisterScreen from './screens/RegisterScreen'
11 | import ProfileScreen from './screens/ProfileScreen'
12 | import ShippingScreen from './screens/ShippingScreen'
13 | import PaymentScreen from './screens/PaymentScreen'
14 | import PlaceOrderScreen from './screens/PlaceOrderScreen'
15 | import OrderScreen from './screens/OrderScreen'
16 | import UserListScreen from './screens/UserListScreen'
17 | import UserEditScreen from './screens/UserEditScreen'
18 | import ProductListScreen from './screens/ProductListScreen'
19 | import ProductEditScreen from './screens/ProductEditScreen'
20 | import OrderListScreen from './screens/OrderListScreen'
21 |
22 | const App = () => {
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
44 |
49 |
50 |
51 |
52 |
53 |
58 |
59 |
60 |
61 |
62 |
63 | )
64 | }
65 |
66 | export default App
67 |
--------------------------------------------------------------------------------
/frontend/src/screens/UserListScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { LinkContainer } from 'react-router-bootstrap'
3 | import { Table, Button } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import Loader from '../components/Loader'
7 | import { listUsers, deleteUser } from '../actions/userActions'
8 |
9 | const UserListScreen = ({ history }) => {
10 | const dispatch = useDispatch()
11 |
12 | const userList = useSelector((state) => state.userList)
13 | const { loading, error, users } = userList
14 |
15 | const userLogin = useSelector((state) => state.userLogin)
16 | const { userInfo } = userLogin
17 |
18 | const userDelete = useSelector((state) => state.userDelete)
19 | const { success: successDelete } = userDelete
20 |
21 | useEffect(() => {
22 | if (userInfo && userInfo.isAdmin) {
23 | dispatch(listUsers())
24 | } else {
25 | history.push('/login')
26 | }
27 | }, [dispatch, history, successDelete, userInfo])
28 |
29 | const deleteHandler = (id) => {
30 | if (window.confirm('Are you sure')) {
31 | dispatch(deleteUser(id))
32 | }
33 | }
34 |
35 | return (
36 | <>
37 | Users
38 | {loading ? (
39 |
40 | ) : error ? (
41 | {error}
42 | ) : (
43 |
44 |
45 |
46 | | ID |
47 | NAME |
48 | EMAIL |
49 | ADMIN |
50 | |
51 |
52 |
53 |
54 | {users.map((user) => (
55 |
56 | | {user._id} |
57 | {user.name} |
58 |
59 | {user.email}
60 | |
61 |
62 | {user.isAdmin ? (
63 |
64 | ) : (
65 |
66 | )}
67 | |
68 |
69 |
70 |
73 |
74 |
81 | |
82 |
83 | ))}
84 |
85 |
86 | )}
87 | >
88 | )
89 | }
90 |
91 | export default UserListScreen
92 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `npm run build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/backend/controllers/orderController.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from 'express-async-handler'
2 | import Order from '../models/orderModel.js'
3 |
4 | // @desc Create new order
5 | // @route POST /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 | if (orderItems && orderItems.length === 0) {
19 | res.status(400)
20 | throw new Error('No order items')
21 | return
22 | } else {
23 | const order = new Order({
24 | orderItems,
25 | user: req.user._id,
26 | shippingAddress,
27 | paymentMethod,
28 | itemsPrice,
29 | taxPrice,
30 | shippingPrice,
31 | totalPrice,
32 | })
33 |
34 | const createdOrder = await order.save()
35 |
36 | res.status(201).json(createdOrder)
37 | }
38 | })
39 |
40 | // @desc Get order by ID
41 | // @route GET /api/orders/:id
42 | // @access Private
43 | const getOrderById = asyncHandler(async (req, res) => {
44 | const order = await Order.findById(req.params.id).populate(
45 | 'user',
46 | 'name email'
47 | )
48 |
49 | if (order) {
50 | res.json(order)
51 | } else {
52 | res.status(404)
53 | throw new Error('Order not found')
54 | }
55 | })
56 |
57 | // @desc Update order to paid
58 | // @route GET /api/orders/:id/pay
59 | // @access Private
60 | const updateOrderToPaid = asyncHandler(async (req, res) => {
61 | const order = await Order.findById(req.params.id)
62 |
63 | if (order) {
64 | order.isPaid = true
65 | order.paidAt = Date.now()
66 | order.paymentResult = {
67 | id: req.body.id,
68 | status: req.body.status,
69 | update_time: req.body.update_time,
70 | email_address: req.body.payer.email_address,
71 | }
72 |
73 | const updatedOrder = await order.save()
74 |
75 | res.json(updatedOrder)
76 | } else {
77 | res.status(404)
78 | throw new Error('Order not found')
79 | }
80 | })
81 |
82 | // @desc Update order to delivered
83 | // @route GET /api/orders/:id/deliver
84 | // @access Private/Admin
85 | const updateOrderToDelivered = asyncHandler(async (req, res) => {
86 | const order = await Order.findById(req.params.id)
87 |
88 | if (order) {
89 | order.isDelivered = true
90 | order.deliveredAt = Date.now()
91 |
92 | const updatedOrder = await order.save()
93 |
94 | res.json(updatedOrder)
95 | } else {
96 | res.status(404)
97 | throw new Error('Order not found')
98 | }
99 | })
100 |
101 | // @desc Get logged in user orders
102 | // @route GET /api/orders/myorders
103 | // @access Private
104 | const getMyOrders = asyncHandler(async (req, res) => {
105 | const orders = await Order.find({ user: req.user._id })
106 | res.json(orders)
107 | })
108 |
109 | // @desc Get all orders
110 | // @route GET /api/orders
111 | // @access Private/Admin
112 | const getOrders = asyncHandler(async (req, res) => {
113 | const orders = await Order.find({}).populate('user', 'id name')
114 | res.json(orders)
115 | })
116 |
117 | export {
118 | addOrderItems,
119 | getOrderById,
120 | updateOrderToPaid,
121 | updateOrderToDelivered,
122 | getMyOrders,
123 | getOrders,
124 | }
125 |
--------------------------------------------------------------------------------
/frontend/src/screens/RegisterScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { Form, Button, Row, Col } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import Loader from '../components/Loader'
7 | import FormContainer from '../components/FormContainer'
8 | import { register } from '../actions/userActions'
9 |
10 | const RegisterScreen = ({ location, history }) => {
11 | const [name, setName] = useState('')
12 | const [email, setEmail] = useState('')
13 | const [password, setPassword] = useState('')
14 | const [confirmPassword, setConfirmPassword] = useState('')
15 | const [message, setMessage] = useState(null)
16 |
17 | const dispatch = useDispatch()
18 |
19 | const userRegister = useSelector((state) => state.userRegister)
20 | const { loading, error, userInfo } = userRegister
21 |
22 | const redirect = location.search ? location.search.split('=')[1] : '/'
23 |
24 | useEffect(() => {
25 | if (userInfo) {
26 | history.push(redirect)
27 | }
28 | }, [history, userInfo, redirect])
29 |
30 | const submitHandler = (e) => {
31 | e.preventDefault()
32 | if (password !== confirmPassword) {
33 | setMessage('Passwords do not match')
34 | } else {
35 | dispatch(register(name, email, password))
36 | }
37 | }
38 |
39 | return (
40 |
41 | Sign Up
42 | {message && {message}}
43 | {error && {error}}
44 | {loading && }
45 |
47 | Name
48 | setName(e.target.value)}
53 | >
54 |
55 |
56 |
57 | Email Address
58 | setEmail(e.target.value)}
63 | >
64 |
65 |
66 |
67 | Password
68 | setPassword(e.target.value)}
73 | >
74 |
75 |
76 |
77 | Confirm Password
78 | setConfirmPassword(e.target.value)}
83 | >
84 |
85 |
86 |
89 |
90 |
91 |
92 |
93 | Have an Account?{' '}
94 |
95 | Login
96 |
97 |
98 |
99 |
100 | )
101 | }
102 |
103 | export default RegisterScreen
104 |
--------------------------------------------------------------------------------
/frontend/src/screens/UserEditScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { Form, Button } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import Loader from '../components/Loader'
7 | import FormContainer from '../components/FormContainer'
8 | import { getUserDetails, updateUser } from '../actions/userActions'
9 | import { USER_UPDATE_RESET } from '../constants/userConstants'
10 |
11 | const UserEditScreen = ({ match, history }) => {
12 | const userId = match.params.id
13 |
14 | const [name, setName] = useState('')
15 | const [email, setEmail] = useState('')
16 | const [isAdmin, setIsAdmin] = useState(false)
17 |
18 | const dispatch = useDispatch()
19 |
20 | const userDetails = useSelector((state) => state.userDetails)
21 | const { loading, error, user } = userDetails
22 |
23 | const userUpdate = useSelector((state) => state.userUpdate)
24 | const {
25 | loading: loadingUpdate,
26 | error: errorUpdate,
27 | success: successUpdate,
28 | } = userUpdate
29 |
30 | useEffect(() => {
31 | if (successUpdate) {
32 | dispatch({ type: USER_UPDATE_RESET })
33 | history.push('/admin/userlist')
34 | } else {
35 | if (!user.name || user._id !== userId) {
36 | dispatch(getUserDetails(userId))
37 | } else {
38 | setName(user.name)
39 | setEmail(user.email)
40 | setIsAdmin(user.isAdmin)
41 | }
42 | }
43 | }, [dispatch, history, userId, user, successUpdate])
44 |
45 | const submitHandler = (e) => {
46 | e.preventDefault()
47 | dispatch(updateUser({ _id: userId, name, email, isAdmin }))
48 | }
49 |
50 | return (
51 | <>
52 |
53 | Go Back
54 |
55 |
56 | Edit User
57 | {loadingUpdate && }
58 | {errorUpdate && {errorUpdate}}
59 | {loading ? (
60 |
61 | ) : error ? (
62 | {error}
63 | ) : (
64 |
66 | Name
67 | setName(e.target.value)}
72 | >
73 |
74 |
75 |
76 | Email Address
77 | setEmail(e.target.value)}
82 | >
83 |
84 |
85 |
86 | setIsAdmin(e.target.checked)}
91 | >
92 |
93 |
94 |
97 |
98 | )}
99 |
100 | >
101 | )
102 | }
103 |
104 | export default UserEditScreen
105 |
--------------------------------------------------------------------------------
/frontend/src/screens/CartScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { Row, Col, ListGroup, Image, Form, Button, Card } from 'react-bootstrap'
5 | import Message from '../components/Message'
6 | import { addToCart, removeFromCart } from '../actions/cartActions'
7 |
8 | const CartScreen = ({ match, location, history }) => {
9 | const productId = match.params.id
10 |
11 | const qty = location.search ? Number(location.search.split('=')[1]) : 1
12 |
13 | const dispatch = useDispatch()
14 |
15 | const cart = useSelector((state) => state.cart)
16 | const { cartItems } = cart
17 |
18 | useEffect(() => {
19 | if (productId) {
20 | dispatch(addToCart(productId, qty))
21 | }
22 | }, [dispatch, productId, qty])
23 |
24 | const removeFromCartHandler = (id) => {
25 | dispatch(removeFromCart(id))
26 | }
27 |
28 | const checkoutHandler = () => {
29 | history.push('/login?redirect=shipping')
30 | }
31 |
32 | return (
33 |
34 |
35 | Shopping Cart
36 | {cartItems.length === 0 ? (
37 |
38 | Your cart is empty Go Back
39 |
40 | ) : (
41 |
42 | {cartItems.map((item) => (
43 |
44 |
45 |
46 |
47 |
48 |
49 | {item.name}
50 |
51 | ${item.price}
52 |
53 |
57 | dispatch(
58 | addToCart(item.product, Number(e.target.value))
59 | )
60 | }
61 | >
62 | {[...Array(item.countInStock).keys()].map((x) => (
63 |
66 | ))}
67 |
68 |
69 |
70 |
77 |
78 |
79 |
80 | ))}
81 |
82 | )}
83 |
84 |
85 |
86 |
87 |
88 |
89 | Subtotal ({cartItems.reduce((acc, item) => acc + item.qty, 0)})
90 | items
91 |
92 | $
93 | {cartItems
94 | .reduce((acc, item) => acc + item.qty * item.price, 0)
95 | .toFixed(2)}
96 |
97 |
98 |
106 |
107 |
108 |
109 |
110 |
111 | )
112 | }
113 |
114 | export default CartScreen
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ProShop eCommerce Platform
2 |
3 | > eCommerce platform built with the MERN stack & Redux.
4 |
5 | ### THIS PROJECT IS DEPRECATED
6 | This project is no longer supported. The new project/course has been released. The code has been cleaned up and now uses Redux Toolkit. You can find the new version [HERE](https://github.com/bradtraversy/proshop-v2)
7 |
8 | 
9 |
10 | ## Features
11 |
12 | - Full featured shopping cart
13 | - Product reviews and ratings
14 | - Top products carousel
15 | - Product pagination
16 | - Product search feature
17 | - User profile with orders
18 | - Admin product management
19 | - Admin user management
20 | - Admin Order details page
21 | - Mark orders as delivered option
22 | - Checkout process (shipping, payment method, etc)
23 | - PayPal / credit card integration
24 | - Database seeder (products & users)
25 |
26 | ## Note on Issues
27 | Please do not post issues here that are related to your own code when taking the course. Add those in the Udemy Q/A. If you clone THIS repo and there are issues, then you can submit
28 |
29 | ## Usage
30 |
31 | ### ES Modules in Node
32 |
33 | We use ECMAScript Modules in the backend in this project. Be sure to have at least Node v14.6+ or you will need to add the "--experimental-modules" flag.
34 |
35 | Also, when importing a file (not a package), be sure to add .js at the end or you will get a "module not found" error
36 |
37 | You can also install and setup Babel if you would like
38 |
39 | ### Env Variables
40 |
41 | Create a .env file in then root and add the following
42 |
43 | ```
44 | NODE_ENV = development
45 | PORT = 5000
46 | MONGO_URI = your mongodb uri
47 | JWT_SECRET = 'abc123'
48 | PAYPAL_CLIENT_ID = your paypal client id
49 | ```
50 |
51 | ### Install Dependencies (frontend & backend)
52 |
53 | ```
54 | npm install
55 | cd frontend
56 | npm install
57 | ```
58 |
59 | ### Run
60 |
61 | ```
62 | # Run frontend (:3000) & backend (:5000)
63 | npm run dev
64 |
65 | # Run backend only
66 | npm run server
67 | ```
68 |
69 | ## Build & Deploy
70 |
71 | ```
72 | # Create frontend prod build
73 | cd frontend
74 | npm run build
75 | ```
76 |
77 | There is a Heroku postbuild script, so if you push to Heroku, no need to build manually for deployment to Heroku
78 |
79 | ### Seed Database
80 |
81 | You can use the following commands to seed the database with some sample users and products as well as destroy all data
82 |
83 | ```
84 | # Import data
85 | npm run data:import
86 |
87 | # Destroy data
88 | npm run data:destroy
89 | ```
90 |
91 | ```
92 | Sample User Logins
93 |
94 | admin@example.com (Admin)
95 | 123456
96 |
97 | john@example.com (Customer)
98 | 123456
99 |
100 | jane@example.com (Customer)
101 | 123456
102 | ```
103 |
104 |
105 | ## License
106 |
107 | The MIT License
108 |
109 | Copyright (c) 2020 Traversy Media https://traversymedia.com
110 |
111 | Permission is hereby granted, free of charge, to any person obtaining a copy
112 | of this software and associated documentation files (the "Software"), to deal
113 | in the Software without restriction, including without limitation the rights
114 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
115 | copies of the Software, and to permit persons to whom the Software is
116 | furnished to do so, subject to the following conditions:
117 |
118 | The above copyright notice and this permission notice shall be included in
119 | all copies or substantial portions of the Software.
120 |
121 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
122 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
123 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
124 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
125 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
126 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
127 | THE SOFTWARE.
128 |
--------------------------------------------------------------------------------
/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_LIST_REQUEST,
7 | USER_LIST_SUCCESS,
8 | USER_LIST_FAIL,
9 | USER_LIST_RESET,
10 | USER_LOGIN_FAIL,
11 | USER_LOGIN_REQUEST,
12 | USER_LOGIN_SUCCESS,
13 | USER_LOGOUT,
14 | USER_REGISTER_FAIL,
15 | USER_REGISTER_REQUEST,
16 | USER_REGISTER_SUCCESS,
17 | USER_UPDATE_PROFILE_FAIL,
18 | USER_UPDATE_PROFILE_REQUEST,
19 | USER_UPDATE_PROFILE_SUCCESS,
20 | USER_DELETE_REQUEST,
21 | USER_DELETE_SUCCESS,
22 | USER_DELETE_FAIL,
23 | USER_UPDATE_RESET,
24 | USER_UPDATE_REQUEST,
25 | USER_UPDATE_SUCCESS,
26 | USER_UPDATE_FAIL,
27 | USER_UPDATE_PROFILE_RESET,
28 | } from '../constants/userConstants'
29 |
30 | export const userLoginReducer = (state = {}, action) => {
31 | switch (action.type) {
32 | case USER_LOGIN_REQUEST:
33 | return { loading: true }
34 | case USER_LOGIN_SUCCESS:
35 | return { loading: false, userInfo: action.payload }
36 | case USER_LOGIN_FAIL:
37 | return { loading: false, error: action.payload }
38 | case USER_LOGOUT:
39 | return {}
40 | default:
41 | return state
42 | }
43 | }
44 |
45 | export const userRegisterReducer = (state = {}, action) => {
46 | switch (action.type) {
47 | case USER_REGISTER_REQUEST:
48 | return { loading: true }
49 | case USER_REGISTER_SUCCESS:
50 | return { loading: false, userInfo: action.payload }
51 | case USER_REGISTER_FAIL:
52 | return { loading: false, error: action.payload }
53 | case USER_LOGOUT:
54 | return {}
55 | default:
56 | return state
57 | }
58 | }
59 |
60 | export const userDetailsReducer = (state = { user: {} }, action) => {
61 | switch (action.type) {
62 | case USER_DETAILS_REQUEST:
63 | return { ...state, loading: true }
64 | case USER_DETAILS_SUCCESS:
65 | return { loading: false, user: action.payload }
66 | case USER_DETAILS_FAIL:
67 | return { loading: false, error: action.payload }
68 | case USER_DETAILS_RESET:
69 | return { user: {} }
70 | default:
71 | return state
72 | }
73 | }
74 |
75 | export const userUpdateProfileReducer = (state = {}, action) => {
76 | switch (action.type) {
77 | case USER_UPDATE_PROFILE_REQUEST:
78 | return { loading: true }
79 | case USER_UPDATE_PROFILE_SUCCESS:
80 | return { loading: false, success: true, userInfo: action.payload }
81 | case USER_UPDATE_PROFILE_FAIL:
82 | return { loading: false, error: action.payload }
83 | case USER_UPDATE_PROFILE_RESET:
84 | return {}
85 | default:
86 | return state
87 | }
88 | }
89 |
90 | export const userListReducer = (state = { users: [] }, action) => {
91 | switch (action.type) {
92 | case USER_LIST_REQUEST:
93 | return { loading: true }
94 | case USER_LIST_SUCCESS:
95 | return { loading: false, users: action.payload }
96 | case USER_LIST_FAIL:
97 | return { loading: false, error: action.payload }
98 | case USER_LIST_RESET:
99 | return { users: [] }
100 | default:
101 | return state
102 | }
103 | }
104 |
105 | export const userDeleteReducer = (state = {}, action) => {
106 | switch (action.type) {
107 | case USER_DELETE_REQUEST:
108 | return { loading: true }
109 | case USER_DELETE_SUCCESS:
110 | return { loading: false, success: true }
111 | case USER_DELETE_FAIL:
112 | return { loading: false, error: action.payload }
113 | default:
114 | return state
115 | }
116 | }
117 |
118 | export const userUpdateReducer = (state = { user: {} }, action) => {
119 | switch (action.type) {
120 | case USER_UPDATE_REQUEST:
121 | return { loading: true }
122 | case USER_UPDATE_SUCCESS:
123 | return { loading: false, success: true }
124 | case USER_UPDATE_FAIL:
125 | return { loading: false, error: action.payload }
126 | case USER_UPDATE_RESET:
127 | return {
128 | user: {},
129 | }
130 | default:
131 | return state
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/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_MY_REQUEST,
13 | ORDER_LIST_MY_SUCCESS,
14 | ORDER_LIST_MY_FAIL,
15 | ORDER_LIST_MY_RESET,
16 | ORDER_LIST_FAIL,
17 | ORDER_LIST_SUCCESS,
18 | ORDER_LIST_REQUEST,
19 | ORDER_DELIVER_FAIL,
20 | ORDER_DELIVER_SUCCESS,
21 | ORDER_DELIVER_REQUEST,
22 | ORDER_DELIVER_RESET,
23 | ORDER_CREATE_RESET,
24 | } from '../constants/orderConstants'
25 |
26 | export const orderCreateReducer = (state = {}, action) => {
27 | switch (action.type) {
28 | case ORDER_CREATE_REQUEST:
29 | return {
30 | loading: true,
31 | }
32 | case ORDER_CREATE_SUCCESS:
33 | return {
34 | loading: false,
35 | success: true,
36 | order: action.payload,
37 | }
38 | case ORDER_CREATE_FAIL:
39 | return {
40 | loading: false,
41 | error: action.payload,
42 | }
43 | case ORDER_CREATE_RESET:
44 | return {}
45 | default:
46 | return state
47 | }
48 | }
49 |
50 | export const orderDetailsReducer = (
51 | state = { loading: true, orderItems: [], shippingAddress: {} },
52 | action
53 | ) => {
54 | switch (action.type) {
55 | case ORDER_DETAILS_REQUEST:
56 | return {
57 | ...state,
58 | loading: true,
59 | }
60 | case ORDER_DETAILS_SUCCESS:
61 | return {
62 | loading: false,
63 | order: action.payload,
64 | }
65 | case ORDER_DETAILS_FAIL:
66 | return {
67 | loading: false,
68 | error: action.payload,
69 | }
70 | default:
71 | return state
72 | }
73 | }
74 |
75 | export const orderPayReducer = (state = {}, action) => {
76 | switch (action.type) {
77 | case ORDER_PAY_REQUEST:
78 | return {
79 | loading: true,
80 | }
81 | case ORDER_PAY_SUCCESS:
82 | return {
83 | loading: false,
84 | success: true,
85 | }
86 | case ORDER_PAY_FAIL:
87 | return {
88 | loading: false,
89 | error: action.payload,
90 | }
91 | case ORDER_PAY_RESET:
92 | return {}
93 | default:
94 | return state
95 | }
96 | }
97 |
98 | export const orderDeliverReducer = (state = {}, action) => {
99 | switch (action.type) {
100 | case ORDER_DELIVER_REQUEST:
101 | return {
102 | loading: true,
103 | }
104 | case ORDER_DELIVER_SUCCESS:
105 | return {
106 | loading: false,
107 | success: true,
108 | }
109 | case ORDER_DELIVER_FAIL:
110 | return {
111 | loading: false,
112 | error: action.payload,
113 | }
114 | case ORDER_DELIVER_RESET:
115 | return {}
116 | default:
117 | return state
118 | }
119 | }
120 |
121 | export const orderListMyReducer = (state = { orders: [] }, action) => {
122 | switch (action.type) {
123 | case ORDER_LIST_MY_REQUEST:
124 | return {
125 | loading: true,
126 | }
127 | case ORDER_LIST_MY_SUCCESS:
128 | return {
129 | loading: false,
130 | orders: action.payload,
131 | }
132 | case ORDER_LIST_MY_FAIL:
133 | return {
134 | loading: false,
135 | error: action.payload,
136 | }
137 | case ORDER_LIST_MY_RESET:
138 | return { orders: [] }
139 | default:
140 | return state
141 | }
142 | }
143 |
144 | export const orderListReducer = (state = { orders: [] }, action) => {
145 | switch (action.type) {
146 | case ORDER_LIST_REQUEST:
147 | return {
148 | loading: true,
149 | }
150 | case ORDER_LIST_SUCCESS:
151 | return {
152 | loading: false,
153 | orders: action.payload,
154 | }
155 | case ORDER_LIST_FAIL:
156 | return {
157 | loading: false,
158 | error: action.payload,
159 | }
160 | default:
161 | return state
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/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 | PRODUCT_DELETE_REQUEST,
9 | PRODUCT_DELETE_SUCCESS,
10 | PRODUCT_DELETE_FAIL,
11 | PRODUCT_CREATE_RESET,
12 | PRODUCT_CREATE_FAIL,
13 | PRODUCT_CREATE_SUCCESS,
14 | PRODUCT_CREATE_REQUEST,
15 | PRODUCT_UPDATE_REQUEST,
16 | PRODUCT_UPDATE_SUCCESS,
17 | PRODUCT_UPDATE_FAIL,
18 | PRODUCT_UPDATE_RESET,
19 | PRODUCT_CREATE_REVIEW_REQUEST,
20 | PRODUCT_CREATE_REVIEW_SUCCESS,
21 | PRODUCT_CREATE_REVIEW_FAIL,
22 | PRODUCT_CREATE_REVIEW_RESET,
23 | PRODUCT_TOP_REQUEST,
24 | PRODUCT_TOP_SUCCESS,
25 | PRODUCT_TOP_FAIL,
26 | } from '../constants/productConstants'
27 |
28 | export const productListReducer = (state = { products: [] }, action) => {
29 | switch (action.type) {
30 | case PRODUCT_LIST_REQUEST:
31 | return { loading: true, products: [] }
32 | case PRODUCT_LIST_SUCCESS:
33 | return {
34 | loading: false,
35 | products: action.payload.products,
36 | pages: action.payload.pages,
37 | page: action.payload.page,
38 | }
39 | case PRODUCT_LIST_FAIL:
40 | return { loading: false, error: action.payload }
41 | default:
42 | return state
43 | }
44 | }
45 |
46 | export const productDetailsReducer = (
47 | state = { product: { reviews: [] } },
48 | action
49 | ) => {
50 | switch (action.type) {
51 | case PRODUCT_DETAILS_REQUEST:
52 | return { ...state, loading: true }
53 | case PRODUCT_DETAILS_SUCCESS:
54 | return { loading: false, product: action.payload }
55 | case PRODUCT_DETAILS_FAIL:
56 | return { loading: false, error: action.payload }
57 | default:
58 | return state
59 | }
60 | }
61 |
62 | export const productDeleteReducer = (state = {}, action) => {
63 | switch (action.type) {
64 | case PRODUCT_DELETE_REQUEST:
65 | return { loading: true }
66 | case PRODUCT_DELETE_SUCCESS:
67 | return { loading: false, success: true }
68 | case PRODUCT_DELETE_FAIL:
69 | return { loading: false, error: action.payload }
70 | default:
71 | return state
72 | }
73 | }
74 |
75 | export const productCreateReducer = (state = {}, action) => {
76 | switch (action.type) {
77 | case PRODUCT_CREATE_REQUEST:
78 | return { loading: true }
79 | case PRODUCT_CREATE_SUCCESS:
80 | return { loading: false, success: true, product: action.payload }
81 | case PRODUCT_CREATE_FAIL:
82 | return { loading: false, error: action.payload }
83 | case PRODUCT_CREATE_RESET:
84 | return {}
85 | default:
86 | return state
87 | }
88 | }
89 |
90 | export const productUpdateReducer = (state = { product: {} }, action) => {
91 | switch (action.type) {
92 | case PRODUCT_UPDATE_REQUEST:
93 | return { loading: true }
94 | case PRODUCT_UPDATE_SUCCESS:
95 | return { loading: false, success: true, product: action.payload }
96 | case PRODUCT_UPDATE_FAIL:
97 | return { loading: false, error: action.payload }
98 | case PRODUCT_UPDATE_RESET:
99 | return { product: {} }
100 | default:
101 | return state
102 | }
103 | }
104 |
105 | export const productReviewCreateReducer = (state = {}, action) => {
106 | switch (action.type) {
107 | case PRODUCT_CREATE_REVIEW_REQUEST:
108 | return { loading: true }
109 | case PRODUCT_CREATE_REVIEW_SUCCESS:
110 | return { loading: false, success: true }
111 | case PRODUCT_CREATE_REVIEW_FAIL:
112 | return { loading: false, error: action.payload }
113 | case PRODUCT_CREATE_REVIEW_RESET:
114 | return {}
115 | default:
116 | return state
117 | }
118 | }
119 |
120 | export const productTopRatedReducer = (state = { products: [] }, action) => {
121 | switch (action.type) {
122 | case PRODUCT_TOP_REQUEST:
123 | return { loading: true, products: [] }
124 | case PRODUCT_TOP_SUCCESS:
125 | return { loading: false, products: action.payload }
126 | case PRODUCT_TOP_FAIL:
127 | return { loading: false, error: action.payload }
128 | default:
129 | return state
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/frontend/src/screens/ProductListScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { LinkContainer } from 'react-router-bootstrap'
3 | import { Table, Button, Row, Col } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import Loader from '../components/Loader'
7 | import Paginate from '../components/Paginate'
8 | import {
9 | listProducts,
10 | deleteProduct,
11 | createProduct,
12 | } from '../actions/productActions'
13 | import { PRODUCT_CREATE_RESET } from '../constants/productConstants'
14 |
15 | const ProductListScreen = ({ history, match }) => {
16 | const pageNumber = match.params.pageNumber || 1
17 |
18 | const dispatch = useDispatch()
19 |
20 | const productList = useSelector((state) => state.productList)
21 | const { loading, error, products, page, pages } = productList
22 |
23 | const productDelete = useSelector((state) => state.productDelete)
24 | const {
25 | loading: loadingDelete,
26 | error: errorDelete,
27 | success: successDelete,
28 | } = productDelete
29 |
30 | const productCreate = useSelector((state) => state.productCreate)
31 | const {
32 | loading: loadingCreate,
33 | error: errorCreate,
34 | success: successCreate,
35 | product: createdProduct,
36 | } = productCreate
37 |
38 | const userLogin = useSelector((state) => state.userLogin)
39 | const { userInfo } = userLogin
40 |
41 | useEffect(() => {
42 | dispatch({ type: PRODUCT_CREATE_RESET })
43 |
44 | if (!userInfo || !userInfo.isAdmin) {
45 | history.push('/login')
46 | }
47 |
48 | if (successCreate) {
49 | history.push(`/admin/product/${createdProduct._id}/edit`)
50 | } else {
51 | dispatch(listProducts('', pageNumber))
52 | }
53 | }, [
54 | dispatch,
55 | history,
56 | userInfo,
57 | successDelete,
58 | successCreate,
59 | createdProduct,
60 | pageNumber,
61 | ])
62 |
63 | const deleteHandler = (id) => {
64 | if (window.confirm('Are you sure')) {
65 | dispatch(deleteProduct(id))
66 | }
67 | }
68 |
69 | const createProductHandler = () => {
70 | dispatch(createProduct())
71 | }
72 |
73 | return (
74 | <>
75 |
76 |
77 | Products
78 |
79 |
80 |
83 |
84 |
85 | {loadingDelete && }
86 | {errorDelete && {errorDelete}}
87 | {loadingCreate && }
88 | {errorCreate && {errorCreate}}
89 | {loading ? (
90 |
91 | ) : error ? (
92 | {error}
93 | ) : (
94 | <>
95 |
96 |
97 |
98 | | ID |
99 | NAME |
100 | PRICE |
101 | CATEGORY |
102 | BRAND |
103 | |
104 |
105 |
106 |
107 | {products.map((product) => (
108 |
109 | | {product._id} |
110 | {product.name} |
111 | ${product.price} |
112 | {product.category} |
113 | {product.brand} |
114 |
115 |
116 |
119 |
120 |
127 | |
128 |
129 | ))}
130 |
131 |
132 |
133 | >
134 | )}
135 | >
136 | )
137 | }
138 |
139 | export default ProductListScreen
140 |
--------------------------------------------------------------------------------
/backend/controllers/productController.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from 'express-async-handler'
2 | import Product from '../models/productModel.js'
3 |
4 | // @desc Fetch all products
5 | // @route GET /api/products
6 | // @access Public
7 | const getProducts = asyncHandler(async (req, res) => {
8 | const pageSize = 10
9 | const page = Number(req.query.pageNumber) || 1
10 |
11 | const keyword = req.query.keyword
12 | ? {
13 | name: {
14 | $regex: req.query.keyword,
15 | $options: 'i',
16 | },
17 | }
18 | : {}
19 |
20 | const count = await Product.countDocuments({ ...keyword })
21 | const products = await Product.find({ ...keyword })
22 | .limit(pageSize)
23 | .skip(pageSize * (page - 1))
24 |
25 | res.json({ products, page, pages: Math.ceil(count / pageSize) })
26 | })
27 |
28 | // @desc Fetch single product
29 | // @route GET /api/products/:id
30 | // @access Public
31 | const getProductById = asyncHandler(async (req, res) => {
32 | const product = await Product.findById(req.params.id)
33 |
34 | if (product) {
35 | res.json(product)
36 | } else {
37 | res.status(404)
38 | throw new Error('Product not found')
39 | }
40 | })
41 |
42 | // @desc Delete a product
43 | // @route DELETE /api/products/:id
44 | // @access Private/Admin
45 | const deleteProduct = asyncHandler(async (req, res) => {
46 | const product = await Product.findById(req.params.id)
47 |
48 | if (product) {
49 | await product.remove()
50 | res.json({ message: 'Product removed' })
51 | } else {
52 | res.status(404)
53 | throw new Error('Product not found')
54 | }
55 | })
56 |
57 | // @desc Create a product
58 | // @route POST /api/products
59 | // @access Private/Admin
60 | const createProduct = asyncHandler(async (req, res) => {
61 | const product = new Product({
62 | name: 'Sample name',
63 | price: 0,
64 | user: req.user._id,
65 | image: '/images/sample.jpg',
66 | brand: 'Sample brand',
67 | category: 'Sample category',
68 | countInStock: 0,
69 | numReviews: 0,
70 | description: 'Sample description',
71 | })
72 |
73 | const createdProduct = await product.save()
74 | res.status(201).json(createdProduct)
75 | })
76 |
77 | // @desc Update a product
78 | // @route PUT /api/products/:id
79 | // @access Private/Admin
80 | const updateProduct = asyncHandler(async (req, res) => {
81 | const {
82 | name,
83 | price,
84 | description,
85 | image,
86 | brand,
87 | category,
88 | countInStock,
89 | } = req.body
90 |
91 | const product = await Product.findById(req.params.id)
92 |
93 | if (product) {
94 | product.name = name
95 | product.price = price
96 | product.description = description
97 | product.image = image
98 | product.brand = brand
99 | product.category = category
100 | product.countInStock = countInStock
101 |
102 | const updatedProduct = await product.save()
103 | res.json(updatedProduct)
104 | } else {
105 | res.status(404)
106 | throw new Error('Product not found')
107 | }
108 | })
109 |
110 | // @desc Create new review
111 | // @route POST /api/products/:id/reviews
112 | // @access Private
113 | const createProductReview = asyncHandler(async (req, res) => {
114 | const { rating, comment } = req.body
115 |
116 | const product = await Product.findById(req.params.id)
117 |
118 | if (product) {
119 | const alreadyReviewed = product.reviews.find(
120 | (r) => r.user.toString() === req.user._id.toString()
121 | )
122 |
123 | if (alreadyReviewed) {
124 | res.status(400)
125 | throw new Error('Product already reviewed')
126 | }
127 |
128 | const review = {
129 | name: req.user.name,
130 | rating: Number(rating),
131 | comment,
132 | user: req.user._id,
133 | }
134 |
135 | product.reviews.push(review)
136 |
137 | product.numReviews = product.reviews.length
138 |
139 | product.rating =
140 | product.reviews.reduce((acc, item) => item.rating + acc, 0) /
141 | product.reviews.length
142 |
143 | await product.save()
144 | res.status(201).json({ message: 'Review added' })
145 | } else {
146 | res.status(404)
147 | throw new Error('Product not found')
148 | }
149 | })
150 |
151 | // @desc Get top rated products
152 | // @route GET /api/products/top
153 | // @access Public
154 | const getTopProducts = asyncHandler(async (req, res) => {
155 | const products = await Product.find({}).sort({ rating: -1 }).limit(3)
156 |
157 | res.json(products)
158 | })
159 |
160 | export {
161 | getProducts,
162 | getProductById,
163 | deleteProduct,
164 | createProduct,
165 | updateProduct,
166 | createProductReview,
167 | getTopProducts,
168 | }
169 |
--------------------------------------------------------------------------------
/backend/controllers/userController.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from 'express-async-handler'
2 | import generateToken from '../utils/generateToken.js'
3 | import User from '../models/userModel.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 |
11 | const user = await User.findOne({ email })
12 |
13 | if (user && (await user.matchPassword(password))) {
14 | res.json({
15 | _id: user._id,
16 | name: user.name,
17 | email: user.email,
18 | isAdmin: user.isAdmin,
19 | token: generateToken(user._id),
20 | })
21 | } else {
22 | res.status(401)
23 | throw new Error('Invalid email or password')
24 | }
25 | })
26 |
27 | // @desc Register a new user
28 | // @route POST /api/users
29 | // @access Public
30 | const registerUser = asyncHandler(async (req, res) => {
31 | const { name, email, password } = req.body
32 |
33 | const userExists = await User.findOne({ email })
34 |
35 | if (userExists) {
36 | res.status(400)
37 | throw new Error('User already exists')
38 | }
39 |
40 | const user = await User.create({
41 | name,
42 | email,
43 | password,
44 | })
45 |
46 | if (user) {
47 | res.status(201).json({
48 | _id: user._id,
49 | name: user.name,
50 | email: user.email,
51 | isAdmin: user.isAdmin,
52 | token: generateToken(user._id),
53 | })
54 | } else {
55 | res.status(400)
56 | throw new Error('Invalid user data')
57 | }
58 | })
59 |
60 | // @desc Get user profile
61 | // @route GET /api/users/profile
62 | // @access Private
63 | const getUserProfile = asyncHandler(async (req, res) => {
64 | const user = await User.findById(req.user._id)
65 |
66 | if (user) {
67 | res.json({
68 | _id: user._id,
69 | name: user.name,
70 | email: user.email,
71 | isAdmin: user.isAdmin,
72 | })
73 | } else {
74 | res.status(404)
75 | throw new Error('User not found')
76 | }
77 | })
78 |
79 | // @desc Update user profile
80 | // @route PUT /api/users/profile
81 | // @access Private
82 | const updateUserProfile = asyncHandler(async (req, res) => {
83 | const user = await User.findById(req.user._id)
84 |
85 | if (user) {
86 | user.name = req.body.name || user.name
87 | user.email = req.body.email || user.email
88 | if (req.body.password) {
89 | user.password = req.body.password
90 | }
91 |
92 | const updatedUser = await user.save()
93 |
94 | res.json({
95 | _id: updatedUser._id,
96 | name: updatedUser.name,
97 | email: updatedUser.email,
98 | isAdmin: updatedUser.isAdmin,
99 | token: generateToken(updatedUser._id),
100 | })
101 | } else {
102 | res.status(404)
103 | throw new Error('User not found')
104 | }
105 | })
106 |
107 | // @desc Get all users
108 | // @route GET /api/users
109 | // @access Private/Admin
110 | const getUsers = asyncHandler(async (req, res) => {
111 | const users = await User.find({})
112 | res.json(users)
113 | })
114 |
115 | // @desc Delete user
116 | // @route DELETE /api/users/:id
117 | // @access Private/Admin
118 | const deleteUser = asyncHandler(async (req, res) => {
119 | const user = await User.findById(req.params.id)
120 |
121 | if (user) {
122 | await user.remove()
123 | res.json({ message: 'User removed' })
124 | } else {
125 | res.status(404)
126 | throw new Error('User not found')
127 | }
128 | })
129 |
130 | // @desc Get user by ID
131 | // @route GET /api/users/:id
132 | // @access Private/Admin
133 | const getUserById = asyncHandler(async (req, res) => {
134 | const user = await User.findById(req.params.id).select('-password')
135 |
136 | if (user) {
137 | res.json(user)
138 | } else {
139 | res.status(404)
140 | throw new Error('User not found')
141 | }
142 | })
143 |
144 | // @desc Update user
145 | // @route PUT /api/users/:id
146 | // @access Private/Admin
147 | const updateUser = asyncHandler(async (req, res) => {
148 | const user = await User.findById(req.params.id)
149 |
150 | if (user) {
151 | user.name = req.body.name || user.name
152 | user.email = req.body.email || user.email
153 | user.isAdmin = req.body.isAdmin
154 |
155 | const updatedUser = await user.save()
156 |
157 | res.json({
158 | _id: updatedUser._id,
159 | name: updatedUser.name,
160 | email: updatedUser.email,
161 | isAdmin: updatedUser.isAdmin,
162 | })
163 | } else {
164 | res.status(404)
165 | throw new Error('User not found')
166 | }
167 | })
168 |
169 | export {
170 | authUser,
171 | registerUser,
172 | getUserProfile,
173 | updateUserProfile,
174 | getUsers,
175 | deleteUser,
176 | getUserById,
177 | updateUser,
178 | }
179 |
--------------------------------------------------------------------------------
/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/screens/PlaceOrderScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { Button, Row, Col, ListGroup, Image, Card } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import CheckoutSteps from '../components/CheckoutSteps'
7 | import { createOrder } from '../actions/orderActions'
8 | import { ORDER_CREATE_RESET } from '../constants/orderConstants'
9 | import { USER_DETAILS_RESET } from '../constants/userConstants'
10 |
11 | const PlaceOrderScreen = ({ history }) => {
12 | const dispatch = useDispatch()
13 |
14 | const cart = useSelector((state) => state.cart)
15 |
16 | if (!cart.shippingAddress.address) {
17 | history.push('/shipping')
18 | } else if (!cart.paymentMethod) {
19 | history.push('/payment')
20 | }
21 | // Calculate prices
22 | const addDecimals = (num) => {
23 | return (Math.round(num * 100) / 100).toFixed(2)
24 | }
25 |
26 | cart.itemsPrice = addDecimals(
27 | cart.cartItems.reduce((acc, item) => acc + item.price * item.qty, 0)
28 | )
29 | cart.shippingPrice = addDecimals(cart.itemsPrice > 100 ? 0 : 100)
30 | cart.taxPrice = addDecimals(Number((0.15 * cart.itemsPrice).toFixed(2)))
31 | cart.totalPrice = (
32 | Number(cart.itemsPrice) +
33 | Number(cart.shippingPrice) +
34 | Number(cart.taxPrice)
35 | ).toFixed(2)
36 |
37 | const orderCreate = useSelector((state) => state.orderCreate)
38 | const { order, success, error } = orderCreate
39 |
40 | useEffect(() => {
41 | if (success) {
42 | history.push(`/order/${order._id}`)
43 | dispatch({ type: USER_DETAILS_RESET })
44 | dispatch({ type: ORDER_CREATE_RESET })
45 | }
46 | // eslint-disable-next-line
47 | }, [history, success])
48 |
49 | const placeOrderHandler = () => {
50 | dispatch(
51 | createOrder({
52 | orderItems: cart.cartItems,
53 | shippingAddress: cart.shippingAddress,
54 | paymentMethod: cart.paymentMethod,
55 | itemsPrice: cart.itemsPrice,
56 | shippingPrice: cart.shippingPrice,
57 | taxPrice: cart.taxPrice,
58 | totalPrice: cart.totalPrice,
59 | })
60 | )
61 | }
62 |
63 | return (
64 | <>
65 |
66 |
67 |
68 |
69 |
70 | Shipping
71 |
72 | Address:
73 | {cart.shippingAddress.address}, {cart.shippingAddress.city}{' '}
74 | {cart.shippingAddress.postalCode},{' '}
75 | {cart.shippingAddress.country}
76 |
77 |
78 |
79 |
80 | Payment Method
81 | Method:
82 | {cart.paymentMethod}
83 |
84 |
85 |
86 | Order Items
87 | {cart.cartItems.length === 0 ? (
88 | Your cart is empty
89 | ) : (
90 |
91 | {cart.cartItems.map((item, index) => (
92 |
93 |
94 |
95 |
101 |
102 |
103 |
104 | {item.name}
105 |
106 |
107 |
108 | {item.qty} x ${item.price} = ${item.qty * item.price}
109 |
110 |
111 |
112 | ))}
113 |
114 | )}
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | Order Summary
123 |
124 |
125 |
126 | Items
127 | ${cart.itemsPrice}
128 |
129 |
130 |
131 |
132 | Shipping
133 | ${cart.shippingPrice}
134 |
135 |
136 |
137 |
138 | Tax
139 | ${cart.taxPrice}
140 |
141 |
142 |
143 |
144 | Total
145 | ${cart.totalPrice}
146 |
147 |
148 |
149 | {error && {error}}
150 |
151 |
152 |
160 |
161 |
162 |
163 |
164 |
165 | >
166 | )
167 | }
168 |
169 | export default PlaceOrderScreen
170 |
--------------------------------------------------------------------------------
/frontend/src/screens/ProfileScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Table, Form, Button, Row, Col } from 'react-bootstrap'
3 | import { LinkContainer } from 'react-router-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import Loader from '../components/Loader'
7 | import { getUserDetails, updateUserProfile } from '../actions/userActions'
8 | import { listMyOrders } from '../actions/orderActions'
9 | import { USER_UPDATE_PROFILE_RESET } from '../constants/userConstants'
10 |
11 | const ProfileScreen = ({ location, history }) => {
12 | const [name, setName] = useState('')
13 | const [email, setEmail] = useState('')
14 | const [password, setPassword] = useState('')
15 | const [confirmPassword, setConfirmPassword] = useState('')
16 | const [message, setMessage] = useState(null)
17 |
18 | const dispatch = useDispatch()
19 |
20 | const userDetails = useSelector((state) => state.userDetails)
21 | const { loading, error, user } = userDetails
22 |
23 | const userLogin = useSelector((state) => state.userLogin)
24 | const { userInfo } = userLogin
25 |
26 | const userUpdateProfile = useSelector((state) => state.userUpdateProfile)
27 | const { success } = userUpdateProfile
28 |
29 | const orderListMy = useSelector((state) => state.orderListMy)
30 | const { loading: loadingOrders, error: errorOrders, orders } = orderListMy
31 |
32 | useEffect(() => {
33 | if (!userInfo) {
34 | history.push('/login')
35 | } else {
36 | if (!user || !user.name || success) {
37 | dispatch({ type: USER_UPDATE_PROFILE_RESET })
38 | dispatch(getUserDetails('profile'))
39 | dispatch(listMyOrders())
40 | } else {
41 | setName(user.name)
42 | setEmail(user.email)
43 | }
44 | }
45 | }, [dispatch, history, userInfo, user, success])
46 |
47 | const submitHandler = (e) => {
48 | e.preventDefault()
49 | if (password !== confirmPassword) {
50 | setMessage('Passwords do not match')
51 | } else {
52 | dispatch(updateUserProfile({ id: user._id, name, email, password }))
53 | }
54 | }
55 |
56 | return (
57 |
58 |
59 | User Profile
60 | {message && {message}}
61 | {}
62 | {success && Profile Updated}
63 | {loading ? (
64 |
65 | ) : error ? (
66 | {error}
67 | ) : (
68 |
70 | Name
71 | setName(e.target.value)}
76 | >
77 |
78 |
79 |
80 | Email Address
81 | setEmail(e.target.value)}
86 | >
87 |
88 |
89 |
90 | Password
91 | setPassword(e.target.value)}
96 | >
97 |
98 |
99 |
100 | Confirm Password
101 | setConfirmPassword(e.target.value)}
106 | >
107 |
108 |
109 |
112 |
113 | )}
114 |
115 |
116 | My Orders
117 | {loadingOrders ? (
118 |
119 | ) : errorOrders ? (
120 | {errorOrders}
121 | ) : (
122 |
123 |
124 |
125 | | ID |
126 | DATE |
127 | TOTAL |
128 | PAID |
129 | DELIVERED |
130 | |
131 |
132 |
133 |
134 | {orders.map((order) => (
135 |
136 | | {order._id} |
137 | {order.createdAt.substring(0, 10)} |
138 | {order.totalPrice} |
139 |
140 | {order.isPaid ? (
141 | order.paidAt.substring(0, 10)
142 | ) : (
143 |
144 | )}
145 | |
146 |
147 | {order.isDelivered ? (
148 | order.deliveredAt.substring(0, 10)
149 | ) : (
150 |
151 | )}
152 | |
153 |
154 |
155 |
158 |
159 | |
160 |
161 | ))}
162 |
163 |
164 | )}
165 |
166 |
167 | )
168 | }
169 |
170 | export default ProfileScreen
171 |
--------------------------------------------------------------------------------
/frontend/src/actions/productActions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {
3 | PRODUCT_LIST_REQUEST,
4 | PRODUCT_LIST_SUCCESS,
5 | PRODUCT_LIST_FAIL,
6 | PRODUCT_DETAILS_REQUEST,
7 | PRODUCT_DETAILS_SUCCESS,
8 | PRODUCT_DETAILS_FAIL,
9 | PRODUCT_DELETE_SUCCESS,
10 | PRODUCT_DELETE_REQUEST,
11 | PRODUCT_DELETE_FAIL,
12 | PRODUCT_CREATE_REQUEST,
13 | PRODUCT_CREATE_SUCCESS,
14 | PRODUCT_CREATE_FAIL,
15 | PRODUCT_UPDATE_REQUEST,
16 | PRODUCT_UPDATE_SUCCESS,
17 | PRODUCT_UPDATE_FAIL,
18 | PRODUCT_CREATE_REVIEW_REQUEST,
19 | PRODUCT_CREATE_REVIEW_SUCCESS,
20 | PRODUCT_CREATE_REVIEW_FAIL,
21 | PRODUCT_TOP_REQUEST,
22 | PRODUCT_TOP_SUCCESS,
23 | PRODUCT_TOP_FAIL,
24 | } from '../constants/productConstants'
25 | import { logout } from './userActions'
26 |
27 | export const listProducts = (keyword = '', pageNumber = '') => async (
28 | dispatch
29 | ) => {
30 | try {
31 | dispatch({ type: PRODUCT_LIST_REQUEST })
32 |
33 | const { data } = await axios.get(
34 | `/api/products?keyword=${keyword}&pageNumber=${pageNumber}`
35 | )
36 |
37 | dispatch({
38 | type: PRODUCT_LIST_SUCCESS,
39 | payload: data,
40 | })
41 | } catch (error) {
42 | dispatch({
43 | type: PRODUCT_LIST_FAIL,
44 | payload:
45 | error.response && error.response.data.message
46 | ? error.response.data.message
47 | : error.message,
48 | })
49 | }
50 | }
51 |
52 | export const listProductDetails = (id) => async (dispatch) => {
53 | try {
54 | dispatch({ type: PRODUCT_DETAILS_REQUEST })
55 |
56 | const { data } = await axios.get(`/api/products/${id}`)
57 |
58 | dispatch({
59 | type: PRODUCT_DETAILS_SUCCESS,
60 | payload: data,
61 | })
62 | } catch (error) {
63 | dispatch({
64 | type: PRODUCT_DETAILS_FAIL,
65 | payload:
66 | error.response && error.response.data.message
67 | ? error.response.data.message
68 | : error.message,
69 | })
70 | }
71 | }
72 |
73 | export const deleteProduct = (id) => async (dispatch, getState) => {
74 | try {
75 | dispatch({
76 | type: PRODUCT_DELETE_REQUEST,
77 | })
78 |
79 | const {
80 | userLogin: { userInfo },
81 | } = getState()
82 |
83 | const config = {
84 | headers: {
85 | Authorization: `Bearer ${userInfo.token}`,
86 | },
87 | }
88 |
89 | await axios.delete(`/api/products/${id}`, config)
90 |
91 | dispatch({
92 | type: PRODUCT_DELETE_SUCCESS,
93 | })
94 | } catch (error) {
95 | const message =
96 | error.response && error.response.data.message
97 | ? error.response.data.message
98 | : error.message
99 | if (message === 'Not authorized, token failed') {
100 | dispatch(logout())
101 | }
102 | dispatch({
103 | type: PRODUCT_DELETE_FAIL,
104 | payload: message,
105 | })
106 | }
107 | }
108 |
109 | export const createProduct = () => async (dispatch, getState) => {
110 | try {
111 | dispatch({
112 | type: PRODUCT_CREATE_REQUEST,
113 | })
114 |
115 | const {
116 | userLogin: { userInfo },
117 | } = getState()
118 |
119 | const config = {
120 | headers: {
121 | Authorization: `Bearer ${userInfo.token}`,
122 | },
123 | }
124 |
125 | const { data } = await axios.post(`/api/products`, {}, config)
126 |
127 | dispatch({
128 | type: PRODUCT_CREATE_SUCCESS,
129 | payload: data,
130 | })
131 | } catch (error) {
132 | const message =
133 | error.response && error.response.data.message
134 | ? error.response.data.message
135 | : error.message
136 | if (message === 'Not authorized, token failed') {
137 | dispatch(logout())
138 | }
139 | dispatch({
140 | type: PRODUCT_CREATE_FAIL,
141 | payload: message,
142 | })
143 | }
144 | }
145 |
146 | export const updateProduct = (product) => async (dispatch, getState) => {
147 | try {
148 | dispatch({
149 | type: PRODUCT_UPDATE_REQUEST,
150 | })
151 |
152 | const {
153 | userLogin: { userInfo },
154 | } = getState()
155 |
156 | const config = {
157 | headers: {
158 | 'Content-Type': 'application/json',
159 | Authorization: `Bearer ${userInfo.token}`,
160 | },
161 | }
162 |
163 | const { data } = await axios.put(
164 | `/api/products/${product._id}`,
165 | product,
166 | config
167 | )
168 |
169 | dispatch({
170 | type: PRODUCT_UPDATE_SUCCESS,
171 | payload: data,
172 | })
173 | dispatch({ type: PRODUCT_DETAILS_SUCCESS, payload: data })
174 | } catch (error) {
175 | const message =
176 | error.response && error.response.data.message
177 | ? error.response.data.message
178 | : error.message
179 | if (message === 'Not authorized, token failed') {
180 | dispatch(logout())
181 | }
182 | dispatch({
183 | type: PRODUCT_UPDATE_FAIL,
184 | payload: message,
185 | })
186 | }
187 | }
188 |
189 | export const createProductReview = (productId, review) => async (
190 | dispatch,
191 | getState
192 | ) => {
193 | try {
194 | dispatch({
195 | type: PRODUCT_CREATE_REVIEW_REQUEST,
196 | })
197 |
198 | const {
199 | userLogin: { userInfo },
200 | } = getState()
201 |
202 | const config = {
203 | headers: {
204 | 'Content-Type': 'application/json',
205 | Authorization: `Bearer ${userInfo.token}`,
206 | },
207 | }
208 |
209 | await axios.post(`/api/products/${productId}/reviews`, review, config)
210 |
211 | dispatch({
212 | type: PRODUCT_CREATE_REVIEW_SUCCESS,
213 | })
214 | } catch (error) {
215 | const message =
216 | error.response && error.response.data.message
217 | ? error.response.data.message
218 | : error.message
219 | if (message === 'Not authorized, token failed') {
220 | dispatch(logout())
221 | }
222 | dispatch({
223 | type: PRODUCT_CREATE_REVIEW_FAIL,
224 | payload: message,
225 | })
226 | }
227 | }
228 |
229 | export const listTopProducts = () => async (dispatch) => {
230 | try {
231 | dispatch({ type: PRODUCT_TOP_REQUEST })
232 |
233 | const { data } = await axios.get(`/api/products/top`)
234 |
235 | dispatch({
236 | type: PRODUCT_TOP_SUCCESS,
237 | payload: data,
238 | })
239 | } catch (error) {
240 | dispatch({
241 | type: PRODUCT_TOP_FAIL,
242 | payload:
243 | error.response && error.response.data.message
244 | ? error.response.data.message
245 | : error.message,
246 | })
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/frontend/src/actions/orderActions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { CART_CLEAR_ITEMS } from '../constants/cartConstants'
3 | import {
4 | ORDER_CREATE_REQUEST,
5 | ORDER_CREATE_SUCCESS,
6 | ORDER_CREATE_FAIL,
7 | ORDER_DETAILS_FAIL,
8 | ORDER_DETAILS_SUCCESS,
9 | ORDER_DETAILS_REQUEST,
10 | ORDER_PAY_FAIL,
11 | ORDER_PAY_SUCCESS,
12 | ORDER_PAY_REQUEST,
13 | ORDER_LIST_MY_REQUEST,
14 | ORDER_LIST_MY_SUCCESS,
15 | ORDER_LIST_MY_FAIL,
16 | ORDER_LIST_FAIL,
17 | ORDER_LIST_SUCCESS,
18 | ORDER_LIST_REQUEST,
19 | ORDER_DELIVER_FAIL,
20 | ORDER_DELIVER_SUCCESS,
21 | ORDER_DELIVER_REQUEST,
22 | } from '../constants/orderConstants'
23 | import { logout } from './userActions'
24 |
25 | export const createOrder = (order) => async (dispatch, getState) => {
26 | try {
27 | dispatch({
28 | type: ORDER_CREATE_REQUEST,
29 | })
30 |
31 | const {
32 | userLogin: { userInfo },
33 | } = getState()
34 |
35 | const config = {
36 | headers: {
37 | 'Content-Type': 'application/json',
38 | Authorization: `Bearer ${userInfo.token}`,
39 | },
40 | }
41 |
42 | const { data } = await axios.post(`/api/orders`, order, config)
43 |
44 | dispatch({
45 | type: ORDER_CREATE_SUCCESS,
46 | payload: data,
47 | })
48 | dispatch({
49 | type: CART_CLEAR_ITEMS,
50 | payload: data,
51 | })
52 | localStorage.removeItem('cartItems')
53 | } catch (error) {
54 | const message =
55 | error.response && error.response.data.message
56 | ? error.response.data.message
57 | : error.message
58 | if (message === 'Not authorized, token failed') {
59 | dispatch(logout())
60 | }
61 | dispatch({
62 | type: ORDER_CREATE_FAIL,
63 | payload: message,
64 | })
65 | }
66 | }
67 |
68 | export const getOrderDetails = (id) => async (dispatch, getState) => {
69 | try {
70 | dispatch({
71 | type: ORDER_DETAILS_REQUEST,
72 | })
73 |
74 | const {
75 | userLogin: { userInfo },
76 | } = getState()
77 |
78 | const config = {
79 | headers: {
80 | Authorization: `Bearer ${userInfo.token}`,
81 | },
82 | }
83 |
84 | const { data } = await axios.get(`/api/orders/${id}`, config)
85 |
86 | dispatch({
87 | type: ORDER_DETAILS_SUCCESS,
88 | payload: data,
89 | })
90 | } catch (error) {
91 | const message =
92 | error.response && error.response.data.message
93 | ? error.response.data.message
94 | : error.message
95 | if (message === 'Not authorized, token failed') {
96 | dispatch(logout())
97 | }
98 | dispatch({
99 | type: ORDER_DETAILS_FAIL,
100 | payload: message,
101 | })
102 | }
103 | }
104 |
105 | export const payOrder = (orderId, paymentResult) => async (
106 | dispatch,
107 | getState
108 | ) => {
109 | try {
110 | dispatch({
111 | type: ORDER_PAY_REQUEST,
112 | })
113 |
114 | const {
115 | userLogin: { userInfo },
116 | } = getState()
117 |
118 | const config = {
119 | headers: {
120 | 'Content-Type': 'application/json',
121 | Authorization: `Bearer ${userInfo.token}`,
122 | },
123 | }
124 |
125 | const { data } = await axios.put(
126 | `/api/orders/${orderId}/pay`,
127 | paymentResult,
128 | config
129 | )
130 |
131 | dispatch({
132 | type: ORDER_PAY_SUCCESS,
133 | payload: data,
134 | })
135 | } catch (error) {
136 | const message =
137 | error.response && error.response.data.message
138 | ? error.response.data.message
139 | : error.message
140 | if (message === 'Not authorized, token failed') {
141 | dispatch(logout())
142 | }
143 | dispatch({
144 | type: ORDER_PAY_FAIL,
145 | payload: message,
146 | })
147 | }
148 | }
149 |
150 | export const deliverOrder = (order) => async (dispatch, getState) => {
151 | try {
152 | dispatch({
153 | type: ORDER_DELIVER_REQUEST,
154 | })
155 |
156 | const {
157 | userLogin: { userInfo },
158 | } = getState()
159 |
160 | const config = {
161 | headers: {
162 | Authorization: `Bearer ${userInfo.token}`,
163 | },
164 | }
165 |
166 | const { data } = await axios.put(
167 | `/api/orders/${order._id}/deliver`,
168 | {},
169 | config
170 | )
171 |
172 | dispatch({
173 | type: ORDER_DELIVER_SUCCESS,
174 | payload: data,
175 | })
176 | } catch (error) {
177 | const message =
178 | error.response && error.response.data.message
179 | ? error.response.data.message
180 | : error.message
181 | if (message === 'Not authorized, token failed') {
182 | dispatch(logout())
183 | }
184 | dispatch({
185 | type: ORDER_DELIVER_FAIL,
186 | payload: message,
187 | })
188 | }
189 | }
190 |
191 | export const listMyOrders = () => async (dispatch, getState) => {
192 | try {
193 | dispatch({
194 | type: ORDER_LIST_MY_REQUEST,
195 | })
196 |
197 | const {
198 | userLogin: { userInfo },
199 | } = getState()
200 |
201 | const config = {
202 | headers: {
203 | Authorization: `Bearer ${userInfo.token}`,
204 | },
205 | }
206 |
207 | const { data } = await axios.get(`/api/orders/myorders`, config)
208 |
209 | dispatch({
210 | type: ORDER_LIST_MY_SUCCESS,
211 | payload: data,
212 | })
213 | } catch (error) {
214 | const message =
215 | error.response && error.response.data.message
216 | ? error.response.data.message
217 | : error.message
218 | if (message === 'Not authorized, token failed') {
219 | dispatch(logout())
220 | }
221 | dispatch({
222 | type: ORDER_LIST_MY_FAIL,
223 | payload: message,
224 | })
225 | }
226 | }
227 |
228 | export const listOrders = () => async (dispatch, getState) => {
229 | try {
230 | dispatch({
231 | type: ORDER_LIST_REQUEST,
232 | })
233 |
234 | const {
235 | userLogin: { userInfo },
236 | } = getState()
237 |
238 | const config = {
239 | headers: {
240 | Authorization: `Bearer ${userInfo.token}`,
241 | },
242 | }
243 |
244 | const { data } = await axios.get(`/api/orders`, config)
245 |
246 | dispatch({
247 | type: ORDER_LIST_SUCCESS,
248 | payload: data,
249 | })
250 | } catch (error) {
251 | const message =
252 | error.response && error.response.data.message
253 | ? error.response.data.message
254 | : error.message
255 | if (message === 'Not authorized, token failed') {
256 | dispatch(logout())
257 | }
258 | dispatch({
259 | type: ORDER_LIST_FAIL,
260 | payload: message,
261 | })
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/frontend/src/screens/ProductEditScreen.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import React, { useState, useEffect } from 'react'
3 | import { Link } from 'react-router-dom'
4 | import { Form, Button } from 'react-bootstrap'
5 | import { useDispatch, useSelector } from 'react-redux'
6 | import Message from '../components/Message'
7 | import Loader from '../components/Loader'
8 | import FormContainer from '../components/FormContainer'
9 | import { listProductDetails, updateProduct } from '../actions/productActions'
10 | import { PRODUCT_UPDATE_RESET } from '../constants/productConstants'
11 |
12 | const ProductEditScreen = ({ match, history }) => {
13 | const productId = match.params.id
14 |
15 | const [name, setName] = useState('')
16 | const [price, setPrice] = useState(0)
17 | const [image, setImage] = useState('')
18 | const [brand, setBrand] = useState('')
19 | const [category, setCategory] = useState('')
20 | const [countInStock, setCountInStock] = useState(0)
21 | const [description, setDescription] = useState('')
22 | const [uploading, setUploading] = useState(false)
23 |
24 | const dispatch = useDispatch()
25 |
26 | const productDetails = useSelector((state) => state.productDetails)
27 | const { loading, error, product } = productDetails
28 |
29 | const productUpdate = useSelector((state) => state.productUpdate)
30 | const {
31 | loading: loadingUpdate,
32 | error: errorUpdate,
33 | success: successUpdate,
34 | } = productUpdate
35 |
36 | useEffect(() => {
37 | if (successUpdate) {
38 | dispatch({ type: PRODUCT_UPDATE_RESET })
39 | history.push('/admin/productlist')
40 | } else {
41 | if (!product.name || product._id !== productId) {
42 | dispatch(listProductDetails(productId))
43 | } else {
44 | setName(product.name)
45 | setPrice(product.price)
46 | setImage(product.image)
47 | setBrand(product.brand)
48 | setCategory(product.category)
49 | setCountInStock(product.countInStock)
50 | setDescription(product.description)
51 | }
52 | }
53 | }, [dispatch, history, productId, product, successUpdate])
54 |
55 | const uploadFileHandler = async (e) => {
56 | const file = e.target.files[0]
57 | const formData = new FormData()
58 | formData.append('image', file)
59 | setUploading(true)
60 |
61 | try {
62 | const config = {
63 | headers: {
64 | 'Content-Type': 'multipart/form-data',
65 | },
66 | }
67 |
68 | const { data } = await axios.post('/api/upload', formData, config)
69 |
70 | setImage(data)
71 | setUploading(false)
72 | } catch (error) {
73 | console.error(error)
74 | setUploading(false)
75 | }
76 | }
77 |
78 | const submitHandler = (e) => {
79 | e.preventDefault()
80 | dispatch(
81 | updateProduct({
82 | _id: productId,
83 | name,
84 | price,
85 | image,
86 | brand,
87 | category,
88 | description,
89 | countInStock,
90 | })
91 | )
92 | }
93 |
94 | return (
95 | <>
96 |
97 | Go Back
98 |
99 |
100 | Edit Product
101 | {loadingUpdate && }
102 | {errorUpdate && {errorUpdate}}
103 | {loading ? (
104 |
105 | ) : error ? (
106 | {error}
107 | ) : (
108 |
110 | Name
111 | setName(e.target.value)}
116 | >
117 |
118 |
119 |
120 | Price
121 | setPrice(e.target.value)}
126 | >
127 |
128 |
129 |
130 | Image
131 | setImage(e.target.value)}
136 | >
137 |
143 | {uploading && }
144 |
145 |
146 |
147 | Brand
148 | setBrand(e.target.value)}
153 | >
154 |
155 |
156 |
157 | Count In Stock
158 | setCountInStock(e.target.value)}
163 | >
164 |
165 |
166 |
167 | Category
168 | setCategory(e.target.value)}
173 | >
174 |
175 |
176 |
177 | Description
178 | setDescription(e.target.value)}
183 | >
184 |
185 |
186 |
189 |
190 | )}
191 |
192 | >
193 | )
194 | }
195 |
196 | export default ProductEditScreen
197 |
--------------------------------------------------------------------------------
/frontend/src/actions/userActions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {
3 | USER_DETAILS_FAIL,
4 | USER_DETAILS_REQUEST,
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 | USER_DETAILS_RESET,
17 | USER_LIST_FAIL,
18 | USER_LIST_SUCCESS,
19 | USER_LIST_REQUEST,
20 | USER_LIST_RESET,
21 | USER_DELETE_REQUEST,
22 | USER_DELETE_SUCCESS,
23 | USER_DELETE_FAIL,
24 | USER_UPDATE_FAIL,
25 | USER_UPDATE_SUCCESS,
26 | USER_UPDATE_REQUEST,
27 | } from '../constants/userConstants'
28 | import { ORDER_LIST_MY_RESET } from '../constants/orderConstants'
29 |
30 | export const login = (email, password) => async (dispatch) => {
31 | try {
32 | dispatch({
33 | type: USER_LOGIN_REQUEST,
34 | })
35 |
36 | const config = {
37 | headers: {
38 | 'Content-Type': 'application/json',
39 | },
40 | }
41 |
42 | const { data } = await axios.post(
43 | '/api/users/login',
44 | { email, password },
45 | config
46 | )
47 |
48 | dispatch({
49 | type: USER_LOGIN_SUCCESS,
50 | payload: data,
51 | })
52 |
53 | localStorage.setItem('userInfo', JSON.stringify(data))
54 | } catch (error) {
55 | dispatch({
56 | type: USER_LOGIN_FAIL,
57 | payload:
58 | error.response && error.response.data.message
59 | ? error.response.data.message
60 | : error.message,
61 | })
62 | }
63 | }
64 |
65 | export const logout = () => (dispatch) => {
66 | localStorage.removeItem('userInfo')
67 | localStorage.removeItem('cartItems')
68 | localStorage.removeItem('shippingAddress')
69 | localStorage.removeItem('paymentMethod')
70 | dispatch({ type: USER_LOGOUT })
71 | dispatch({ type: USER_DETAILS_RESET })
72 | dispatch({ type: ORDER_LIST_MY_RESET })
73 | dispatch({ type: USER_LIST_RESET })
74 | document.location.href = '/login'
75 | }
76 |
77 | export const register = (name, email, password) => async (dispatch) => {
78 | try {
79 | dispatch({
80 | type: USER_REGISTER_REQUEST,
81 | })
82 |
83 | const config = {
84 | headers: {
85 | 'Content-Type': 'application/json',
86 | },
87 | }
88 |
89 | const { data } = await axios.post(
90 | '/api/users',
91 | { name, email, password },
92 | config
93 | )
94 |
95 | dispatch({
96 | type: USER_REGISTER_SUCCESS,
97 | payload: data,
98 | })
99 |
100 | dispatch({
101 | type: USER_LOGIN_SUCCESS,
102 | payload: data,
103 | })
104 |
105 | localStorage.setItem('userInfo', JSON.stringify(data))
106 | } catch (error) {
107 | dispatch({
108 | type: USER_REGISTER_FAIL,
109 | payload:
110 | error.response && error.response.data.message
111 | ? error.response.data.message
112 | : error.message,
113 | })
114 | }
115 | }
116 |
117 | export const getUserDetails = (id) => async (dispatch, getState) => {
118 | try {
119 | dispatch({
120 | type: USER_DETAILS_REQUEST,
121 | })
122 |
123 | const {
124 | userLogin: { userInfo },
125 | } = getState()
126 |
127 | const config = {
128 | headers: {
129 | Authorization: `Bearer ${userInfo.token}`,
130 | },
131 | }
132 |
133 | const { data } = await axios.get(`/api/users/${id}`, config)
134 |
135 | dispatch({
136 | type: USER_DETAILS_SUCCESS,
137 | payload: data,
138 | })
139 | } catch (error) {
140 | const message =
141 | error.response && error.response.data.message
142 | ? error.response.data.message
143 | : error.message
144 | if (message === 'Not authorized, token failed') {
145 | dispatch(logout())
146 | }
147 | dispatch({
148 | type: USER_DETAILS_FAIL,
149 | payload: message,
150 | })
151 | }
152 | }
153 |
154 | export const updateUserProfile = (user) => async (dispatch, getState) => {
155 | try {
156 | dispatch({
157 | type: USER_UPDATE_PROFILE_REQUEST,
158 | })
159 |
160 | const {
161 | userLogin: { userInfo },
162 | } = getState()
163 |
164 | const config = {
165 | headers: {
166 | 'Content-Type': 'application/json',
167 | Authorization: `Bearer ${userInfo.token}`,
168 | },
169 | }
170 |
171 | const { data } = await axios.put(`/api/users/profile`, user, config)
172 |
173 | dispatch({
174 | type: USER_UPDATE_PROFILE_SUCCESS,
175 | payload: data,
176 | })
177 | dispatch({
178 | type: USER_LOGIN_SUCCESS,
179 | payload: data,
180 | })
181 | localStorage.setItem('userInfo', JSON.stringify(data))
182 | } catch (error) {
183 | const message =
184 | error.response && error.response.data.message
185 | ? error.response.data.message
186 | : error.message
187 | if (message === 'Not authorized, token failed') {
188 | dispatch(logout())
189 | }
190 | dispatch({
191 | type: USER_UPDATE_PROFILE_FAIL,
192 | payload: message,
193 | })
194 | }
195 | }
196 |
197 | export const listUsers = () => async (dispatch, getState) => {
198 | try {
199 | dispatch({
200 | type: USER_LIST_REQUEST,
201 | })
202 |
203 | const {
204 | userLogin: { userInfo },
205 | } = getState()
206 |
207 | const config = {
208 | headers: {
209 | Authorization: `Bearer ${userInfo.token}`,
210 | },
211 | }
212 |
213 | const { data } = await axios.get(`/api/users`, config)
214 |
215 | dispatch({
216 | type: USER_LIST_SUCCESS,
217 | payload: data,
218 | })
219 | } catch (error) {
220 | const message =
221 | error.response && error.response.data.message
222 | ? error.response.data.message
223 | : error.message
224 | if (message === 'Not authorized, token failed') {
225 | dispatch(logout())
226 | }
227 | dispatch({
228 | type: USER_LIST_FAIL,
229 | payload: message,
230 | })
231 | }
232 | }
233 |
234 | export const deleteUser = (id) => async (dispatch, getState) => {
235 | try {
236 | dispatch({
237 | type: USER_DELETE_REQUEST,
238 | })
239 |
240 | const {
241 | userLogin: { userInfo },
242 | } = getState()
243 |
244 | const config = {
245 | headers: {
246 | Authorization: `Bearer ${userInfo.token}`,
247 | },
248 | }
249 |
250 | await axios.delete(`/api/users/${id}`, config)
251 |
252 | dispatch({ type: USER_DELETE_SUCCESS })
253 | } catch (error) {
254 | const message =
255 | error.response && error.response.data.message
256 | ? error.response.data.message
257 | : error.message
258 | if (message === 'Not authorized, token failed') {
259 | dispatch(logout())
260 | }
261 | dispatch({
262 | type: USER_DELETE_FAIL,
263 | payload: message,
264 | })
265 | }
266 | }
267 |
268 | export const updateUser = (user) => async (dispatch, getState) => {
269 | try {
270 | dispatch({
271 | type: USER_UPDATE_REQUEST,
272 | })
273 |
274 | const {
275 | userLogin: { userInfo },
276 | } = getState()
277 |
278 | const config = {
279 | headers: {
280 | 'Content-Type': 'application/json',
281 | Authorization: `Bearer ${userInfo.token}`,
282 | },
283 | }
284 |
285 | const { data } = await axios.put(`/api/users/${user._id}`, user, config)
286 |
287 | dispatch({ type: USER_UPDATE_SUCCESS })
288 |
289 | dispatch({ type: USER_DETAILS_SUCCESS, payload: data })
290 |
291 | dispatch({ type: USER_DETAILS_RESET })
292 | } catch (error) {
293 | const message =
294 | error.response && error.response.data.message
295 | ? error.response.data.message
296 | : error.message
297 | if (message === 'Not authorized, token failed') {
298 | dispatch(logout())
299 | }
300 | dispatch({
301 | type: USER_UPDATE_FAIL,
302 | payload: message,
303 | })
304 | }
305 | }
306 |
--------------------------------------------------------------------------------
/frontend/src/screens/OrderScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import axios from 'axios'
3 | import { PayPalButton } from 'react-paypal-button-v2'
4 | import { Link } from 'react-router-dom'
5 | import { Row, Col, ListGroup, Image, Card, Button } from 'react-bootstrap'
6 | import { useDispatch, useSelector } from 'react-redux'
7 | import Message from '../components/Message'
8 | import Loader from '../components/Loader'
9 | import {
10 | getOrderDetails,
11 | payOrder,
12 | deliverOrder,
13 | } from '../actions/orderActions'
14 | import {
15 | ORDER_PAY_RESET,
16 | ORDER_DELIVER_RESET,
17 | } from '../constants/orderConstants'
18 |
19 | const OrderScreen = ({ match, history }) => {
20 | const orderId = match.params.id
21 |
22 | const [sdkReady, setSdkReady] = useState(false)
23 |
24 | const dispatch = useDispatch()
25 |
26 | const orderDetails = useSelector((state) => state.orderDetails)
27 | const { order, loading, error } = orderDetails
28 |
29 | const orderPay = useSelector((state) => state.orderPay)
30 | const { loading: loadingPay, success: successPay } = orderPay
31 |
32 | const orderDeliver = useSelector((state) => state.orderDeliver)
33 | const { loading: loadingDeliver, success: successDeliver } = orderDeliver
34 |
35 | const userLogin = useSelector((state) => state.userLogin)
36 | const { userInfo } = userLogin
37 |
38 | if (!loading) {
39 | // Calculate prices
40 | const addDecimals = (num) => {
41 | return (Math.round(num * 100) / 100).toFixed(2)
42 | }
43 |
44 | order.itemsPrice = addDecimals(
45 | order.orderItems.reduce((acc, item) => acc + item.price * item.qty, 0)
46 | )
47 | }
48 |
49 | useEffect(() => {
50 | if (!userInfo) {
51 | history.push('/login')
52 | }
53 |
54 | const addPayPalScript = async () => {
55 | const { data: clientId } = await axios.get('/api/config/paypal')
56 | const script = document.createElement('script')
57 | script.type = 'text/javascript'
58 | script.src = `https://www.paypal.com/sdk/js?client-id=${clientId}`
59 | script.async = true
60 | script.onload = () => {
61 | setSdkReady(true)
62 | }
63 | document.body.appendChild(script)
64 | }
65 |
66 | if (!order || successPay || successDeliver || order._id !== orderId) {
67 | dispatch({ type: ORDER_PAY_RESET })
68 | dispatch({ type: ORDER_DELIVER_RESET })
69 | dispatch(getOrderDetails(orderId))
70 | } else if (!order.isPaid) {
71 | if (!window.paypal) {
72 | addPayPalScript()
73 | } else {
74 | setSdkReady(true)
75 | }
76 | }
77 | }, [dispatch, orderId, successPay, successDeliver, order])
78 |
79 | const successPaymentHandler = (paymentResult) => {
80 | console.log(paymentResult)
81 | dispatch(payOrder(orderId, paymentResult))
82 | }
83 |
84 | const deliverHandler = () => {
85 | dispatch(deliverOrder(order))
86 | }
87 |
88 | return loading ? (
89 |
90 | ) : error ? (
91 | {error}
92 | ) : (
93 | <>
94 | Order {order._id}
95 |
96 |
97 |
98 |
99 | Shipping
100 |
101 | Name: {order.user.name}
102 |
103 |
104 | Email: {' '}
105 | {order.user.email}
106 |
107 |
108 | Address:
109 | {order.shippingAddress.address}, {order.shippingAddress.city}{' '}
110 | {order.shippingAddress.postalCode},{' '}
111 | {order.shippingAddress.country}
112 |
113 | {order.isDelivered ? (
114 |
115 | Delivered on {order.deliveredAt}
116 |
117 | ) : (
118 | Not Delivered
119 | )}
120 |
121 |
122 |
123 | Payment Method
124 |
125 | Method:
126 | {order.paymentMethod}
127 |
128 | {order.isPaid ? (
129 | Paid on {order.paidAt}
130 | ) : (
131 | Not Paid
132 | )}
133 |
134 |
135 |
136 | Order Items
137 | {order.orderItems.length === 0 ? (
138 | Order is empty
139 | ) : (
140 |
141 | {order.orderItems.map((item, index) => (
142 |
143 |
144 |
145 |
151 |
152 |
153 |
154 | {item.name}
155 |
156 |
157 |
158 | {item.qty} x ${item.price} = ${item.qty * item.price}
159 |
160 |
161 |
162 | ))}
163 |
164 | )}
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | Order Summary
173 |
174 |
175 |
176 | Items
177 | ${order.itemsPrice}
178 |
179 |
180 |
181 |
182 | Shipping
183 | ${order.shippingPrice}
184 |
185 |
186 |
187 |
188 | Tax
189 | ${order.taxPrice}
190 |
191 |
192 |
193 |
194 | Total
195 | ${order.totalPrice}
196 |
197 |
198 | {!order.isPaid && (
199 |
200 | {loadingPay && }
201 | {!sdkReady ? (
202 |
203 | ) : (
204 |
208 | )}
209 |
210 | )}
211 | {loadingDeliver && }
212 | {userInfo &&
213 | userInfo.isAdmin &&
214 | order.isPaid &&
215 | !order.isDelivered && (
216 |
217 |
224 |
225 | )}
226 |
227 |
228 |
229 |
230 | >
231 | )
232 | }
233 |
234 | export default OrderScreen
235 |
--------------------------------------------------------------------------------
/frontend/src/screens/ProductScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { Row, Col, Image, ListGroup, Card, Button, Form } from 'react-bootstrap'
5 | import Rating from '../components/Rating'
6 | import Message from '../components/Message'
7 | import Loader from '../components/Loader'
8 | import Meta from '../components/Meta'
9 | import {
10 | listProductDetails,
11 | createProductReview,
12 | } from '../actions/productActions'
13 | import { PRODUCT_CREATE_REVIEW_RESET } from '../constants/productConstants'
14 |
15 | const ProductScreen = ({ history, match }) => {
16 | const [qty, setQty] = useState(1)
17 | const [rating, setRating] = useState(0)
18 | const [comment, setComment] = useState('')
19 |
20 | const dispatch = useDispatch()
21 |
22 | const productDetails = useSelector((state) => state.productDetails)
23 | const { loading, error, product } = productDetails
24 |
25 | const userLogin = useSelector((state) => state.userLogin)
26 | const { userInfo } = userLogin
27 |
28 | const productReviewCreate = useSelector((state) => state.productReviewCreate)
29 | const {
30 | success: successProductReview,
31 | loading: loadingProductReview,
32 | error: errorProductReview,
33 | } = productReviewCreate
34 |
35 | useEffect(() => {
36 | if (successProductReview) {
37 | setRating(0)
38 | setComment('')
39 | }
40 | if (!product._id || product._id !== match.params.id) {
41 | dispatch(listProductDetails(match.params.id))
42 | dispatch({ type: PRODUCT_CREATE_REVIEW_RESET })
43 | }
44 | }, [dispatch, match, successProductReview])
45 |
46 | const addToCartHandler = () => {
47 | history.push(`/cart/${match.params.id}?qty=${qty}`)
48 | }
49 |
50 | const submitHandler = (e) => {
51 | e.preventDefault()
52 | dispatch(
53 | createProductReview(match.params.id, {
54 | rating,
55 | comment,
56 | })
57 | )
58 | }
59 |
60 | return (
61 | <>
62 |
63 | Go Back
64 |
65 | {loading ? (
66 |
67 | ) : error ? (
68 | {error}
69 | ) : (
70 | <>
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | {product.name}
80 |
81 |
82 |
86 |
87 | Price: ${product.price}
88 |
89 | Description: {product.description}
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | Price:
99 |
100 | ${product.price}
101 |
102 |
103 |
104 |
105 |
106 |
107 | Status:
108 |
109 | {product.countInStock > 0 ? 'In Stock' : 'Out Of Stock'}
110 |
111 |
112 |
113 |
114 | {product.countInStock > 0 && (
115 |
116 |
117 | Qty
118 |
119 | setQty(e.target.value)}
123 | >
124 | {[...Array(product.countInStock).keys()].map(
125 | (x) => (
126 |
129 | )
130 | )}
131 |
132 |
133 |
134 |
135 | )}
136 |
137 |
138 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 | Reviews
154 | {product.reviews.length === 0 && No Reviews}
155 |
156 | {product.reviews.map((review) => (
157 |
158 | {review.name}
159 |
160 | {review.createdAt.substring(0, 10)}
161 | {review.comment}
162 |
163 | ))}
164 |
165 | Write a Customer Review
166 | {successProductReview && (
167 |
168 | Review submitted successfully
169 |
170 | )}
171 | {loadingProductReview && }
172 | {errorProductReview && (
173 | {errorProductReview}
174 | )}
175 | {userInfo ? (
176 |
178 | Rating
179 | setRating(e.target.value)}
183 | >
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 | Comment
194 | setComment(e.target.value)}
199 | >
200 |
201 |
208 |
209 | ) : (
210 |
211 | Please sign in to write a review{' '}
212 |
213 | )}
214 |
215 |
216 |
217 |
218 | >
219 | )}
220 | >
221 | )
222 | }
223 |
224 | export default ProductScreen
225 |
--------------------------------------------------------------------------------