├── 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 |
18 | 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 | {product.name} 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 |
31 | 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 |
38 | 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 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {orders.map((order) => ( 48 | 49 | 50 | 51 | 52 | 53 | 60 | 67 | 74 | 75 | ))} 76 | 77 |
IDUSERDATETOTALPAIDDELIVERED
{order._id}{order.user && order.user.name}{order.createdAt.substring(0, 10)}${order.totalPrice} 54 | {order.isPaid ? ( 55 | order.paidAt.substring(0, 10) 56 | ) : ( 57 | 58 | )} 59 | 61 | {order.isDelivered ? ( 62 | order.deliveredAt.substring(0, 10) 63 | ) : ( 64 | 65 | )} 66 | 68 | 69 | 72 | 73 |
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 |
30 | 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 |