├── Procfile
├── client
├── public
│ ├── robots.txt
│ ├── images
│ │ ├── sample.jpg
│ │ ├── 0006546064.jpg
│ │ ├── 0060513039.jpg
│ │ ├── 0060557818.jpg
│ │ ├── 006056251X.jpg
│ │ ├── 0060929871.jpg
│ │ ├── 0060931728.jpg
│ │ ├── 0061125237.jpg
│ │ ├── 0062024035.jpg
│ │ ├── 0062301233.jpg
│ │ ├── 0062457713.jpg
│ │ ├── 0140421998.jpg
│ │ ├── 0140424385.jpg
│ │ ├── 0141345659.jpg
│ │ ├── 014241493X.jpg
│ │ ├── 0142437204.jpg
│ │ ├── 0143124544.jpg
│ │ ├── 015694877X.jpg
│ │ ├── 0307277674.jpg
│ │ ├── 0307353133.jpg
│ │ ├── 0307588378.jpg
│ │ ├── 030788743X.jpg
│ │ ├── 0312330870.jpg
│ │ ├── 0316015849.jpg
│ │ ├── 0316322407.jpg
│ │ ├── 0330258648.jpg
│ │ ├── 0345273680.jpg
│ │ ├── 0375831002.jpg
│ │ ├── 0385486804.jpg
│ │ ├── 0385737947.jpg
│ │ ├── 0393341763.jpg
│ │ ├── 0393355942.jpg
│ │ ├── 0393974995.jpg
│ │ ├── 0439023483.jpg
│ │ ├── 0446310786.jpg
│ │ ├── 0450040186.jpg
│ │ ├── 0451457994.jpg
│ │ ├── 0553213873.jpg
│ │ ├── 055357339X.jpg
│ │ ├── 0553577123.jpg
│ │ ├── 0553588486.jpg
│ │ ├── 0553803700.jpg
│ │ ├── 0590353403.jpg
│ │ ├── 059309932X.jpg
│ │ ├── 0595002021.jpg
│ │ ├── 0606264728.jpg
│ │ ├── 0618129022.jpg
│ │ ├── 0671027034.jpg
│ │ ├── 0679735771.jpg
│ │ ├── 0735211299.jpg
│ │ ├── 0743255062.jpg
│ │ ├── 0743264738.jpg
│ │ ├── 0743269519.jpg
│ │ ├── 0747591059.jpg
│ │ ├── 0751532711.jpg
│ │ ├── 075640407X.jpg
│ │ ├── 0762447699.jpg
│ │ ├── 0785815538.jpg
│ │ ├── 0804139024.jpg
│ │ ├── 080701429X.jpg
│ │ ├── 0807059099.jpg
│ │ ├── 0812505042.jpg
│ │ ├── 0812550706.jpg
│ │ ├── 1250012570.jpg
│ │ ├── 1444723448.jpg
│ │ ├── 1451648537.jpg
│ │ ├── 1577314808.jpg
│ │ ├── 1594633665.jpg
│ │ ├── 159514174X.jpg
│ │ ├── 1676097708.jpg
│ │ ├── 1786495252.jpg
│ │ ├── 1788441028.jpg
│ │ ├── 1903436575.jpg
│ │ ├── 1904808166.jpg
│ │ ├── 1932429247.jpg
│ │ ├── 1982110996.jpg
│ │ ├── B000Q67J66.jpg
│ │ ├── B075HXST4P.jpg
│ │ └── 9780451524935.jpg
│ ├── icons
│ │ ├── github-32.png
│ │ ├── open-book.png
│ │ ├── illustration.jpg
│ │ └── illustration2.jpg
│ ├── manifest.json
│ └── index.html
├── src
│ ├── pages
│ │ ├── UserList
│ │ │ ├── UserList.elements.js
│ │ │ └── index.js
│ │ ├── Cart
│ │ │ ├── Cart.elements.js
│ │ │ └── index.js
│ │ ├── Shipping
│ │ │ ├── Shipping.elements.js
│ │ │ └── index.js
│ │ ├── PlaceOrder
│ │ │ ├── PlaceOrder.elements.js
│ │ │ └── index.js
│ │ ├── Home
│ │ │ ├── Home.elements.js
│ │ │ └── index.js
│ │ ├── ProductList
│ │ │ ├── ProductList.elements.js
│ │ │ └── index.js
│ │ ├── Order
│ │ │ └── Order.elements.js
│ │ ├── OrderList
│ │ │ ├── OrderList.elements.js
│ │ │ └── index.js
│ │ ├── ProductEdit
│ │ │ └── ProductEdit.elements.js
│ │ ├── Payment
│ │ │ ├── Payment.elements.js
│ │ │ └── index.js
│ │ ├── Profile
│ │ │ ├── Profile.elements.js
│ │ │ └── index.js
│ │ ├── Login
│ │ │ ├── Login.elements.js
│ │ │ └── index.js
│ │ ├── Register
│ │ │ ├── Register.elements.js
│ │ │ └── index.js
│ │ ├── Search
│ │ │ └── index.js
│ │ ├── Genre
│ │ │ └── index.js
│ │ └── Product
│ │ │ └── Product.elements.js
│ ├── setupTests.js
│ ├── App.test.js
│ ├── components
│ │ ├── Meta
│ │ │ └── index.js
│ │ ├── Stars
│ │ │ ├── Stars.elements.js
│ │ │ └── index.js
│ │ ├── StepperNav
│ │ │ ├── Stepper.elements.js
│ │ │ └── index.js
│ │ ├── Footer
│ │ │ └── index.js
│ │ ├── HomeMain
│ │ │ ├── HomeMain.js
│ │ │ └── index.js
│ │ ├── CartItems
│ │ │ ├── CartItems.elements.js
│ │ │ └── index.js
│ │ ├── GenreSelector
│ │ │ ├── index.js
│ │ │ └── GenreSelector.elements.js
│ │ ├── Product
│ │ │ ├── index.js
│ │ │ └── Product.elements.js
│ │ ├── Header
│ │ │ ├── Header.elements.js
│ │ │ └── index.js
│ │ ├── SearchBox
│ │ │ ├── SearchBox.elements.js
│ │ │ └── index.js
│ │ ├── ProductsCarousel
│ │ │ ├── index.js
│ │ │ └── ProductCarousel.elements.js
│ │ ├── Loader
│ │ │ ├── Loader.css
│ │ │ └── Loader.js
│ │ └── QuoteGenerator
│ │ │ └── index.js
│ ├── theme.js
│ ├── index.js
│ ├── reducers
│ │ ├── cartReducers.js
│ │ ├── userReducers.js
│ │ ├── productReducers.js
│ │ └── orderReducers.js
│ ├── actions
│ │ ├── cartActions.js
│ │ ├── types.js
│ │ ├── productActions.js
│ │ ├── orderActions.js
│ │ └── userActions.js
│ ├── store.js
│ ├── App.js
│ ├── products.js
│ └── index.css
└── package.json
├── uploads
├── 1619981945511--fcb_logo_PNG25.png
├── 1619983747433--fcb_logo_PNG25.png
└── 1620761734111--chamber-of-secrets.jpg
├── server
├── utils
│ └── generateToken.js
├── middleware
│ ├── errorMiddleware.js
│ └── authMiddleware.js
├── config
│ └── db.js
├── data
│ └── users.js
├── routes
│ ├── userRoutes.js
│ ├── orderRoutes.js
│ ├── productRouter.js
│ └── uploadRoutes.js
├── models
│ ├── userModel.js
│ ├── productModel.js
│ └── orderModel.js
├── server.js
├── seeder.js
└── controllers
│ ├── orderController.js
│ ├── userController.js
│ └── productController.js
├── .gitignore
├── README.md
└── package.json
/Procfile:
--------------------------------------------------------------------------------
1 | web: node server/server.js
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client/public/images/sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/sample.jpg
--------------------------------------------------------------------------------
/client/public/icons/github-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/icons/github-32.png
--------------------------------------------------------------------------------
/client/public/icons/open-book.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/icons/open-book.png
--------------------------------------------------------------------------------
/client/public/icons/illustration.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/icons/illustration.jpg
--------------------------------------------------------------------------------
/client/public/images/0006546064.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0006546064.jpg
--------------------------------------------------------------------------------
/client/public/images/0060513039.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0060513039.jpg
--------------------------------------------------------------------------------
/client/public/images/0060557818.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0060557818.jpg
--------------------------------------------------------------------------------
/client/public/images/006056251X.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/006056251X.jpg
--------------------------------------------------------------------------------
/client/public/images/0060929871.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0060929871.jpg
--------------------------------------------------------------------------------
/client/public/images/0060931728.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0060931728.jpg
--------------------------------------------------------------------------------
/client/public/images/0061125237.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0061125237.jpg
--------------------------------------------------------------------------------
/client/public/images/0062024035.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0062024035.jpg
--------------------------------------------------------------------------------
/client/public/images/0062301233.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0062301233.jpg
--------------------------------------------------------------------------------
/client/public/images/0062457713.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0062457713.jpg
--------------------------------------------------------------------------------
/client/public/images/0140421998.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0140421998.jpg
--------------------------------------------------------------------------------
/client/public/images/0140424385.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0140424385.jpg
--------------------------------------------------------------------------------
/client/public/images/0141345659.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0141345659.jpg
--------------------------------------------------------------------------------
/client/public/images/014241493X.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/014241493X.jpg
--------------------------------------------------------------------------------
/client/public/images/0142437204.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0142437204.jpg
--------------------------------------------------------------------------------
/client/public/images/0143124544.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0143124544.jpg
--------------------------------------------------------------------------------
/client/public/images/015694877X.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/015694877X.jpg
--------------------------------------------------------------------------------
/client/public/images/0307277674.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0307277674.jpg
--------------------------------------------------------------------------------
/client/public/images/0307353133.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0307353133.jpg
--------------------------------------------------------------------------------
/client/public/images/0307588378.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0307588378.jpg
--------------------------------------------------------------------------------
/client/public/images/030788743X.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/030788743X.jpg
--------------------------------------------------------------------------------
/client/public/images/0312330870.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0312330870.jpg
--------------------------------------------------------------------------------
/client/public/images/0316015849.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0316015849.jpg
--------------------------------------------------------------------------------
/client/public/images/0316322407.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0316322407.jpg
--------------------------------------------------------------------------------
/client/public/images/0330258648.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0330258648.jpg
--------------------------------------------------------------------------------
/client/public/images/0345273680.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0345273680.jpg
--------------------------------------------------------------------------------
/client/public/images/0375831002.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0375831002.jpg
--------------------------------------------------------------------------------
/client/public/images/0385486804.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0385486804.jpg
--------------------------------------------------------------------------------
/client/public/images/0385737947.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0385737947.jpg
--------------------------------------------------------------------------------
/client/public/images/0393341763.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0393341763.jpg
--------------------------------------------------------------------------------
/client/public/images/0393355942.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0393355942.jpg
--------------------------------------------------------------------------------
/client/public/images/0393974995.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0393974995.jpg
--------------------------------------------------------------------------------
/client/public/images/0439023483.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0439023483.jpg
--------------------------------------------------------------------------------
/client/public/images/0446310786.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0446310786.jpg
--------------------------------------------------------------------------------
/client/public/images/0450040186.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0450040186.jpg
--------------------------------------------------------------------------------
/client/public/images/0451457994.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0451457994.jpg
--------------------------------------------------------------------------------
/client/public/images/0553213873.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0553213873.jpg
--------------------------------------------------------------------------------
/client/public/images/055357339X.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/055357339X.jpg
--------------------------------------------------------------------------------
/client/public/images/0553577123.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0553577123.jpg
--------------------------------------------------------------------------------
/client/public/images/0553588486.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0553588486.jpg
--------------------------------------------------------------------------------
/client/public/images/0553803700.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0553803700.jpg
--------------------------------------------------------------------------------
/client/public/images/0590353403.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0590353403.jpg
--------------------------------------------------------------------------------
/client/public/images/059309932X.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/059309932X.jpg
--------------------------------------------------------------------------------
/client/public/images/0595002021.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0595002021.jpg
--------------------------------------------------------------------------------
/client/public/images/0606264728.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0606264728.jpg
--------------------------------------------------------------------------------
/client/public/images/0618129022.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0618129022.jpg
--------------------------------------------------------------------------------
/client/public/images/0671027034.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0671027034.jpg
--------------------------------------------------------------------------------
/client/public/images/0679735771.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0679735771.jpg
--------------------------------------------------------------------------------
/client/public/images/0735211299.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0735211299.jpg
--------------------------------------------------------------------------------
/client/public/images/0743255062.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0743255062.jpg
--------------------------------------------------------------------------------
/client/public/images/0743264738.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0743264738.jpg
--------------------------------------------------------------------------------
/client/public/images/0743269519.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0743269519.jpg
--------------------------------------------------------------------------------
/client/public/images/0747591059.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0747591059.jpg
--------------------------------------------------------------------------------
/client/public/images/0751532711.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0751532711.jpg
--------------------------------------------------------------------------------
/client/public/images/075640407X.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/075640407X.jpg
--------------------------------------------------------------------------------
/client/public/images/0762447699.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0762447699.jpg
--------------------------------------------------------------------------------
/client/public/images/0785815538.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0785815538.jpg
--------------------------------------------------------------------------------
/client/public/images/0804139024.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0804139024.jpg
--------------------------------------------------------------------------------
/client/public/images/080701429X.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/080701429X.jpg
--------------------------------------------------------------------------------
/client/public/images/0807059099.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0807059099.jpg
--------------------------------------------------------------------------------
/client/public/images/0812505042.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0812505042.jpg
--------------------------------------------------------------------------------
/client/public/images/0812550706.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/0812550706.jpg
--------------------------------------------------------------------------------
/client/public/images/1250012570.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/1250012570.jpg
--------------------------------------------------------------------------------
/client/public/images/1444723448.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/1444723448.jpg
--------------------------------------------------------------------------------
/client/public/images/1451648537.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/1451648537.jpg
--------------------------------------------------------------------------------
/client/public/images/1577314808.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/1577314808.jpg
--------------------------------------------------------------------------------
/client/public/images/1594633665.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/1594633665.jpg
--------------------------------------------------------------------------------
/client/public/images/159514174X.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/159514174X.jpg
--------------------------------------------------------------------------------
/client/public/images/1676097708.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/1676097708.jpg
--------------------------------------------------------------------------------
/client/public/images/1786495252.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/1786495252.jpg
--------------------------------------------------------------------------------
/client/public/images/1788441028.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/1788441028.jpg
--------------------------------------------------------------------------------
/client/public/images/1903436575.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/1903436575.jpg
--------------------------------------------------------------------------------
/client/public/images/1904808166.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/1904808166.jpg
--------------------------------------------------------------------------------
/client/public/images/1932429247.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/1932429247.jpg
--------------------------------------------------------------------------------
/client/public/images/1982110996.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/1982110996.jpg
--------------------------------------------------------------------------------
/client/public/images/B000Q67J66.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/B000Q67J66.jpg
--------------------------------------------------------------------------------
/client/public/images/B075HXST4P.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/B075HXST4P.jpg
--------------------------------------------------------------------------------
/client/public/icons/illustration2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/icons/illustration2.jpg
--------------------------------------------------------------------------------
/client/public/images/9780451524935.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/client/public/images/9780451524935.jpg
--------------------------------------------------------------------------------
/uploads/1619981945511--fcb_logo_PNG25.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/uploads/1619981945511--fcb_logo_PNG25.png
--------------------------------------------------------------------------------
/uploads/1619983747433--fcb_logo_PNG25.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/uploads/1619983747433--fcb_logo_PNG25.png
--------------------------------------------------------------------------------
/uploads/1620761734111--chamber-of-secrets.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiragdatwani/mern-e-commerce/HEAD/uploads/1620761734111--chamber-of-secrets.jpg
--------------------------------------------------------------------------------
/server/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;
--------------------------------------------------------------------------------
/client/src/pages/UserList/UserList.elements.js:
--------------------------------------------------------------------------------
1 | import DeleteIcon from '@material-ui/icons/Delete';
2 | import styled from 'styled-components';
3 |
4 | export const Delete = styled(DeleteIcon)`
5 | & :hover{
6 | cursor: pointer;
7 | }
8 | `
--------------------------------------------------------------------------------
/client/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/client/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render( );
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/client/src/components/Meta/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Helmet } from 'react-helmet';
3 |
4 |
5 | const Meta = ({title}) => {
6 | return (
7 |
8 | {title}
9 |
10 | )
11 | }
12 |
13 | export default Meta;
14 |
--------------------------------------------------------------------------------
/client/src/theme.js:
--------------------------------------------------------------------------------
1 | import { createMuiTheme } from '@material-ui/core/styles';
2 |
3 | export const myTheme = createMuiTheme({
4 |
5 | palette: {
6 |
7 | primary: {
8 | main:'#624af9'
9 | },
10 | romance: {
11 | main: '#CD1E28'
12 | }
13 | }
14 | })
--------------------------------------------------------------------------------
/client/src/pages/Cart/Cart.elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { Button } from '@material-ui/core';
3 |
4 | export const StyledButton = styled(Button)`
5 | width: 100%;
6 | background-color: black;
7 | color: white;
8 | & :hover{
9 | color: black
10 | }
11 |
12 | `
--------------------------------------------------------------------------------
/client/src/components/Stars/Stars.elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Container = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 |
7 |
8 | > .rating-text{
9 | font-size:12px;
10 | };
11 | & .MuiRating-icon {
12 | font-size: 1.3rem;
13 | }
14 | `
--------------------------------------------------------------------------------
/client/src/pages/Shipping/Shipping.elements.js:
--------------------------------------------------------------------------------
1 | import { Container } from '@material-ui/core';
2 | import styled from 'styled-components';
3 |
4 | export const FormContainer = styled(Container)`
5 | margin-top: 30px;
6 | padding: 20px;
7 | & > h2 {
8 | margin-top: 0;
9 | margin-bottom: 0.8rem;
10 | }
11 |
12 | `
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { Provider } from 'react-redux'
6 | import store from './store'
7 |
8 |
9 | ReactDOM.render(
10 |
11 |
12 | ,
13 | document.getElementById('root')
14 | );
15 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Book Attic",
3 | "name": "Book Attic | Buy Books Online",
4 | "icons": [
5 | {
6 | "src": "/icons/open-book.png",
7 | "type": "image/png",
8 | "sizes": "512x512"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/components/Stars/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Container} from './Stars.elements';
3 | import {Rating} from '@material-ui/lab'
4 |
5 | function Stars(props) {
6 | return (
7 |
8 |
9 | {props.text}
10 |
11 | )
12 | }
13 |
14 | export default Stars
15 |
--------------------------------------------------------------------------------
/client/src/components/StepperNav/Stepper.elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { Stepper } from '@material-ui/core'
3 |
4 | export const StyledStepper = styled(Stepper)`
5 |
6 | margin-top: 8px;
7 | @media (max-width: 450px){
8 | padding: 15px 0;
9 | & h4 {
10 | font-size: 12px
11 | }
12 | }
13 |
14 | @media (max-width: 400px){
15 | margin-left: -20px;
16 | }
17 | `
--------------------------------------------------------------------------------
/client/src/pages/PlaceOrder/PlaceOrder.elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const OrderItem = styled.div`
4 | padding: 5px 0;
5 | display: flex;
6 | justify-content: space-between;
7 | align-items: center;
8 | & > img{
9 | margin-right: 10px;
10 | }
11 | `
12 |
13 | export const ShippingMessage = styled.span`
14 | font-size: 10px;
15 | `
16 |
17 | export const SummaryItem = styled.p`
18 | font-size: 20px;
19 | `
--------------------------------------------------------------------------------
/.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 | /client/build
14 | /server/data
15 | /server/seeder.js
16 |
17 | # misc
18 | .DS_Store
19 | .env
20 | .env.local
21 | .env.development.local
22 | .env.test.local
23 | .env.production.local
24 |
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
--------------------------------------------------------------------------------
/server/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}
--------------------------------------------------------------------------------
/server/config/db.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const connectDB = async () => {
4 | try {
5 | const connection = await mongoose.connect(process.env.MONGO_URI, {
6 | useUnifiedTopology: true,
7 | useNewUrlParser: true,
8 | useCreateIndex: true
9 | })
10 |
11 | console.log(`MongoDB Connected: ${connection.connection.host}`);
12 | } catch (error) {
13 | console.log(`Error: ${error.message}`);
14 | process.exit(1)
15 | }
16 | }
17 |
18 | export default connectDB;
--------------------------------------------------------------------------------
/server/data/users.js:
--------------------------------------------------------------------------------
1 | import bcrypt from 'bcryptjs';
2 |
3 | const users = [
4 | {
5 | name: 'Admin User',
6 | email: 'admin@admin.com',
7 | password: bcrypt.hashSync('123456', 10),
8 | isAdmin: true
9 | },
10 | {
11 | name: 'Chirag Datwani',
12 | email: 'chirag@mail.com',
13 | password: bcrypt.hashSync('123456', 10)
14 | },
15 | {
16 | name: 'Dimple Datwani',
17 | email: 'dimple@example.com',
18 | password: bcrypt.hashSync('123456', 10)
19 | }
20 | ]
21 |
22 | export default users;
--------------------------------------------------------------------------------
/client/src/pages/Home/Home.elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const PaginationContainer = styled.div`
4 |
5 | margin: 40px auto;
6 | & ul {
7 | justify-content: center;
8 | }
9 |
10 | @media (max-width: 450px){
11 | & .MuiPaginationItem-sizeLarge{
12 | height: 32px;
13 | min-width: 32px;
14 | padding: 0;
15 | }
16 | }
17 |
18 | `
19 |
20 | export const TopRated = styled.div`
21 | margin: 60px auto;
22 | & h1{
23 | margin-bottom: 30px;
24 | }
25 | `
--------------------------------------------------------------------------------
/server/routes/userRoutes.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | const router = express.Router();
3 | import { authUser, getUserProfile, registerUser, updateUserProfile, getUsers, deleteUser } from '../controllers/userController.js';
4 | import {isAdmin, protectRoute} from '../middleware/authMiddleware.js'
5 |
6 | router.route('/').post(registerUser).get(protectRoute, isAdmin, getUsers);
7 | router.route('/login').post(authUser);
8 | router.route('/profile').get(protectRoute , getUserProfile).put(protectRoute , updateUserProfile);
9 | router.route('/:id').delete(protectRoute, isAdmin, deleteUser)
10 |
11 |
12 | export default router;
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 | Book Attic | Buy Books Online
14 |
15 |
16 | You need to enable JavaScript to run this app.
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/server/routes/orderRoutes.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | const router = express.Router();
3 | import { isAdmin, protectRoute } from '../middleware/authMiddleware.js';
4 | import { addOrderItems, getOrderById, getUserOrders, updateOrderToPaid, getOrders, updateOrderToDelivered } from '../controllers/orderController.js'
5 |
6 | router.route('/').post(protectRoute, addOrderItems).get(protectRoute, isAdmin, getOrders);
7 | router.route('/myorders').get(protectRoute, getUserOrders);
8 | router.route('/:id').get(protectRoute, getOrderById);
9 | router.route('/:id/pay').put(protectRoute, updateOrderToPaid);
10 | router.route('/:id/deliver').put(protectRoute, isAdmin, updateOrderToDelivered);
11 |
12 |
13 | export default router;
--------------------------------------------------------------------------------
/client/src/pages/ProductList/ProductList.elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import EditIcon from '@material-ui/icons/Edit';
3 | import DeleteIcon from '@material-ui/icons/Delete';
4 |
5 |
6 | export const Delete = styled(DeleteIcon)`
7 | color: red;
8 | & :hover{
9 | cursor: pointer;
10 | }
11 | `
12 |
13 | export const Edit = styled(EditIcon)`
14 | color: teal;
15 | & :hover{
16 | cursor: pointer;
17 | }
18 | `
19 | export const ButtonContainer = styled.div`
20 | margin-top: -30px;
21 | margin-bottom: 15px;
22 | float: right;
23 | `
24 | export const PaginationContainer = styled.div`
25 |
26 | margin: 40px auto;
27 | & ul {
28 | justify-content: center;
29 | }
30 |
31 | `
--------------------------------------------------------------------------------
/client/src/pages/Order/Order.elements.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import styled from 'styled-components';
3 |
4 | export const OrderItem = styled.div`
5 | padding: 5px 0;
6 | display: flex;
7 | justify-content: space-between;
8 | align-items: center;
9 | & > img{
10 | margin-right: 10px;
11 | }
12 | `
13 |
14 | export const ShippingMessage = styled.span`
15 | font-size: 10px;
16 | `
17 |
18 | export const SummaryItem = styled.p`
19 | font-size: 20px;
20 | `
21 |
22 | export const Message = styled.div`
23 | margin-bottom: 5px;
24 | `
25 |
26 | export const StyledLink = styled(Link)`
27 | color: inherit;
28 | text-decoration: none;
29 | & button{
30 | margin: 10px 0 20px 17px;
31 | }
32 |
33 | `
--------------------------------------------------------------------------------
/client/src/pages/OrderList/OrderList.elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import EditIcon from '@material-ui/icons/Edit';
3 | import DeleteIcon from '@material-ui/icons/Delete';
4 | import { TableRow } from '@material-ui/core';
5 |
6 |
7 | export const Delete = styled(DeleteIcon)`
8 | color: red;
9 | & :hover{
10 | cursor: pointer;
11 | }
12 | `
13 |
14 | export const Edit = styled(EditIcon)`
15 | color: teal;
16 | & :hover{
17 | cursor: pointer;
18 | }
19 | `
20 | export const ButtonContainer = styled.div`
21 | margin-top: -30px;
22 | margin-bottom: 15px;
23 | float: right;
24 | `
25 |
26 | export const StyledTableRow = styled(TableRow)`
27 | & :hover{
28 | cursor: pointer;
29 | background-color: #f5f5f5f5;
30 | }
31 | `
32 |
--------------------------------------------------------------------------------
/client/src/pages/ProductEdit/ProductEdit.elements.js:
--------------------------------------------------------------------------------
1 | import { Container } from '@material-ui/core';
2 | import styled from 'styled-components';
3 | import { Button } from '@material-ui/core';
4 | import { Link } from 'react-router-dom';
5 |
6 | export const FormContainer = styled(Container)`
7 | margin-top: 30px;
8 | padding: 20px;
9 | & > h1 {
10 | margin-top: 0;
11 | }
12 |
13 | `
14 |
15 | export const StyledButton = styled(Button)`
16 | margin-top: 25px;
17 | width: 100%;
18 | background-color: black;
19 | color: white;
20 | & :hover{
21 | color: black;
22 | font-weight: bold;
23 | }
24 |
25 | `
26 |
27 | export const StyledLink = styled(Link)`
28 | color: inherit;
29 | margin: 2rem;
30 | font-weight: 500;
31 | text-decoration: underline;
32 | `
--------------------------------------------------------------------------------
/server/routes/productRouter.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import {isAdmin, protectRoute} from '../middleware/authMiddleware.js';
3 | const router = express.Router();
4 |
5 | import { getProducts, getProductById, deleteProduct, updateProduct, createProduct, addProductReview, searchProducts, getTopProducts, getProductsByGenre } from '../controllers/productController.js'
6 |
7 | router.route('/').get(getProducts).post(protectRoute, isAdmin, createProduct);
8 |
9 | router.get('/top', getTopProducts)
10 |
11 | router.get('/genre/:genre', getProductsByGenre)
12 |
13 | router.route('/search/:keyword').get(searchProducts)
14 |
15 | router.route('/:id').get(getProductById).delete(protectRoute, isAdmin, deleteProduct).put(protectRoute, isAdmin, updateProduct);
16 |
17 | router.route('/:id/reviews').post(protectRoute, addProductReview)
18 |
19 | export default router;
--------------------------------------------------------------------------------
/client/src/pages/Payment/Payment.elements.js:
--------------------------------------------------------------------------------
1 | import { Container } from '@material-ui/core';
2 | import styled from 'styled-components';
3 | import { Button } from '@material-ui/core';
4 |
5 | export const FormContainer = styled(Container)`
6 | margin-top: 30px;
7 | padding: 20px;
8 | & > h2 {
9 | margin-top: 0;
10 | margin-bottom: 0.9rem;
11 | };
12 | & > form {
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: space-between;
16 | & button{
17 | width: 108px;
18 | margin-top: 10px;
19 | }
20 | }
21 |
22 |
23 | `
24 |
25 | export const StyledButton = styled(Button)`
26 | margin-top: 25px;
27 | width: 100%;
28 | background-color: black;
29 | color: white;
30 | & :hover{
31 | color: black;
32 | font-weight: bold;
33 | }
34 |
35 | `
--------------------------------------------------------------------------------
/client/src/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components';
3 |
4 | function Footer() {
5 | return (
6 |
14 | )
15 | }
16 |
17 | export default Footer;
18 |
19 | const Container = styled.div`
20 | & a{
21 | display: flex;
22 | align-items: center;
23 | justify-content: center;
24 | text-decoration: none;
25 | color: black;
26 | & img{
27 | width: 25px;
28 | margin-left: 10px;
29 | }
30 | }
31 | `
32 |
--------------------------------------------------------------------------------
/client/src/pages/Profile/Profile.elements.js:
--------------------------------------------------------------------------------
1 | import { Container, TableRow } from '@material-ui/core';
2 | import styled from 'styled-components';
3 | import { Button } from '@material-ui/core';
4 | import { Link } from 'react-router-dom';
5 |
6 | export const FormContainer = styled(Container)`
7 | margin-top: 30px;
8 | padding: 20px;
9 | & > h1 {
10 | margin-top: 0;
11 | }
12 |
13 | `
14 |
15 | export const StyledButton = styled(Button)`
16 | margin-top: 25px;
17 | width: 100%;
18 | background-color: black;
19 | color: white;
20 | & :hover{
21 | color: black;
22 | font-weight: bold;
23 | }
24 |
25 | `
26 |
27 | export const StyledLink = styled(Link)`
28 | color: inherit;
29 | text-decoration: none;
30 | `
31 |
32 | export const StyledTableRow = styled(TableRow)`
33 | & :hover{
34 | cursor: pointer;
35 | }
36 | `
--------------------------------------------------------------------------------
/server/models/userModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | import bcrypt from 'bcryptjs';
3 |
4 | const UserSchema = new mongoose.Schema({
5 | name:{
6 | type: String,
7 | required: true
8 | },
9 | email:{
10 | type: String,
11 | required: true,
12 | unique: true
13 | },
14 | password:{
15 | type: String,
16 | required: true
17 | },
18 | isAdmin:{
19 | type: Boolean,
20 | required: true,
21 | default: false
22 | },
23 |
24 | }, {
25 | timestamps: true
26 | })
27 |
28 |
29 | UserSchema.pre('save', async function(next){
30 | if(!this.isModified('password')){
31 | next()
32 | }
33 |
34 | const salt = await bcrypt.genSalt(10);
35 | this.password = await bcrypt.hash(this.password, salt)
36 | })
37 | const User = mongoose.model('User', UserSchema)
38 |
39 | export default User;
--------------------------------------------------------------------------------
/client/src/pages/Login/Login.elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 |
4 | export const LoginContainer = styled.div`
5 | height: 85vh;
6 | position: relative;
7 | @media (orientation: portrait){
8 | height: 75vh;
9 | };
10 | `
11 |
12 |
13 | export const FormContainer = styled.div`
14 | margin-top: 30px;
15 | padding: 20px;
16 | width: 300px;;
17 | & > h2 {
18 | margin-top: 0;
19 | }
20 |
21 | & button{
22 | margin: 10px auto;
23 | width: 50%;
24 | }
25 |
26 | `
27 |
28 |
29 | export const ImgContainer = styled.div`
30 | position: absolute;
31 | width: 50vw;
32 | height: 100%;
33 | right: -5%;
34 | bottom: 0%;
35 | z-index: -100;
36 | @media (orientation: portrait){
37 | width: 100vw;
38 | };
39 | & img{
40 | position: absolute;
41 | bottom: 0;
42 | width: 100%;
43 | }
44 | `
--------------------------------------------------------------------------------
/client/src/pages/Register/Register.elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 |
4 |
5 | export const RegisterContainer = styled.div`
6 | height: 85vh;
7 | position: relative;
8 | @media (orientation: portrait){
9 | height: 75vh;
10 | };
11 | `
12 |
13 | export const FormContainer = styled.div`
14 | margin-top: 30px;
15 | padding: 20px;
16 | width: 300px;;
17 | & > h2 {
18 | margin-top: 0;
19 | }
20 |
21 | & button{
22 | margin: 10px auto;
23 | width: 50%;
24 | }
25 | `
26 |
27 | export const ImgContainer = styled.div`
28 | position: absolute;
29 | display: flex;
30 | justify-content: flex-end;
31 | width: 50vw;
32 | height: 100%;
33 | right: 0%;
34 | bottom: 0%;
35 | z-index: -100;
36 | @media (orientation: portrait){
37 | width: 100vw;
38 | };
39 | & img{
40 | position: absolute;
41 | bottom: 0;
42 | width: 65%;
43 | }
44 | `
--------------------------------------------------------------------------------
/client/src/components/StepperNav/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Step, StepLabel } from '@material-ui/core';
3 | import { Link } from 'react-router-dom';
4 | import { StyledStepper } from './Stepper.elements';
5 |
6 |
7 | const StepperNav = ({stepNumber}) => {
8 |
9 | const steps = ['Login', 'Shipping', 'Payment', 'Place Order']
10 | return (
11 |
12 | {steps.map( (step, index) => (
13 |
14 |
15 | {index > stepNumber?
16 | {step} :
17 |
18 | {{step} }
19 |
20 | }
21 |
22 |
23 | ))}
24 |
25 | )
26 | }
27 |
28 | export default StepperNav;
29 |
--------------------------------------------------------------------------------
/server/routes/uploadRoutes.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import multer from 'multer';
3 | import path from 'path';
4 |
5 | const router = express.Router();
6 |
7 | const storage = multer.diskStorage({
8 | destination: (req, file, cb) =>{
9 | cb(null, 'uploads/')
10 | },
11 | filename: (req, file, cb) => {
12 | cb(null, `${Date.now()}--${file.originalname}`)
13 | }
14 | });
15 |
16 |
17 | const checkFileType = (file, cb) => {
18 | const filetypes = /jpg|jpeg|png/
19 | const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
20 | const mimetype = filetypes.test(file.mimetype);
21 |
22 | if(extname & mimetype){
23 | return cb(null, cb)
24 | }else{
25 | cb('Images only!')
26 | }
27 | }
28 |
29 | const upload = multer({
30 | storage,
31 | fileFilter: (req, file, cb) => {
32 | checkFileType(file, cb)
33 | }
34 | });
35 |
36 | router.post('/', upload.single('image'),(req, res) => {
37 | res.send(`/${req.file.path}`)
38 | })
39 |
40 | export default router;
41 |
--------------------------------------------------------------------------------
/client/src/components/HomeMain/HomeMain.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Main = styled.div`
4 | display: flex;
5 | justify-content: space-evenly;
6 | align-items:center;
7 | position: absolute;
8 | left:0;
9 | @media (max-width: 700px){
10 | flex-direction:column;
11 | align-items:center;
12 | };
13 | @media (min-width: 1640px){
14 | right: 0;
15 | & h1{
16 | font-size: 3rem;
17 | }
18 | }
19 |
20 | `
21 |
22 | export const Heading = styled.h1`
23 | flex: 0.4;
24 | color: #ffffff;
25 | font-size: 2.4rem;
26 | width: 100%;
27 | margin-top: -12px;
28 | @media (max-width: 1200px){
29 | flex: 0.5;
30 | }
31 | @media (max-width: 860px){
32 | flex: 0.5;
33 | font-size:2rem;
34 | text-align:center;
35 | margin-top: 0;
36 | }
37 | @media (max-width: 450px){
38 | flex: 0.5;
39 | font-size:1.8rem;
40 | text-align:center;
41 | font-weight: 500;
42 | word-spacing: 0.1px;
43 | }
44 | `
--------------------------------------------------------------------------------
/server/middleware/authMiddleware.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import User from '../models/userModel.js';
3 | import asyncHandler from 'express-async-handler'
4 |
5 | export const protectRoute = asyncHandler(async (req, res, next) => {
6 | let token
7 |
8 | if(req.headers.authorization && req.headers.authorization.startsWith('Bearer')){
9 | try {
10 | token = req.headers.authorization.split(' ')[1];
11 | const decoded = jwt.verify(token, process.env.JWT_SECRET);
12 | req.user = await User.findById(decoded.id).select('-password');
13 | } catch (error) {
14 | res.status(401);
15 | throw new Error('Not authorized, token failed') ;
16 | }
17 | }
18 | if(!token){
19 | res.status(401)
20 | throw new Error('Not Authorized, no token')
21 | }
22 | next()
23 | });
24 |
25 | export const isAdmin = ( req, res, next ) => {
26 |
27 | if(req.user && req.user.isAdmin){
28 | next()
29 | }else{
30 | res.status(401)
31 | throw new Error('Not authorized as an Admin')
32 | }
33 |
34 | }
--------------------------------------------------------------------------------
/client/src/components/HomeMain/index.js:
--------------------------------------------------------------------------------
1 | import { Container } from '@material-ui/core'
2 | import React, { useEffect, useRef } from 'react'
3 | import ProductCarousel from '../ProductsCarousel'
4 | import { Heading, Main } from './HomeMain'
5 | import {TweenMax, Power3} from 'gsap';
6 |
7 | const HomeMain = () => {
8 |
9 | let headingRef = useRef(null);
10 |
11 | useEffect(() => {
12 | TweenMax.from(headingRef, 1.2, {opacity: 0, y: 100, ease: Power3.easeOut, delay: 0.5})
13 | }, [])
14 | return (
15 |
16 |
17 | headingRef = el}>
18 |
19 | Lose yourself between the lines!
20 | Select from a wide range of books.
21 |
22 |
23 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | export default HomeMain
34 |
--------------------------------------------------------------------------------
/client/src/components/CartItems/CartItems.elements.js:
--------------------------------------------------------------------------------
1 | import {TableContainer } from '@material-ui/core';
2 | import styled from 'styled-components';
3 | import {Link} from 'react-router-dom';
4 |
5 | export const StyledLink = styled(Link)`
6 | color: inherit;
7 | text-decoration: none;
8 | `
9 |
10 | export const StyledTable = styled(TableContainer)`
11 |
12 | @media (max-width: 768px){
13 | & .MuiTableCell-root{
14 | padding: 10px;
15 | }
16 | }
17 |
18 | @media (max-width: 450px){
19 | & .MuiTableCell-root{
20 | padding: 10px;
21 | }
22 |
23 | & h3{
24 | font-size: 10px;
25 | font-weight: bolder;
26 | }
27 | & img {
28 | width: 50px;
29 | }
30 | & p {
31 | font-size: 12px
32 | }
33 |
34 | }
35 |
36 | & svg{
37 | & :hover{
38 | cursor: pointer;
39 | }
40 | }
41 | `
42 |
43 | export const Image = styled.img`
44 | width: 80px;
45 | height: auto;
46 | `
47 |
48 | export const StyledOption = styled.option`
49 | cursor: pointer;
50 | & :hover{
51 | background-color: gray !important
52 | }
53 | `
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BookAttic is an online bookstore made using the MERN stack.
2 | [Link](https://bookattic.herokuapp.com/) to the website.
3 |
4 | ## Table of contents
5 | * [General info](#general-info)
6 | * [Technologies](#technologies)
7 | * [Setup](#setup)
8 |
9 | ## General info
10 | This project is my first attempt at making a full-stack application. Node was the technology of choice here for the backend which further improved my understanding of JS.
11 | This is a full fledged e-commerce site for books. People can create an account and look at all their orders.
12 | This also has admin features built-in which can be accessed by signing in with an admin account. An Admin can add, edit or remove books and can also access a list of all orders.
13 |
14 | ## Technologies
15 |
16 | * NodeJs
17 | * Express
18 | * React
19 | * Redux
20 | * MongoDB (mongoose)
21 | * Styled-Components
22 | * Material-UI
23 | * GSAP (animations)
24 | * PayPal SDK
25 | * JWT (auth)
26 | * Axios
27 |
28 | ## Setup
29 | To run this project, install both server and client dependencies using npm:
30 |
31 | ```
32 | $ npm install
33 | $ cd client
34 | $ npm install
35 | ```
36 |
37 | Using concurrently you can run both express server and CRA server at the same time:
38 |
39 | ```
40 | $ npm run dev
41 | ```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mern-e-commerce",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "type": "module",
7 | "dependencies": {
8 | "bcryptjs": "^2.4.3",
9 | "dotenv": "^8.2.0",
10 | "express": "^4.17.1",
11 | "express-async-handler": "^1.1.4",
12 | "jsonwebtoken": "^8.5.1",
13 | "mongoose": "^5.12.3",
14 | "multer": "^1.4.2",
15 | "styled-components": "^5.2.3"
16 | },
17 | "devDependencies": {
18 | "concurrently": "^6.0.2"
19 | },
20 | "scripts": {
21 | "start": "node server/server",
22 | "server": "nodemon server/server",
23 | "client": "npm start --prefix client",
24 | "dev": "concurrently \"npm run server\" \"npm run client\"",
25 | "data:import": "node server/seeder",
26 | "data:destroy": "node server/seeder -d",
27 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client"
28 | },
29 | "repository": {
30 | "type": "git",
31 | "url": "git+https://github.com/chiragdatwani/mern-e-commerce.git"
32 | },
33 | "author": "Chirag Datwani",
34 | "license": "ISC",
35 | "bugs": {
36 | "url": "https://github.com/chiragdatwani/mern-e-commerce/issues"
37 | },
38 | "homepage": "https://github.com/chiragdatwani/mern-e-commerce#readme"
39 | }
40 |
--------------------------------------------------------------------------------
/client/src/components/GenreSelector/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import { Grid } from '@material-ui/core/';
3 | import { GridContainer } from './GenreSelector.elements';
4 | import { Link } from 'react-router-dom';
5 | import { TweenMax, Power3 } from 'gsap';
6 |
7 | const GenreSelector = () => {
8 |
9 | const genres = ['thriller', 'romance', 'young adult', 'science fiction', 'fantasy', 'poetry', 'biography', 'self help'];
10 | let genreRef = useRef([]);
11 |
12 | useEffect(() => {
13 | TweenMax.from(genreRef.current,{opacity: 0, scale: 0, stagger: .1, ease: Power3.easeOut, delay: 1})
14 | }, [])
15 | return (
16 |
17 | { genres.map( (genre, index) => (
18 | genreRef.current[index] = el } key={genre} item xs={6} sm={3}>
19 |
20 |
21 |
{genre}
22 |
23 |
24 |
25 |
26 | ))}
27 |
28 | )
29 | }
30 |
31 | export default GenreSelector
32 |
--------------------------------------------------------------------------------
/client/src/components/Product/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react'
2 | import { Grid } from '@material-ui/core';
3 | import {CardContainer, ImageContainer, Info, StyledLink} from './Product.elements'
4 | import Stars from '../Stars/index'
5 | import {gsap, TweenMax, Power3 } from 'gsap'
6 | import {ScrollTrigger} from 'gsap/ScrollTrigger'
7 | gsap.registerPlugin(ScrollTrigger)
8 |
9 | function Product({product}) {
10 |
11 | let prodRef = useRef([]);
12 |
13 | useEffect(() => {
14 | TweenMax.from(prodRef, 1.2, {scrollTrigger: prodRef, opacity: 0, y: 20, ease: Power3.easeOut})
15 | }, [])
16 |
17 | return (
18 | prodRef = el }>
19 |
20 |
21 |
22 |
23 |
24 |
25 | {product.name}
26 |
27 | ${product.price.toFixed(2)}
28 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
36 | export default Product;
37 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import express from 'express'
3 | import dotenv from 'dotenv';
4 | import connectDB from './config/db.js'
5 | import productRoutes from './routes/productRouter.js';
6 | import userRoutes from './routes/userRoutes.js'
7 | import orderRoutes from './routes/orderRoutes.js'
8 | import uploadRoutes from './routes/uploadRoutes.js'
9 | import {notFound, errorHandler} from './middleware/errorMiddleware.js'
10 |
11 | dotenv.config();
12 |
13 | connectDB();
14 |
15 | const app = express();
16 |
17 | app.use(express.json())
18 |
19 | app.use('/api/products', productRoutes)
20 | app.use('/api/users', userRoutes)
21 | app.use('/api/orders', orderRoutes)
22 | app.use('/api/upload', uploadRoutes)
23 |
24 | app.get('/api/config/paypal', (req,res) => {
25 | res.send(process.env.PAYPAL_CLIENT_ID)
26 | })
27 |
28 | const __dirname = path.resolve();
29 | app.use('/uploads', express.static(path.join(__dirname, '/uploads')))
30 |
31 | if(process.env.NODE_ENV === 'production'){
32 | app.use(express.static(path.join(__dirname, '/client/build')))
33 |
34 | app.get('*', ( req, res ) => { res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html')) })
35 | };
36 |
37 | app.use(notFound);
38 | app.use(errorHandler);
39 |
40 | const PORT = process.env.PORT || 5000
41 |
42 | app.listen(PORT, console.log(`Server Running in ${process.env.NODE_ENV} on port ${PORT}`));
--------------------------------------------------------------------------------
/client/src/components/Header/Header.elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import {Link} from 'react-router-dom'
3 | import {Container} from '@material-ui/core'
4 |
5 | export const NavContainer = styled(Container)`
6 | display: flex;
7 | justify-content: space-between !important;
8 | align-items: center;
9 | width: 100%;
10 | margin-top: 2px;
11 | color: #ffffff
12 | `
13 |
14 | export const ButtonContainer = styled.div`
15 | display: flex;
16 | justify-content: flex-end;
17 |
18 | margin-right: -25px;
19 | & > *{
20 | margin-left: 8px;
21 | }
22 |
23 | @media (max-width: 500px){
24 | margin-right: -30px;
25 | padding: 0;
26 |
27 | & .nav-label{
28 | display:none;
29 | }
30 |
31 | & > *{
32 | margin-left: 0px;
33 | }
34 |
35 | & .MuiButtonBase-root{
36 | width: 45px;
37 | }
38 |
39 | /* & button{
40 | padding: 0px;
41 | } */
42 |
43 | }
44 | `
45 |
46 | export const StyledLink = styled(Link)`
47 | color: inherit;
48 | text-decoration:none;
49 | display: flex;
50 | align-items: center;
51 | & > img{
52 | width: 50px;
53 | }
54 |
55 | @media (max-width:400px){
56 | & > img{
57 | width: 40px;
58 | margin-left:-12px
59 | }
60 | }
61 | `
62 |
63 |
--------------------------------------------------------------------------------
/client/src/reducers/cartReducers.js:
--------------------------------------------------------------------------------
1 | import types from '../actions/types';
2 |
3 | export const cartReducer = (state = {cartItems: [], shippingAddress: {}}, action) => {
4 |
5 | switch (action.type) {
6 | case types.CART_ADD_ITEM:
7 | const item = action.payload;
8 | const exists = state.cartItems.find(x => x.product === item.product);
9 | if(exists){
10 | return {
11 | ...state,
12 | cartItems: state.cartItems.map(x => x.product === exists.product ? item : x)
13 | }
14 | }else{
15 | return {...state,cartItems:[...state.cartItems, action.payload]}
16 | }
17 | case types.CART_REMOVE_ITEM:
18 | return {
19 | ...state,
20 | cartItems: state.cartItems.filter(item => item.product !== action.payload )
21 | }
22 | case types.CART_SAVE_SHIPPING_ADDRESS:
23 | return {
24 | ...state,
25 | shippingAddress: action.payload
26 | }
27 | case types.CART_SAVE_PAYMENT_METHOD:
28 | return {
29 | ...state,
30 | paymentMethod: action.payload
31 | }
32 | case types.CART_CLEAR:
33 | return {
34 | cartItems: [], shippingAddress: {}
35 | }
36 | default:
37 | return state
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/server/seeder.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 | import dotenv from 'dotenv'
3 | import users from './data/users.js'
4 | import products from './data/products.js'
5 | import User from './models/userModel.js'
6 | import Product from './models/productModel.js'
7 | import Order from './models/orderModel.js'
8 |
9 | import connectDB from './config/db.js';
10 |
11 | dotenv.config();
12 | connectDB();
13 |
14 | const importData = async () => {
15 | try {
16 | await Order.deleteMany()
17 | await Product.deleteMany()
18 | await User.deleteMany();
19 |
20 | const createdUsers = await User.insertMany(users);
21 |
22 | const adminUser = createdUsers[0];
23 |
24 | const sampleProducts = products.map((product) => {
25 | return {...product, user: adminUser}
26 | })
27 |
28 | await Product.insertMany(sampleProducts);
29 | console.log('Data imported!');
30 |
31 | process.exit()
32 | } catch (error) {
33 | console.log(error.message);
34 | process.exit(1)
35 | }
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!');
46 | process.exit()
47 | } catch (error) {
48 | console.log(error.message);
49 | process.exit(1)
50 | }
51 | }
52 |
53 | if(process.argv[2] === '-d') {
54 | destroyData();
55 | } else{
56 | importData()
57 | };
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "proxy": "http://localhost:5000",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {
7 | "@material-ui/core": "^4.11.3",
8 | "@material-ui/icons": "^4.11.2",
9 | "@material-ui/lab": "^4.0.0-alpha.57",
10 | "@testing-library/jest-dom": "^5.11.4",
11 | "@testing-library/react": "^11.1.0",
12 | "@testing-library/user-event": "^12.1.10",
13 | "axios": "^0.21.1",
14 | "gsap": "^3.6.1",
15 | "react": "^17.0.2",
16 | "react-dom": "^17.0.2",
17 | "react-helmet": "^6.1.0",
18 | "react-material-ui-carousel": "^2.2.4",
19 | "react-paypal-button-v2": "^2.6.3",
20 | "react-redux": "^7.2.3",
21 | "react-router-dom": "^5.2.0",
22 | "react-scripts": "4.0.3",
23 | "redux": "^4.0.5",
24 | "redux-devtools-extension": "^2.13.9",
25 | "redux-thunk": "^2.3.0",
26 | "styled-components": "^5.2.3",
27 | "web-vitals": "^1.0.1"
28 | },
29 | "scripts": {
30 | "start": "react-scripts start",
31 | "build": "react-scripts build",
32 | "test": "react-scripts test",
33 | "eject": "react-scripts eject"
34 | },
35 | "eslintConfig": {
36 | "extends": [
37 | "react-app",
38 | "react-app/jest"
39 | ]
40 | },
41 | "browserslist": {
42 | "production": [
43 | ">0.2%",
44 | "not dead",
45 | "not op_mini all"
46 | ],
47 | "development": [
48 | "last 1 chrome version",
49 | "last 1 firefox version",
50 | "last 1 safari version"
51 | ]
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/client/src/actions/cartActions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | import types from './types';
4 |
5 | export const addToCart = (id, qty) => async (dispatch, getState) => {
6 |
7 | const { data } = await axios.get(`/api/products/${id}`)
8 |
9 | dispatch({
10 | type: types.CART_ADD_ITEM,
11 | payload: {
12 | product: data._id,
13 | name: data.name,
14 | image: data.image,
15 | price:data.price,
16 | countInStock: data.countInStock,
17 | qty
18 | }
19 | })
20 |
21 | localStorage.setItem('cartItems', JSON.stringify(getState().cart.cartItems))
22 | }
23 |
24 | export const removeFromCart = (id) => (dispatch, getState) => {
25 | dispatch({
26 | type: types.CART_REMOVE_ITEM,
27 | payload: id
28 | })
29 |
30 | localStorage.setItem('cartItems', JSON.stringify(getState().cart.cartItems))
31 | };
32 |
33 | export const saveShippingAddress = (data) => (dispatch) => {
34 |
35 | dispatch({
36 | type: types.CART_SAVE_SHIPPING_ADDRESS,
37 | payload: data
38 | })
39 |
40 | localStorage.setItem('shippingAddress', JSON.stringify(data))
41 | }
42 |
43 | export const savePaymentMethod = (data) => (dispatch) => {
44 |
45 | dispatch({
46 | type: types.CART_SAVE_PAYMENT_METHOD,
47 | payload: data
48 | })
49 |
50 | localStorage.setItem('paymentMethod', JSON.stringify(data))
51 | }
52 |
53 | export const clearCart = () => {
54 | return {
55 | type: types.CART_CLEAR
56 | }
57 | };
--------------------------------------------------------------------------------
/client/src/pages/Search/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Container, Grid } from '@material-ui/core';
3 | import { Alert } from '@material-ui/lab';
4 | import axios from 'axios';
5 | import Loader from '../../components/Loader/Loader';
6 | import Product from '../../components/Product';
7 | import Meta from '../../components/Meta';
8 |
9 | const SearchPage = ({match}) => {
10 |
11 | const [products, setProducts] = useState([]);
12 | const [loading, setLoading] = useState(true);
13 |
14 | useEffect(()=>{
15 | const fetchProducts = async() => {
16 | const { data } = await axios.get(`/api/products/search/${match.params.keyword}`)
17 | setProducts(data);
18 | setLoading(false);
19 | }
20 | fetchProducts();
21 | },[match])
22 |
23 | return (
24 |
25 |
26 |
27 | {`Search results for: ${match.params.keyword}`}
28 | {loading ? : products && products.length === 0 ? {`Can't find any related books`} : (
29 |
30 | {products.map( product => (
31 |
32 | ))}
33 |
34 | )}
35 |
36 |
37 | )
38 | }
39 |
40 | export default SearchPage
41 |
--------------------------------------------------------------------------------
/server/models/productModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const ReviewSchema = new mongoose.Schema({
4 | name: {
5 | type:String,
6 | required: true
7 | },
8 | rating: {
9 | type: Number,
10 | required: true
11 | },
12 | comment: {
13 | type: String,
14 | required: true
15 | },
16 | user: {
17 | type: mongoose.Schema.Types.ObjectId,
18 | required: true,
19 | ref: 'User '
20 | },
21 | }, {timestamps: true})
22 |
23 | const ProductSchema = new mongoose.Schema({
24 |
25 | name: {
26 | type: String,
27 | required: true
28 | },
29 | image: {
30 | type: String,
31 | required: true
32 | },
33 | author: {
34 | type: String,
35 | required: true
36 | },
37 | category: {
38 | type: String,
39 | required: true
40 | },
41 | publication: {
42 | type: String,
43 | required: true
44 | },
45 | ISBN: {
46 | type: String,
47 | required: true
48 | },
49 | description: {
50 | type: String,
51 | required: true
52 | },
53 | reviews: [ReviewSchema],
54 | rating: {
55 | type: Number,
56 | required: true,
57 | default: 0
58 | },
59 | numReviews: {
60 | type: Number,
61 | required: true,
62 | default: 0
63 | },
64 | price: {
65 | type: Number,
66 | required: true,
67 | default: 0
68 | },
69 | countInStock:{
70 | type: Number,
71 | required: true,
72 | default: 0
73 | }
74 | })
75 |
76 | const Product = mongoose.model('Product', ProductSchema);
77 |
78 | export default Product;
--------------------------------------------------------------------------------
/client/src/components/Product/Product.elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import {Link} from 'react-router-dom';
3 | import { Paper } from '@material-ui/core';
4 |
5 | export const StyledLink = styled(Link)`
6 | color: inherit;
7 | text-decoration: none;
8 | `
9 |
10 | export const ImageContainer = styled.div`
11 | width: 100px;
12 | height: 150px;
13 | position: absolute;
14 | transition: all .3s ease-in-out;
15 | box-shadow: rgba(0, 0, 0, 0.4) 0px 30px 90px;
16 |
17 | left: -10px;
18 | & img{
19 | width: 100px;
20 | height: 150px;
21 | };
22 |
23 | `
24 |
25 | export const CardContainer = styled(Paper)`
26 | height: 170px;
27 | margin: 0 auto;
28 | width: 260px;
29 | display: flex;
30 | align-items: center;
31 | transition: all 3.s ease;
32 | position: relative;
33 | @media (max-width: 1130px){
34 | width: 240px;
35 | };
36 | &:hover {
37 | .card-img{
38 | left: -15px;
39 | }
40 | }
41 |
42 | `
43 |
44 | export const Info = styled.div`
45 | display: flex;
46 | padding: 10px 10px 10px 100px;
47 | height: 100%;
48 | flex-direction: column;
49 | justify-content: space-evenly;
50 | & h4{
51 | margin:0;
52 | color: black;
53 | text-overflow: scroll;
54 | }
55 | & p{
56 | margin:0
57 | }
58 | @media (max-width: 1120px){
59 | & h4{
60 | font-size: 12px;
61 | }
62 | }
63 |
64 | `
--------------------------------------------------------------------------------
/client/src/pages/Payment/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Container, FormControl, FormControlLabel, Paper, Radio, RadioGroup } from '@material-ui/core';
3 | import { FormContainer } from './Payment.elements';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { savePaymentMethod } from '../../actions/cartActions';
6 | import StepperNav from '../../components/StepperNav';
7 | import Meta from '../../components/Meta';
8 |
9 |
10 | const PaymentPage = ({ history }) => {
11 |
12 | const cart = useSelector(state => state.cart);
13 | const {shippingAddress} = cart
14 |
15 | if(!shippingAddress){
16 | history.push('/shipping')
17 | }
18 |
19 | const [paymentMethod, setPaymentMethod] = useState('PayPal');
20 |
21 | const dispatch = useDispatch()
22 |
23 | const submitHandler = (e) => {
24 | e.preventDefault();
25 | dispatch(savePaymentMethod(paymentMethod));
26 | history.push('/placeorder');
27 | }
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 | PAYMENT METHOD
36 |
44 |
45 |
46 |
47 |
48 | )
49 | }
50 |
51 | export default PaymentPage;
52 |
--------------------------------------------------------------------------------
/server/models/orderModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const orderSchema = mongoose.Schema({
4 |
5 | user: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | required: true,
8 | ref: 'User'
9 | },
10 | orderItems: [
11 | {
12 | name: {
13 | type: String,
14 | required: true
15 | },
16 | qty: {
17 | type: Number,
18 | required: true
19 | },
20 | image: {
21 | type: String,
22 | required: true
23 | },
24 | price: {
25 | type: Number,
26 | required: true
27 | },
28 | product: {
29 | type: mongoose.Schema.Types.ObjectId,
30 | required: true,
31 | ref: 'Product'
32 | }
33 | }
34 | ],
35 | shippingAddress: {
36 | address: {type: String, required:true},
37 | city: {type: String, required:true},
38 | postalCode: {type: String, required:true},
39 | country: {type: String, required:true}
40 | },
41 | paymentMethod: {
42 | type: 'String',
43 | required: true
44 | },
45 | paymentResult: {
46 | id: {type: String},
47 | status: {type: String},
48 | update_time: {type: String},
49 | email_address: {type: String}
50 | },
51 | shippingPrice: {
52 | type: Number,
53 | required: true,
54 | default: 0.0
55 | },
56 | totalPrice: {
57 | type: Number,
58 | required: true,
59 | default: 0.0
60 | },
61 | isPaid: {
62 | type: Boolean,
63 | required: true,
64 | default: false
65 | },
66 | isDelivered: {
67 | type: Boolean,
68 | reuired: true,
69 | default: false
70 | },
71 | paidAt: {
72 | type: Date
73 | },
74 | deliveredAt: {
75 | type: Date
76 | }
77 | }, {timestamps: true})
78 |
79 | const order = mongoose.model('Order', orderSchema);
80 |
81 | export default order;
--------------------------------------------------------------------------------
/client/src/components/SearchBox/SearchBox.elements.js:
--------------------------------------------------------------------------------
1 | import { List, ListItem } from '@material-ui/core';
2 | import styled from 'styled-components';
3 |
4 | export const SearchContainer = styled.div`
5 | margin: auto;
6 | width: 40%;
7 | color: #ffffff;
8 | & .MuiInputBase-input{
9 | padding: 9.5px 14px !important;
10 | color: #ffffff !important;
11 | };
12 | & form{
13 | position: relative;
14 | & .MuiSvgIcon-root {
15 | position: absolute;
16 | right: 15%;
17 | top: 18%;
18 | @media (max-width: 750px){
19 | right: -5%;
20 | }
21 | @media (max-width: 620px){
22 | display: none;
23 | }
24 | }
25 | }
26 | `
27 |
28 | export const MyList = styled(List)`
29 |
30 | color: black;
31 | background-color: #ffffff;
32 | border-radius: 10px;
33 | padding: 0;
34 | box-shadow: 0px 18px 23px 1px rgba(0,0,0,0.55);
35 |
36 | `
37 |
38 | export const MyListItem = styled(ListItem)`
39 | & :hover{
40 | background: '#f5f5f5f5'
41 | }
42 | `
43 |
44 | export const SearchList = styled.div`
45 |
46 | position: absolute;
47 | margin-top: 5px;
48 | width: 36%;
49 |
50 | @media (max-width: 500px){
51 | width: 50%
52 | }
53 | `
54 |
55 | export const StyledInput = styled.input`
56 | color: #ffffff;
57 | font-size: 16px;
58 | border: none;
59 | border-radius: 16px;
60 | background-color: rgba(255, 255, 255, .15);
61 | outline: none;
62 | width: 80%;
63 | text-overflow: ellipsis;
64 | padding: 10px 20px;
65 | box-shadow: 0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%);
66 | ::placeholder{
67 | color: #ffffff;
68 | };
69 | &:hover{
70 | background-color: rgba(255, 255, 255, .25);
71 | };
72 | &:focus{
73 | background-color: rgba(255, 255, 255, .25);
74 | };
75 | @media (max-width: 750px){
76 | font-size: 15px;
77 | padding: 11px 10px;
78 | width: 100%
79 | }
80 | `
--------------------------------------------------------------------------------
/client/src/store.js:
--------------------------------------------------------------------------------
1 | import {createStore, combineReducers, applyMiddleware} from 'redux';
2 | import thunk from 'redux-thunk';
3 | import {composeWithDevTools} from 'redux-devtools-extension';
4 |
5 | import {productListReducer, productDetailsReducer, productDeleteReducer, productCreateReducer, productUpdateReducer, productCreateReviewReducer} from './reducers/productReducers';
6 | import {cartReducer} from './reducers/cartReducers';
7 | import { orderCreateReducer, orderDetailsReducer, orderPayReducer, orderDeliverReducer, orderListReducer, orderListAdminReducer } from './reducers/orderReducers';
8 | import { userDeleteReducer, userDetailsReducer, userListReducer, userLoginReducer, userProfileUpdateReducer, userRegisterReducer } from './reducers/userReducers';
9 |
10 |
11 | const reducer = combineReducers({
12 | productList : productListReducer,
13 | currentProduct: productDetailsReducer,
14 | cart: cartReducer,
15 | currentUser: userLoginReducer,
16 | registeredUser: userRegisterReducer,
17 | userDetails: userDetailsReducer,
18 | userUpdateProfile: userProfileUpdateReducer,
19 | orderCreate: orderCreateReducer,
20 | orderDetails: orderDetailsReducer,
21 | orderPay: orderPayReducer,
22 | orderDeliver: orderDeliverReducer,
23 | myOrders: orderListReducer,
24 | orderList: orderListAdminReducer,
25 | userList: userListReducer,
26 | deleteUser: userDeleteReducer,
27 | deleteProduct: productDeleteReducer,
28 | createProduct: productCreateReducer,
29 | updateProduct: productUpdateReducer,
30 | productReviewCreate: productCreateReviewReducer
31 | });
32 |
33 | const cartItemsLocalStorage = localStorage.getItem('cartItems') ? JSON.parse(localStorage.getItem('cartItems')) : [];
34 |
35 | const userInfoLocalStorage = localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')) : null;
36 |
37 | const shippingAddressLocalStorage = localStorage.getItem('shippingAddress') ? JSON.parse(localStorage.getItem('shippingAddress')) : {};
38 |
39 | const initialState= {
40 | cart: {cartItems: cartItemsLocalStorage, shippingAddress: shippingAddressLocalStorage,},
41 | currentUser: {userInfo: userInfoLocalStorage},
42 | };
43 |
44 | const middleware = [thunk];
45 |
46 | const store = createStore(reducer, initialState, composeWithDevTools(applyMiddleware(...middleware)));
47 |
48 | export default store;
--------------------------------------------------------------------------------
/client/src/pages/Cart/index.js:
--------------------------------------------------------------------------------
1 | import React, {useEffect} from 'react';
2 | import { useDispatch, useSelector} from 'react-redux';
3 | import { addToCart } from '../../actions/cartActions';
4 | import { Button, Card, CardContent, Container, Grid } from '@material-ui/core'
5 | import { Alert } from '@material-ui/lab';
6 | import CartItems from '../../components/CartItems'
7 | import Meta from '../../components/Meta';
8 |
9 |
10 | const CartPage = ({match, location, history}) => {
11 |
12 | const productId = match.params.id;
13 |
14 | const qty = location.search ? Number(location.search.split('=')[1]) : 1;
15 |
16 | const dispatch = useDispatch();
17 | const cartItems = useSelector(state => state.cart.cartItems)
18 |
19 | const checkoutHandler = () => {
20 | console.log('checkout');
21 | history.push('/login?redirect=shipping')
22 | }
23 |
24 | useEffect(() => {
25 |
26 | if(productId){
27 | dispatch(addToCart(productId, qty))
28 | }
29 | }, [dispatch, productId, qty])
30 |
31 | return (
32 |
33 |
34 |
35 | SHOPPING CART
36 |
37 |
38 | {cartItems.length === 0 ? (
39 | {'Shopping cart is empty'} ) :(
40 |
41 |
42 | )}
43 |
44 |
45 |
46 |
47 | Total Items : {cartItems.reduce((acc,item) => (acc + item.qty), 0)}
48 | Total Amount : ${cartItems.reduce((acc,item) => (acc + item.qty * item.price), 0).toFixed(2)}
49 |
50 |
51 | Proceed To Checkout
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | )
61 | }
62 |
63 | export default CartPage
64 |
--------------------------------------------------------------------------------
/client/src/pages/Genre/index.js:
--------------------------------------------------------------------------------
1 | import { Container, Grid } from '@material-ui/core'
2 | import axios from 'axios';
3 | import React, { useEffect, useRef, useState } from 'react'
4 | import Loader from '../../components/Loader/Loader';
5 | import Product from '../../components/Product';
6 | import QuoteGenerator from '../../components/QuoteGenerator';
7 | import Meta from '../../components/Meta';
8 | import {gsap, TweenMax, Power3} from 'gsap';
9 | import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
10 |
11 | gsap.registerPlugin(ScrollToPlugin);
12 |
13 | const GenrePage = ({match}) => {
14 |
15 | const genre = match.params.genre
16 |
17 | const [ products, setProducts ] = useState([]);
18 | const [ loading, setLoading ] = useState(true);
19 |
20 | let quoteRef = useRef(null);
21 | let prodsRef = useRef(null);
22 |
23 |
24 | useEffect(()=>{
25 | TweenMax.to(window, 0.3, {scrollTo: 0, ease: Power3.easeOut});
26 | TweenMax.from(quoteRef, 1.5, {opacity: 0, y:40, ease: Power3.easeOut})
27 | TweenMax.from(prodsRef, 1.5, {opacity: 0, y:40, ease: Power3.easeOut, delay: 0.5})
28 | const fetchProducts = async() => {
29 | const {data} = await axios.get(`/api/products/genre/${genre}`);
30 | setProducts(data);
31 | setLoading(false);
32 | };
33 | fetchProducts()
34 | }, [match, genre])
35 |
36 | return (
37 |
38 |
(e.charAt(0).toUpperCase() + e.slice(1))).join(' ')} Books | Book Attic`} />
39 |
40 |
quoteRef = el}>
41 |
42 |
43 |
44 |
45 |
prodsRef = el}>
46 | {`Top ${genre.split('-').map(e => (e.charAt(0).toUpperCase() + e.slice(1))).join(' ')} Books`}
47 | {loading ? : (
48 |
49 | {products.map( product => (
50 |
51 | ))}
52 |
53 | )}
54 |
55 |
56 | )
57 | }
58 |
59 | export default GenrePage;
60 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {BrowserRouter as Router, Route} from 'react-router-dom'
3 | import Header from './components/Header/';
4 | import Product from './pages/Product'
5 | import Footer from './components/Footer/';
6 | import HomePage from './pages/Home';
7 | import { ThemeProvider } from '@material-ui/core';
8 | import CartPage from './pages/Cart';
9 | import LoginPage from './pages/Login';
10 | import RegisterPage from './pages/Register';
11 | import ProfilePage from './pages/Profile';
12 | import ShippingPage from './pages/Shipping';
13 | import { myTheme } from './theme';
14 | import PaymentPage from './pages/Payment';
15 | import PlaceOrder from './pages/PlaceOrder';
16 | import Order from './pages/Order';
17 | import UserList from './pages/UserList';
18 | import ProductList from './pages/ProductList';
19 | import ProductEdit from './pages/ProductEdit';
20 | import OrderList from './pages/OrderList';
21 | import GenrePage from './pages/Genre'
22 | import SearchPage from './pages/Search';
23 |
24 |
25 |
26 | const App = () => {
27 |
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | );
66 | }
67 |
68 | export default App;
69 |
--------------------------------------------------------------------------------
/client/src/products.js:
--------------------------------------------------------------------------------
1 | const products = [
2 | {
3 | _id: '1',
4 | name: 'Airpods Wireless Bluetooth Headphones',
5 | image: '/images/airpods.jpg',
6 | description:
7 | 'Bluetooth technology lets you connect it with compatible devices wirelessly High-quality AAC audio offers immersive listening experience Built-in microphone allows you to take calls while working',
8 | brand: 'Apple',
9 | category: 'Electronics',
10 | price: 89.99,
11 | countInStock: 10,
12 | rating: 4.5,
13 | numReviews: 12,
14 | },
15 | {
16 | _id: '2',
17 | name: 'iPhone 11 Pro 256GB Memory',
18 | image: '/images/phone.jpg',
19 | description:
20 | 'Introducing the iPhone 11 Pro. A transformative triple-camera system that adds tons of capability without complexity. An unprecedented leap in battery life',
21 | brand: 'Apple',
22 | category: 'Electronics',
23 | price: 599.99,
24 | countInStock: 7,
25 | rating: 4.0,
26 | numReviews: 8,
27 | },
28 | {
29 | _id: '3',
30 | name: 'Cannon EOS 80D DSLR Camera',
31 | image: '/images/camera.jpg',
32 | description:
33 | 'Characterized by versatile imaging specs, the Canon EOS 80D further clarifies itself using a pair of robust focusing systems and an intuitive design',
34 | brand: 'Cannon',
35 | category: 'Electronics',
36 | price: 929.99,
37 | countInStock: 5,
38 | rating: 3,
39 | numReviews: 12,
40 | },
41 | {
42 | _id: '4',
43 | name: 'Sony Playstation 4 Pro White Version',
44 | image: '/images/playstation.jpg',
45 | description:
46 | 'The ultimate home entertainment center starts with PlayStation. Whether you are into gaming, HD movies, television, music',
47 | brand: 'Sony',
48 | category: 'Electronics',
49 | price: 399.99,
50 | countInStock: 11,
51 | rating: 5,
52 | numReviews: 12,
53 | },
54 | {
55 | _id: '5',
56 | name: 'Logitech G-Series Gaming Mouse',
57 | image: '/images/mouse.jpg',
58 | description:
59 | 'Get a better handle on your games with this Logitech LIGHTSYNC gaming mouse. The six programmable buttons allow customization for a smooth playing experience',
60 | brand: 'Logitech',
61 | category: 'Electronics',
62 | price: 49.99,
63 | countInStock: 7,
64 | rating: 3.5,
65 | numReviews: 10,
66 | },
67 | {
68 | _id: '6',
69 | name: 'Amazon Echo Dot 3rd Generation',
70 | image: '/images/alexa.jpg',
71 | description:
72 | 'Meet Echo Dot - Our most popular smart speaker with a fabric design. It is our most compact smart speaker that fits perfectly into small space',
73 | brand: 'Amazon',
74 | category: 'Electronics',
75 | price: 29.99,
76 | countInStock: 0,
77 | rating: 4,
78 | numReviews: 12,
79 | },
80 | ]
81 |
82 | export default products
83 |
--------------------------------------------------------------------------------
/client/src/components/ProductsCarousel/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { useState, useEffect } from 'react';
3 | import Carousel from 'react-material-ui-carousel';
4 | import Loader from '../Loader/Loader.js'
5 | import { CarouselItem, LoaderContainer, StyledLink } from './ProductCarousel.elements';
6 | import ArrowForwardIosRoundedIcon from '@material-ui/icons/ArrowForwardIosRounded';
7 | import ArrowBackIosRoundedIcon from '@material-ui/icons/ArrowBackIosRounded';
8 |
9 |
10 | const ProductCarousel = () => {
11 |
12 | const [ products, setProducts ] = useState([]);
13 |
14 | useEffect(()=>{
15 |
16 | const fetchProducts = async() => {
17 | const { data } = await axios.get('/api/products/top');
18 | setProducts(data)
19 | }
20 |
21 | fetchProducts();
22 |
23 | },[])
24 |
25 | return (
26 | <>
27 | {products.length === 0 && }
28 | }
31 | PrevIcon={ }
32 | navButtonsProps={{
33 | style: {
34 | backgroundColor: 'cornflowerblue',
35 | borderRadius: '50%'
36 | }
37 | }}
38 | indicatorIconButtonProps={{
39 | style: {
40 | padding: '10px', // 1
41 | color: 'white' // 3
42 | }
43 | }}
44 | activeIndicatorIconButtonProps={{
45 | style: {
46 | transition: 'all .3s ease-out',
47 | transform: 'scale(1.5)'
48 | }
49 | }}
50 |
51 | >
52 | {products.length > 0 && (
53 | products.map( product => (
54 |
55 |
56 |
57 |
58 |
59 |
60 |
{product.name}
61 |
{`by ${product.author}`}
62 |
63 |
64 |
65 | Buy Now
66 |
67 |
68 |
69 |
70 | ))
71 | )}
72 |
73 | >
74 | )
75 | }
76 |
77 |
78 | export default ProductCarousel
79 |
--------------------------------------------------------------------------------
/client/src/components/CartItems/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react'
2 | import { Paper, Table, TableBody, TableCell, TableRow, Select, MenuItem } from '@material-ui/core'
3 | import DeleteIcon from '@material-ui/icons/Delete';
4 | import { Image,StyledLink,StyledTable } from './CartItems.elements'
5 | import { useDispatch } from 'react-redux';
6 | import { addToCart, removeFromCart } from '../../actions/cartActions';
7 | import { TweenMax, Power3 } from 'gsap';
8 |
9 | const CartItems = ({items}) => {
10 |
11 | const dispatch = useDispatch();
12 |
13 | const deleteHandler = (id) => {
14 | dispatch(removeFromCart(id))
15 | }
16 |
17 | let rowRef = useRef([]);
18 | useEffect(() => {
19 | TweenMax.from(rowRef.current, 2, {opacity: 0, y: 20, stagger: .2, ease: Power3.easeOut})
20 | }, [])
21 |
22 | return (
23 | //
24 |
25 |
26 |
27 | {
28 | items.map((item, index)=>(
29 | rowRef.current[index] = el} key={item.product}>
30 |
31 |
32 |
33 |
34 | {{{item.name} } }
35 |
36 |
37 |
38 | {`$${item.price}`}
39 |
40 |
41 | {
42 | dispatch(
45 | addToCart(item.product, Number(e.target.value))
46 | )}
47 | displayEmpty
48 | inputProps={{
49 | name: 'quantity',
50 | id: item.name,
51 | }} >
52 | Quantity
53 | {[...Array(item.countInStock).keys()].map(x => (
54 | {x + 1}
55 | ))}
56 |
57 | }
58 | { deleteHandler(item.product)}/>}
59 |
60 | ))
61 | }
62 |
63 |
64 |
65 | )
66 | }
67 |
68 | export default CartItems
69 |
--------------------------------------------------------------------------------
/client/src/pages/Product/Product.elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import {Card, Container} from '@material-ui/core'
3 |
4 | export const InfoContainer = styled.div`
5 |
6 | max-height: 100%;
7 | & p{
8 | margin: 10px 0;
9 | };
10 |
11 | @media (max-width: 900px){
12 | & h2{
13 | font-size: 22px;
14 | }
15 | };
16 | @media (max-width: 500px){
17 | & h2{
18 | font-size: 19px;
19 | }
20 | }
21 |
22 | `
23 |
24 | export const ButtonContainer = styled.div`
25 | width: 100%;
26 | display: flex;
27 | align-items: center;
28 | justify-content: space-around;
29 | padding: 0;
30 | margin: 0 auto;
31 | > * {
32 | width: 65%;
33 | }
34 | `
35 |
36 | export const StyledContainer = styled(Container)`
37 | margin-top: 2rem;
38 |
39 | `
40 |
41 | export const AddToCartContainer = styled(Card)`
42 |
43 | padding: 1rem 1.5rem;
44 |
45 | > * {
46 | width: 100%;
47 | margin: 10px auto;
48 | text-align: center;
49 | }
50 |
51 | `
52 |
53 | export const ReviewContainer = styled.div`
54 | display: flex;
55 | flex-direction: column;
56 | border-bottom: 1px solid gray;
57 | padding: 10px;
58 |
59 | & > h4{
60 | margin: 0;
61 | }
62 |
63 | & > .MuiRating-root{
64 | margin: 10px;
65 | font-size: 1.2rem;
66 | }
67 |
68 | & > h5{
69 | margin: 0px 0 5px 15px;
70 | font-size: 15px;
71 | font-weight: 550
72 | }
73 | `
74 |
75 | export const ModalBody = styled(Card)`
76 | position: absolute;
77 | top: 50%;
78 | left: 50%;
79 | transform: translate(-50%, -50%);
80 | outline: none;
81 | width: 400px;
82 | padding: 10px 20px;
83 | display: flex;
84 | flex-direction: column;
85 | align-items:flex-start;
86 | & > .MuiTextField-root{
87 | margin: 10px 0;
88 | }
89 |
90 | @media (max-width: 470px){
91 | width: 80vw;
92 | }
93 | `
94 |
95 | export const ImgAndInfo = styled.div`
96 | display: flex;
97 | justify-content: flex-start;
98 | align-items: center;
99 | height: 250px;
100 | & img{
101 | width: 160px;
102 | height: 250px;
103 | margin-right: 15px;
104 | }
105 | & h2{
106 | margin: 0;
107 | }
108 | `
109 |
110 | export const RatingContainer = styled.div`
111 | display: flex;
112 | align-items: center;
113 | margin-top: 10px;
114 | & .num-review {
115 | margin-left: 8px;
116 | font-size:12px;
117 | }
118 | `
119 |
120 | export const DescriptionContainer = styled.div`
121 | margin-top: 40px;
122 |
123 | & p{
124 | white-space: pre-line;
125 | &::first-letter{
126 | font-size: 2rem;
127 | font-weight: 400;
128 | color: #4A2FF9;
129 | line-height: 30px;
130 | }
131 | }
132 | `
--------------------------------------------------------------------------------
/client/src/components/SearchBox/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Divider, ListItemText } from '@material-ui/core'
3 | import axios from 'axios'
4 | import {Link, useHistory} from 'react-router-dom'
5 | import { SearchContainer, SearchList, StyledInput } from './SearchBox.elements'
6 | import SearchIcon from "@material-ui/icons/Search";
7 | import {MyList, MyListItem} from './SearchBox.elements'
8 |
9 | const SearchBox = () => {
10 |
11 | const [keyword, setKeyword] = useState("");
12 | const [products, setProducts] = useState([]);
13 |
14 | useEffect(()=>{
15 | async function fetchData() {
16 | if(keyword.length < 3){
17 | setProducts([]);
18 | }else{
19 | const {data} = await axios.get(`/api/products/search/${keyword}`);
20 | setProducts(data.slice(0,5))
21 | }
22 | }
23 |
24 | let timeoutId = setTimeout(()=>{
25 | if(keyword){
26 | fetchData();
27 | }
28 | }, 300)
29 |
30 | return () => {
31 | if(keyword.length < 3){
32 | setProducts([])
33 | }
34 | clearTimeout(timeoutId)
35 | }
36 |
37 | },[keyword])
38 |
39 | let history = useHistory();
40 | const handleSubmit = (e) => {
41 | e.preventDefault();
42 | history.push(`/search/${keyword}`)
43 | setKeyword('');
44 | };
45 |
46 | return (
47 |
48 |
49 |
61 | {
62 | products.length > 0 && keyword.length > 2 && (
63 |
64 |
65 |
66 | {products.map((product,index) => (
67 |
68 |
72 |
setKeyword('')}>
73 |
74 |
75 | {index !== (products.length -1) &&
}
76 |
77 |
78 |
79 | ))}
80 |
81 |
82 | )
83 | }
84 |
85 | )
86 | }
87 |
88 | export default SearchBox
89 |
--------------------------------------------------------------------------------
/client/src/pages/Home/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import {useDispatch, useSelector } from 'react-redux';
3 | import { Container, Grid } from '@material-ui/core';
4 | import Product from '../../components/Product';
5 | import Loader from '../../components/Loader/Loader';
6 | import {fetchProductsList} from '../../actions/productActions';
7 | import { Alert, Pagination } from '@material-ui/lab';
8 | import { PaginationContainer, TopRated } from './Home.elements';
9 | import HomeMain from '../../components/HomeMain';
10 | import GenreSelector from '../../components/GenreSelector';
11 | import Meta from '../../components/Meta';
12 | import {gsap, TweenMax, Power3} from 'gsap';
13 | import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
14 |
15 | gsap.registerPlugin(ScrollToPlugin);
16 |
17 |
18 |
19 | function HomePage({match, history}) {
20 |
21 | const pageNumber = Number(match.params.page) || 1;
22 |
23 | const dispatch = useDispatch();
24 | const productList = useSelector((state) => state.productList)
25 | const{loading, error, products, page, totalPages} = productList
26 |
27 | const gridRef = useRef(null);
28 | const headingRef = useRef(null);
29 |
30 | useEffect(()=>{
31 | dispatch(fetchProductsList(pageNumber));
32 | }, [dispatch, pageNumber])
33 |
34 | const handlePagination = (e, v) => {
35 | TweenMax.to(window, 0.3, {scrollTo: headingRef.current.offsetTop, ease: Power3.easeOut} )
36 | history.push(`/page/${v}`)
37 |
38 | }
39 |
40 | return (
41 |
42 |
43 |
44 |
45 |
46 |
47 | Top Rated Books
48 | {loading ?
:
49 | error ? {error} :
50 | <>
51 |
52 | {products.map( (product) => (
53 |
54 | ))}
55 |
56 |
57 |
64 |
65 | >
66 | }
67 |
68 |
69 |
70 | )
71 | };
72 |
73 | export default HomePage;
74 |
--------------------------------------------------------------------------------
/client/src/pages/Shipping/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Container, Paper, TextField } from '@material-ui/core';
3 | import { FormContainer } from './Shipping.elements';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { saveShippingAddress } from '../../actions/cartActions';
6 | import StepperNav from '../../components/StepperNav';
7 | import Meta from '../../components/Meta';
8 |
9 |
10 | const ShippingPage = ({ history }) => {
11 |
12 | const cart = useSelector(state => state.cart);
13 | const {shippingAddress} = cart
14 |
15 | const [address, setAddress] = useState(shippingAddress.address);
16 | const [city, setCity] = useState(shippingAddress.city);
17 | const [postalCode, setPostalCode] = useState(shippingAddress.postalCode);
18 | const [country, setCountry] = useState(shippingAddress.country);
19 |
20 | const dispatch = useDispatch()
21 |
22 | const submitHandler = (e) => {
23 | e.preventDefault();
24 | dispatch(saveShippingAddress({ address, city, postalCode, country }));
25 | history.push('/payment')
26 | }
27 | return (
28 |
29 |
30 |
31 |
32 |
33 | SHIPPING
34 |
70 |
71 |
72 |
73 | )
74 | }
75 |
76 | export default ShippingPage
77 |
--------------------------------------------------------------------------------
/client/src/pages/Login/index.js:
--------------------------------------------------------------------------------
1 | import { Button, Container, Paper, TextField } from '@material-ui/core';
2 | import { Alert } from '@material-ui/lab';
3 | import React, { useState, useEffect, useRef } from 'react';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { Link } from 'react-router-dom';
6 | import { login } from '../../actions/userActions'
7 | import { FormContainer, ImgContainer, LoginContainer } from './Login.elements'
8 | import { TweenMax, Power3 } from 'gsap';
9 | import Meta from '../../components/Meta';
10 |
11 | const LoginPage = ({location, history}) => {
12 |
13 | const redirect = location.search ? location.search.split('=')[1] : '/';
14 |
15 | const dispatch = useDispatch();
16 |
17 | const currentUser = useSelector(state => state.currentUser);
18 | const {loading, error, userInfo } = currentUser;
19 |
20 |
21 | const submitHandler = (e) => {
22 | e.preventDefault();
23 | dispatch(login(email, password))
24 | }
25 |
26 | const [email, setEmail] = useState('');
27 | const [password, setPassword] = useState('');
28 |
29 | //refs for GSAP
30 | let formRef = useRef(null);
31 | let imgRef = useRef(null);
32 |
33 | useEffect(() => {
34 |
35 | TweenMax.from(formRef, 1 , {opacity: 0, y: 30, ease: Power3.easeOut, delay: 0.2});
36 | TweenMax.from(imgRef, 1 , {opacity: 0, x: 50, ease: Power3.easeOut});
37 |
38 | if(userInfo){
39 | history.push(redirect)
40 | }
41 | }, [history, userInfo, redirect])
42 |
43 | return (
44 |
45 |
46 |
47 |
48 | formRef = el}>
49 | SIGN IN
50 | {error && {error} }
51 | {loading && {error} }
52 |
62 | New Customer? Register Here
63 |
64 |
65 | imgRef = el} src={process.env.PUBLIC_URL + '/icons/illustration.jpg'} alt="img" />
66 |
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | export default LoginPage;
74 |
--------------------------------------------------------------------------------
/client/src/reducers/userReducers.js:
--------------------------------------------------------------------------------
1 | import types from '../actions/types'
2 |
3 | export const userLoginReducer = (state = {}, action) => {
4 |
5 | switch (action.type) {
6 | case types.USER_LOGIN_REQUEST:
7 | return { loading: true}
8 | case types.USER_LOGIN_SUCCESS:
9 | return { loading: false, userInfo: action.payload}
10 | case types.USER_LOGIN_FAIL:
11 | return { loading: false, error: action.payload}
12 | case types.USER_LOGOUT:
13 | return {}
14 | default:
15 | return state
16 | }
17 | }
18 |
19 | export const userRegisterReducer = (state = {}, action) => {
20 |
21 | switch (action.type) {
22 | case types.USER_REGISTER_REQUEST:
23 | return { loading: true}
24 | case types.USER_REGISTER_SUCCESS:
25 | return { loading: false, userInfo: action.payload}
26 | case types.USER_REGISTER_FAIL:
27 | return { loading: false, error: action.payload}
28 | case types.USER_LOGOUT:
29 | return {}
30 | default:
31 | return state
32 | }
33 | }
34 |
35 | export const userDetailsReducer = (state = {user:{}}, action) => {
36 |
37 | switch (action.type) {
38 | case types.USER_DETAILS_REQUEST:
39 | return { ...state, loading: true}
40 | case types.USER_DETAILS_SUCCESS:
41 | return { loading: false, user: action.payload}
42 | case types.USER_DETAILS_FAIL:
43 | return { loading: false, error: action.payload}
44 | case types.USER_LOGOUT:
45 | return {}
46 | default:
47 | return state
48 | }
49 | };
50 |
51 | export const userProfileUpdateReducer = (state = {}, action) => {
52 |
53 | switch (action.type) {
54 | case types.USER_UPDATE_PROFILE_REQUEST:
55 | return { ...state, loading: true}
56 | case types.USER_UPDATE_PROFILE_SUCCESS:
57 | return { loading: false, success: true,userInfo: action.payload}
58 | case types.USER_UPDATE_PROFILE_FAIL:
59 | return { loading: false, error: action.payload}
60 | case types.USER_UPDATE_PROFILE_RESET:
61 | return {}
62 | default:
63 | return state
64 | }
65 | }
66 |
67 | export const userListReducer = (state = {users: []}, action) => {
68 |
69 | switch (action.type) {
70 | case types.USER_LIST_REQUEST:
71 | return { ...state, loading: true}
72 | case types.USER_LIST_SUCCESS:
73 | return { loading: false, users: action.payload }
74 | case types.USER_LIST_FAIL:
75 | return { loading: false, error: action.payload}
76 | case types.USER_LIST_RESET:
77 | return { users: [] }
78 | default:
79 | return state
80 | }
81 | };
82 |
83 | export const userDeleteReducer = (state = {}, action) => {
84 |
85 | switch (action.type) {
86 | case types.USER_RESET_REQUEST:
87 | return { loading: true }
88 | case types.USER_RESET_SUCCESS:
89 | return { loading: false, success: true }
90 | case types.USER_RESET_FAIL:
91 | return { loading: false, error: action.payload}
92 |
93 | default:
94 | return state
95 | }
96 | }
--------------------------------------------------------------------------------
/client/src/reducers/productReducers.js:
--------------------------------------------------------------------------------
1 | import types from '../actions/types'
2 |
3 | export const productListReducer = (state = {products: []}, action) => {
4 | switch (action.type) {
5 | case types.FETCH_PRODUCTSLIST_REQUEST:
6 | return {products: [], loading: true}
7 | case types.FETCH_PRODUCTSLIST_SUCCESS:
8 | return {products: action.payload.products, page: action.payload.page, totalPages: action.payload.totalPages, loading: false}
9 | case types.FETCH_PRODUCTSLIST_FAIL:
10 | return {loading: false, error: action.payload}
11 | default:
12 | return state
13 | }
14 | };
15 |
16 | export const productDetailsReducer = (state = {product: { reviews: []}}, action) => {
17 |
18 | switch (action.type) {
19 | case types.FETCH_PRODUCT_REQUEST:
20 | return {loading: true, ...state}
21 | case types.FETCH_PRODUCT_SUCCESS:
22 | return {loading: false, product: action.payload}
23 | case types.FETCH_PRODUCT_FAIL:
24 | return {loading: false, error: action.payload}
25 |
26 | default:
27 | return state
28 | }
29 | };
30 |
31 | export const productDeleteReducer = (state = {}, action) => {
32 |
33 | switch (action.type) {
34 | case types.PRODUCT_DELETE_REQUEST:
35 | return {loading: true}
36 | case types.PRODUCT_DELETE_SUCCESS:
37 | return {loading: false, success: true}
38 | case types.PRODUCT_DELETE_FAIL:
39 | return {loading: false, error: action.payload}
40 |
41 | default:
42 | return state
43 | }
44 | };
45 |
46 | export const productCreateReducer = (state = {}, action) => {
47 |
48 | switch (action.type) {
49 | case types.PRODUCT_CREATE_REQUEST:
50 | return {loading: true}
51 | case types.PRODUCT_CREATE_SUCCESS:
52 | return {loading: false, product: action.payload, success:true}
53 | case types.PRODUCT_CREATE_FAIL:
54 | return {loading: false, error: action.payload}
55 | case types.PRODUCT_CREATE_RESET:
56 | return {}
57 | default:
58 | return state
59 | }
60 | };
61 |
62 | export const productUpdateReducer = (state = {product:{}}, action) => {
63 |
64 | switch (action.type) {
65 | case types.PRODUCT_UPDATE_REQUEST:
66 | return {loading: true}
67 | case types.PRODUCT_UPDATE_SUCCESS:
68 | return {loading: false, product: action.payload, success:true}
69 | case types.PRODUCT_UPDATE_FAIL:
70 | return {loading: false, error: action.payload}
71 | case types.PRODUCT_UPDATE_RESET:
72 | return {product: {}}
73 | default:
74 | return state
75 | }
76 | };
77 |
78 | export const productCreateReviewReducer = (state = {}, action) => {
79 |
80 | switch (action.type) {
81 | case types.PRODUCT_CREATE_REVIEW_REQUEST:
82 | return {loading: true}
83 | case types.PRODUCT_CREATE_REVIEW_SUCCESS:
84 | return {loading: false, success:true}
85 | case types.PRODUCT_CREATE_REVIEW_FAIL:
86 | return {loading: false, error: action.payload}
87 | case types.PRODUCT_CREATE_REVIEW_RESET:
88 | return {}
89 | default:
90 | return state
91 | }
92 | };
--------------------------------------------------------------------------------
/server/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 |
8 | export const addOrderItems = asyncHandler( async(req,res) => {
9 |
10 | const { orderItems, shippingAddress, paymentMethod, itemsPrice, shippingPrice, totalPrice } = req.body;
11 |
12 | if(orderItems && orderItems.length === 0){
13 | res.status(400);
14 | throw new Error('No order items');
15 | return
16 | } else {
17 | const order = new Order({
18 | orderItems,
19 | user: req.user._id,
20 | shippingAddress,
21 | paymentMethod,
22 | itemsPrice,
23 | shippingPrice,
24 | totalPrice
25 | });
26 |
27 | const createdOrder = await order.save();
28 |
29 | res.status(201).json(createdOrder);
30 | }
31 | });
32 |
33 | // @desc Get Order by ID
34 | // @route GET /api/orders/:id
35 | // @access Private
36 | export const getOrderById = asyncHandler( async(req,res) => {
37 |
38 | const order = await Order.findById(req.params.id).populate('user', 'name email');
39 |
40 | if(order){
41 | res.json(order)
42 | } else {
43 | res.status(404)
44 | throw new Error('Order not found')
45 | }
46 | });
47 |
48 | // @desc Update order to paid
49 | // @route GET /api/orders/:id/pay
50 | // @access Private
51 | export const updateOrderToPaid = asyncHandler( async(req,res) => {
52 |
53 | const order = await Order.findById(req.params.id);
54 |
55 | if(order){
56 | order.isPaid = true;
57 | order.paidAt = Date.now();
58 | order.paymentResult = {
59 | id: req.body.id,
60 | status: req.body.status,
61 | update_time: req.body.update_time,
62 | email_address: req.body.payer.email_address
63 | }
64 |
65 | const updatedOrder = await order.save()
66 |
67 | res.json(updatedOrder);
68 |
69 | } else {
70 | res.status(404)
71 | throw new Error('Order not found')
72 | }
73 | });
74 |
75 | // @desc Get orders of logged in user
76 | // @route GET /api/orders/myorders
77 | // @access Private
78 | export const getUserOrders = asyncHandler( async(req,res) => {
79 |
80 | const orders = await Order.find({ user: req.user._id });
81 |
82 | res.json(orders);
83 | });
84 |
85 | // @desc Get all orders
86 | // @route GET /api/orders
87 | // @access Private/Admin
88 | export const getOrders = asyncHandler( async(req,res) => {
89 |
90 | const orders = await Order.find({}).populate('user','id name') ;
91 |
92 | res.json(orders);
93 | });
94 |
95 |
96 | // @desc Update order to paid
97 | // @route GET /api/orders/:id/deliver
98 | // @access Private
99 | export const updateOrderToDelivered = asyncHandler( async(req,res) => {
100 |
101 | const order = await Order.findById(req.params.id);
102 |
103 | if(order){
104 | order.isDelivered = true;
105 | order.deliveredAt = Date.now();
106 |
107 | const updatedOrder = await order.save()
108 |
109 | res.json(updatedOrder);
110 |
111 | } else {
112 | res.status(404)
113 | throw new Error('Order not found')
114 | }
115 | });
--------------------------------------------------------------------------------
/client/src/components/ProductsCarousel/ProductCarousel.elements.js:
--------------------------------------------------------------------------------
1 | import { Paper } from '@material-ui/core'
2 | import styled from 'styled-components'
3 | import { Link } from 'react-router-dom';
4 |
5 | export const LoaderContainer = styled.div`
6 | display: grid;
7 | place-items: center;
8 | height: 15rem;
9 | width: 25rem;
10 |
11 | @media (max-width: 1000px){
12 | height: 12rem;
13 | width: 20rem;
14 | };
15 | @media (max-width: 1000px){
16 | margin-top: 40px
17 | };
18 | @media (max-width: 400px){
19 | margin-top: 30px;
20 | height: 10rem;
21 | width: 17rem;
22 | };
23 | `
24 |
25 | export const CarouselItem = styled(Paper)`
26 | background-color: rgba(255,255,255,0.25);
27 | display: flex;
28 | height: 15rem;
29 | color: white;
30 | width: 25rem;
31 | padding: 10px;
32 | @media (max-width: 1000px){
33 | height: 12rem;
34 | width: 20rem;
35 | };
36 | @media (max-width: 1000px){
37 | margin-top: 40px
38 | };
39 | @media (max-width: 400px){
40 | margin-top: 30px;
41 | height: 10rem;
42 | width: 17rem;
43 | };
44 |
45 | & .carousel-img{
46 | width: 280px;
47 | height: 240px;
48 | background: cover;
49 | & img{
50 | width: 100%;
51 | height: 100%;
52 | }
53 | @media (max-width: 1000px){
54 | width: 230px;
55 | height: 190px;
56 | };
57 | @media (max-width: 400px){
58 | width: 180px;
59 | height: 160px;
60 | };
61 | }
62 | & .info{
63 | margin: 5px 5px 10px 20px;
64 | display: flex;
65 | width: 100%;
66 | flex-direction: column;
67 | justify-content: space-between;
68 | position: relative;
69 | padding-bottom: 20px;
70 | @media (max-width: 1000px){
71 | margin: -5px 5px 0px 15px;
72 | };
73 | @media (max-width: 400px){
74 | & *{
75 | font-size: 1rem;
76 | }
77 | };
78 |
79 | & button{
80 | font-weight: bold;
81 | padding: 10px 10px;
82 | text-align: center;
83 | text-transform: uppercase;
84 | transition: 0.3s;
85 | color: white;
86 | display: flex;
87 | width:58%;
88 | align-items: center;
89 | justify-content: space-between;
90 | border: 3px solid white;
91 | border-radius: 10px;
92 | background:inherit;
93 | @media (max-width: 1000px){
94 | padding: 5px 5px;
95 | font-size:12px;
96 | };
97 | @media (max-width: 400px){
98 | font-size:10px;
99 | border: 1px solid white;
100 | padding: 8px 8px;
101 | };
102 |
103 | &:hover{
104 | cursor: pointer;
105 | background-color: white;
106 | color: black;
107 | text-decoration: none;
108 | width: 70%;
109 | }
110 | }
111 | }
112 |
113 | `
114 |
115 |
116 |
117 | export const StyledLink = styled(Link)`
118 | text-decoration: none;
119 | color: inherit;
120 | `
--------------------------------------------------------------------------------
/client/src/pages/UserList/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { deleteUser, getUserList } from '../../actions/userActions';
4 | import { Container, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@material-ui/core';
5 | import { Alert } from '@material-ui/lab';
6 | import CheckIcon from '@material-ui/icons/Check';
7 | import ClearIcon from '@material-ui/icons/Clear';
8 | import Loader from '../../components/Loader/Loader';
9 | import { Delete } from './UserList.elements';
10 | import Meta from '../../components/Meta';
11 |
12 | const UserList = ({history}) => {
13 |
14 | const currentUser = useSelector( state => state.currentUser);
15 | const { userInfo } = currentUser;
16 |
17 | const userList = useSelector( state => state.userList);
18 | const { users, loading, error } = userList;
19 |
20 | const dispatch = useDispatch();
21 |
22 | const deleteHandler = (id) => {
23 | if(window.confirm('Are you sure')){
24 | dispatch(deleteUser(id));
25 | dispatch(getUserList())
26 | }
27 |
28 | }
29 |
30 | useEffect(() => {
31 |
32 | if(userInfo && userInfo.isAdmin){
33 | dispatch(getUserList())
34 | }else{
35 | history.push('/login')
36 | }
37 |
38 | }, [dispatch, history, userInfo])
39 |
40 | return (
41 |
42 |
43 |
44 | {
45 | loading ? : error ? {error} : (
46 | <>
47 | Users
48 |
49 |
50 |
51 |
52 | ID
53 | Name
54 | Email
55 | Admin
56 |
57 |
58 |
59 |
60 | {users.map( user => (
61 |
62 | {user._id}
63 | {user.name}
64 | {user.email}
65 | {user.isAdmin ? : }
66 | deleteHandler(user._id)}/>
67 |
68 | ))}
69 |
70 |
71 |
72 | >
73 | )
74 | }
75 |
76 |
77 | )
78 | }
79 |
80 | export default UserList
81 |
--------------------------------------------------------------------------------
/client/src/components/GenreSelector/GenreSelector.elements.js:
--------------------------------------------------------------------------------
1 | import { Grid } from '@material-ui/core';
2 | import styled from 'styled-components';
3 |
4 | export const GridContainer = styled(Grid)`
5 | width: 80%;
6 | margin: 30px auto;
7 | @media (max-width: 1100px){
8 | width: 90vw;
9 | };
10 | @media (max-width: 400px){
11 | margin: 10px auto;
12 | };
13 |
14 | & .genre{
15 | height: 90px;
16 | width: 230px;
17 | margin: 0 auto;
18 | border-radius: 15px;
19 | display: grid;
20 | place-items: center;
21 | transition: all .3s ease-in-out;
22 | & h3{
23 | color: #ffffff;
24 | text-transform: uppercase;
25 | transition: all .3s ease-in-out;
26 | };
27 | &:hover{
28 | cursor: pointer;
29 | };
30 | @media (max-width: 1300px){
31 | height: 90px;
32 | width: 200px;
33 | };
34 | @media (max-width: 1100px){
35 | height: 75px;
36 | width: 190px;
37 | };
38 | @media (max-width: 880px){
39 | height: 60px;
40 | width: 145px;
41 | & h3{
42 | font-size: 0.9rem;
43 | };
44 | };
45 | };
46 | & .thriller{
47 | background-color: #8C3AC7;
48 | &:hover{
49 | & h3{
50 | color: #8C3AC7;
51 | transform: scale(1.3);
52 | };
53 | background-color: inherit;
54 | };
55 | };
56 | & .romance{
57 | background-color: #E95C70;
58 | &:hover{
59 | & h3{
60 | color: #E95C70;
61 | transform: scale(1.3);
62 | };
63 | background-color: #ffffff;
64 | };
65 | };
66 | & .young-adult{
67 | background-color: #EF6A3A;
68 | &:hover{
69 | & h3{
70 | color: #EF6A3A;
71 | transform: scale(1.3);
72 | };
73 | background-color: #ffffff;
74 | };
75 | };
76 | & .science-fiction{
77 | background-color: #EFD140;
78 | &:hover{
79 | & h3{
80 | color: #EFD140;
81 | transform: scale(1.3);
82 | };
83 | background-color: #ffffff;
84 | };
85 | };
86 | & .fantasy{
87 | background-color: #2BA3BC;
88 | &:hover{
89 | & h3{
90 | color: #2BA3BC;
91 | transform: scale(1.3);
92 | };
93 | background-color: #ffffff;
94 | };
95 | };
96 | & .poetry{
97 | background-color: #93C548;
98 | &:hover{
99 | & h3{
100 | color: #93C548;
101 | transform: scale(1.3);
102 | };
103 | background-color: #ffffff;
104 | };
105 | };
106 | & .biography{
107 | background-color: #7F8B93;
108 | &:hover{
109 | & h3{
110 | color: #7F8B93;
111 | transform: scale(1.3);
112 | };
113 | background-color: #ffffff;
114 | };
115 | };
116 | & .self-help{
117 | background-color: #517DDF;
118 | &:hover{
119 | & h3{
120 | color: #517DDF;
121 | transform: scale(1.3);
122 | };
123 | background-color: #ffffff;
124 | };
125 | };
126 | `
127 |
128 |
129 |
--------------------------------------------------------------------------------
/client/src/pages/OrderList/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { Container, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@material-ui/core';
4 | import { Alert } from '@material-ui/lab';
5 | import Loader from '../../components/Loader/Loader';
6 | import ClearIcon from '@material-ui/icons/Clear';
7 | import { StyledTableRow } from './OrderList.elements';
8 | import { getOrders } from '../../actions/orderActions';
9 | import Meta from '../../components/Meta';
10 |
11 |
12 | const OrderList = ({history}) => {
13 |
14 | const currentUser = useSelector( state => state.currentUser);
15 | const { userInfo } = currentUser;
16 |
17 | const orderList = useSelector( state => state.orderList);
18 | const { orders, loading, error } = orderList;
19 |
20 |
21 | const dispatch = useDispatch();
22 |
23 | useEffect(() => {
24 |
25 | if(!userInfo || !userInfo.isAdmin){
26 | history.push('/login')
27 | }
28 |
29 | dispatch(getOrders())
30 | }, [dispatch, history, userInfo])
31 |
32 | return (
33 |
34 |
35 |
36 | {
37 | loading ? : error ? {error} : (
38 | <>
39 | Orders
40 |
41 |
42 |
43 |
44 | ID
45 | USER
46 | DATE
47 | TOTAL
48 | PAID
49 | DELIVERED
50 |
51 |
52 |
53 | {orders.map( order => (
54 | {history.push(`/order/${order._id}`)}}
57 | >
58 | {order._id}
59 | {order.user.name}
60 | {order.createdAt.slice(0,10)}
61 | ${order.totalPrice}
62 | {order.isPaid ? order.paidAt.slice(0,10) : }
63 | { order.isDelivered ? order.deliveredAt.slice(0,10) : }
64 |
65 | ))}
66 |
67 |
68 |
69 | >
70 | )
71 | }
72 |
73 |
74 | )
75 | }
76 |
77 | export default OrderList;
78 |
--------------------------------------------------------------------------------
/client/src/actions/types.js:
--------------------------------------------------------------------------------
1 | const types = {
2 | FETCH_PRODUCTSLIST_REQUEST: 'FETCH_PRODUCTSLIST_REQUEST',
3 | FETCH_PRODUCTSLIST_SUCCESS: 'FETCH_PRODUCTSLIST_SUCCESS',
4 | FETCH_PRODUCTSLIST_FAIL: 'FETCH_PRODUCTSLIST_FAIL',
5 | FETCH_PRODUCT_REQUEST: 'FETCH_PRODUCT_REQUEST',
6 | FETCH_PRODUCT_SUCCESS: 'FETCH_PRODUCT_SUCCESS',
7 | FETCH_PRODUCT_FAIL: 'FETCH_PRODUCT_FAIL',
8 | CART_ADD_ITEM: 'CART_ADD_ITEM',
9 | CART_REMOVE_ITEM: 'CART_REMOVE_ITEM',
10 | CART_CLEAR: 'CART_CLEAR',
11 | USER_LOGIN_REQUEST: 'USER_LOGIN_REQUEST',
12 | USER_LOGIN_SUCCESS: 'USER_LOGIN_SUCCESS',
13 | USER_LOGIN_FAIL: 'USER_LOGIN_FAIL',
14 | USER_LOGOUT: 'USER_LOGOUT',
15 | USER_REGISTER_REQUEST: 'USER_REGISTER_REQUEST',
16 | USER_REGISTER_SUCCESS: 'USER_REGISTER_SUCCESS',
17 | USER_REGISTER_FAIL: 'USER_REGISTER_FAIL',
18 | USER_DETAILS_REQUEST: 'USER_DETAILS_REQUEST',
19 | USER_DETAILS_SUCCESS: 'USER_DETAILS_SUCCESS',
20 | USER_DETAILS_FAIL: 'USER_DETAILS_FAIL',
21 | USER_UPDATE_PROFILE_REQUEST: 'USER_UPDATE_PROFILE_REQUEST',
22 | USER_UPDATE_PROFILE_SUCCESS: 'USER_UPDATE_PROFILE_SUCCESS',
23 | USER_UPDATE_PROFILE_FAIL: 'USER_UPDATE_PROFILE_FAIL',
24 | USER_UPDATE_PROFILE_RESET: 'USER_UPDATE_PROFILE_RESET',
25 | CART_SAVE_SHIPPING_ADDRESS: 'CART_SAVE_SHIPPING_ADDRESS',
26 | CART_SAVE_PAYMENT_METHOD: 'CART_SAVE_PAYMENT_METHOD',
27 | ORDER_CREATE_REQUEST: 'ORDER_CREATE_REQUEST',
28 | ORDER_CREATE_SUCCESS: 'ORDER_CREATE_SUCCESS',
29 | ORDER_CREATE_FAIL: 'ORDER_CREATE_FAIL',
30 | ORDER_CREATE_RESET: 'ORDER_CREATE_RESET',
31 | ORDER_DETAILS_REQUEST: 'ORDER_DETAILS_REQUEST',
32 | ORDER_DETAILS_SUCCESS: 'ORDER_DETAILS_SUCCESS',
33 | ORDER_DETAILS_FAIL: 'ORDER_DETAILS_FAIL',
34 | ORDER_PAY_REQUEST: 'ORDER_PAY_REQUEST',
35 | ORDER_PAY_SUCCESS: 'ORDER_PAY_SUCCESS',
36 | ORDER_PAY_FAIL: 'ORDER_PAY_FAIL',
37 | ORDER_PAY_RESET: 'ORDER_PAY_RESET',
38 | ORDER_DELIVER_REQUEST: 'ORDER_DELIVER_REQUEST',
39 | ORDER_DELIVER_SUCCESS: 'ORDER_DELIVER_SUCCESS',
40 | ORDER_DELIVER_FAIL: 'ORDER_DELIVER_FAIL',
41 | ORDER_DELIVER_RESET: 'ORDER_DELIVER_RESET',
42 | ORDER_LIST_REQUEST: 'ORDER_LIST_REQUEST',
43 | ORDER_LIST_SUCCESS: 'ORDER_LIST_SUCCESS',
44 | ORDER_LIST_FAIL: 'ORDER_LIST_FAIL',
45 | ORDER_LIST_RESET: 'ORDER_LIST_RESET',
46 | USER_LIST_REQUEST: 'USER_LIST_REQUEST',
47 | USER_LIST_SUCCESS: 'USER_LIST_SUCCESS',
48 | USER_LIST_FAIL: 'USER_LIST_FAIL',
49 | USER_LIST_RESET: 'USER_LIST_RESET',
50 | USER_DELETE_REQUEST: 'USER_DELETE_REQUEST',
51 | USER_DELETE_SUCCESS: 'USER_DELETE_SUCCESS',
52 | USER_DELETE_FAIL: 'USER_DELETE_FAIL',
53 | PRODUCT_DELETE_REQUEST: 'PRODUCT_DELETE_REQUEST',
54 | PRODUCT_DELETE_SUCCESS: 'PRODUCT_DELETE_SUCCESS',
55 | PRODUCT_DELETE_FAIL: 'PRODUCT_DELETE_FAIL',
56 | PRODUCT_CREATE_REQUEST: 'PRODUCT_CREATE_REQUEST',
57 | PRODUCT_CREATE_SUCCESS: 'PRODUCT_CREATE_SUCCESS',
58 | PRODUCT_CREATE_FAIL: 'PRODUCT_CREATE_FAIL',
59 | PRODUCT_CREATE_RESET: 'PRODUCT_CREATE_RESET',
60 | PRODUCT_UPDATE_REQUEST: 'PRODUCT_UPDATE_REQUEST',
61 | PRODUCT_UPDATE_SUCCESS: 'PRODUCT_UPDATE_SUCCESS',
62 | PRODUCT_UPDATE_FAIL: 'PRODUCT_UPDATE_FAIL',
63 | PRODUCT_UPDATE_RESET: 'PRODUCT_UPDATE_RESET',
64 | ORDERS_LIST_ADMIN_REQUEST: 'ORDERS_LIST_ADMIN_REQUEST',
65 | ORDERS_LIST_ADMIN_SUCCESS: 'ORDERS_LIST_ADMIN_SUCCESS',
66 | ORDERS_LIST_ADMIN_FAIL: 'ORDERS_LIST_ADMIN_FAIL',
67 | ORDERS_LIST_ADMIN_RESET: 'ORDERS_LIST_ADMIN_RESET',
68 | PRODUCT_CREATE_REVIEW_REQUEST: 'PRODUCT_CREATE_REVIEW_REQUEST',
69 | PRODUCT_CREATE_REVIEW_SUCCESS: 'PRODUCT_CREATE_REVIEW_SUCCESS',
70 | PRODUCT_CREATE_REVIEW_FAIL: 'PRODUCT_CREATE_REVIEW_FAIL',
71 | PRODUCT_CREATE_REVIEW_RESET: 'PRODUCT_CREATE_REVIEW_RESET'
72 | };
73 |
74 | export default types;
--------------------------------------------------------------------------------
/client/src/reducers/orderReducers.js:
--------------------------------------------------------------------------------
1 | import types from "../actions/types";
2 |
3 | export const orderCreateReducer = (state = {}, action) => {
4 |
5 | switch (action.type) {
6 | case types.ORDER_CREATE_REQUEST:
7 | return {
8 | loading: true
9 | }
10 | case types.ORDER_CREATE_SUCCESS:
11 | return {
12 | loading: false,
13 | success: true,
14 | order: action.payload
15 | }
16 | case types.ORDER_CREATE_FAIL:
17 | return {
18 | loading: false,
19 | error: action.payload
20 | }
21 | case types.ORDER_CREATE_RESET:
22 | return {};
23 | default:
24 | return state;
25 | }
26 | };
27 |
28 | export const orderDetailsReducer = (state = { loading: true, orderItems: [], shippingAddress: {} }, action) => {
29 |
30 | switch (action.type) {
31 | case types.ORDER_DETAILS_REQUEST:
32 | return {
33 | ...state,
34 | loading: true
35 | }
36 | case types.ORDER_DETAILS_SUCCESS:
37 | return {
38 | loading: false,
39 | order: action.payload
40 | }
41 | case types.ORDER_DETAILS_FAIL:
42 | return {
43 | loading: false,
44 | error: action.payload
45 | }
46 | default:
47 | return state;
48 | }
49 | };
50 |
51 | export const orderPayReducer = (state = {}, action) => {
52 |
53 | switch (action.type) {
54 | case types.ORDER_PAY_REQUEST:
55 | return {
56 | loading: true
57 | }
58 | case types.ORDER_PAY_SUCCESS:
59 | return {
60 | loading: false,
61 | success: true
62 | }
63 | case types.ORDER_PAY_FAIL:
64 | return {
65 | loading: false,
66 | error: action.payload
67 | }
68 | case types.ORDER_PAY_RESET:
69 | return {}
70 | default:
71 | return state;
72 | }
73 | };
74 |
75 |
76 | export const orderListReducer = (state = {orders:[]}, action) => {
77 |
78 | switch (action.type) {
79 | case types.ORDER_LIST_REQUEST:
80 | return {
81 | loading: true
82 | }
83 | case types.ORDER_LIST_SUCCESS:
84 | return {
85 | loading: false,
86 | orders: action.payload
87 | }
88 | case types.ORDER_LIST_FAIL:
89 | return {
90 | loading: false,
91 | error: action.payload
92 | }
93 | case types.ORDER_LIST_RESET:
94 | return { orders: []}
95 | default:
96 | return state;
97 | }
98 | };
99 |
100 | export const orderListAdminReducer = (state={orders: []}, action) => {
101 |
102 | switch (action.type) {
103 | case types.ORDERS_LIST_ADMIN_REQUEST:
104 | return { loading: true }
105 | case types.ORDERS_LIST_ADMIN_SUCCESS:
106 | return { loading: false, orders: action.payload }
107 | case types.ORDERS_LIST_ADMIN_FAIL:
108 | return { loading: false, error: action.payload }
109 | case types.ORDERS_LIST_ADMIN_RESET:
110 | return { orders: [] }
111 | default:
112 | return state
113 | }
114 | };
115 |
116 | export const orderDeliverReducer = (state = {}, action) => {
117 |
118 | switch (action.type) {
119 | case types.ORDER_DELIVER_REQUEST:
120 | return {
121 | loading: true
122 | }
123 | case types.ORDER_DELIVER_SUCCESS:
124 | return {
125 | loading: false,
126 | success: true
127 | }
128 | case types.ORDER_DELIVER_FAIL:
129 | return {
130 | loading: false,
131 | error: action.payload
132 | }
133 | case types.ORDER_DELIVER_RESET:
134 | return {}
135 | default:
136 | return state;
137 | }
138 | };
--------------------------------------------------------------------------------
/server/controllers/userController.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from 'express-async-handler'
2 | import User from '../models/userModel.js';
3 | import bcrypt from 'bcryptjs';
4 | import generateToken from '../utils/generateToken.js'
5 |
6 |
7 | // @desc Auth user, get token
8 | // @route POST /api/users/login
9 | // @access Public
10 | export const authUser = asyncHandler( async (req,res) => {
11 | const {email, password} = req.body;
12 | const user = await User.findOne({email});
13 |
14 | if(user){
15 | const passwordCorrect = await bcrypt.compare(password,user.password);
16 | if(passwordCorrect){
17 | res.json({
18 | _id: user._id,
19 | name: user.name,
20 | email: user.email,
21 | isAdmin: user.isAdmin,
22 | token: generateToken(user._id)
23 | })
24 | }else{
25 | res.status(401)
26 | throw new Error('Invalid email or password')
27 | }
28 |
29 | } else {
30 | res.status(401)
31 | throw new Error('Invalid email or password')
32 | }
33 |
34 | });
35 |
36 | //@desc Get user profile
37 | //@route GET /api/users/profile
38 | //@access Private
39 |
40 | export const getUserProfile = asyncHandler( async(req,res) => {
41 | const user = await User.findById(req.user._id);
42 | if(user){
43 | res.json({
44 | _id: user._id,
45 | name: user.name,
46 | email: user.email,
47 | isAdmin: user.isAdmin
48 | })
49 | } else {
50 | res.status(404)
51 | throw new Error('User not found')
52 | }
53 | res.send('Success')
54 | });
55 |
56 |
57 | //@desc Update user profile
58 | //@route PUT /api/users
59 | //@access Private
60 |
61 | export const updateUserProfile = asyncHandler( async (req,res) => {
62 |
63 | const user = await User.findById(req.user._id);
64 |
65 | if(user) {
66 | user.name = req.body.name || user.name;
67 | user.email = req.body.email || user.email;
68 |
69 | if(req.body.password){
70 | user.password = req.body.password || user.password
71 | }
72 |
73 | const updatedUser = await user.save();
74 |
75 | res.json({
76 | _id: updatedUser._id,
77 | name: updatedUser.name,
78 | email: updatedUser.email,
79 | isAdmin: updatedUser.isAdmin,
80 | token: generateToken(user._id)
81 | })
82 | } else {
83 | res.status(404)
84 | throw new Error('User not found')
85 | }
86 | });
87 |
88 | //@desc Register new User
89 | //@route POST /api/users
90 | //@access Public
91 |
92 | export const registerUser = asyncHandler(async (req,res) => {
93 | const {name, email, password} = req.body;
94 | const userExists = await User.findOne({email});
95 | if(userExists){
96 | res.status(400)
97 | throw new Error('User already exists')
98 | }
99 |
100 | const user = await User.create({
101 | name,
102 | email,
103 | password
104 | })
105 |
106 | if(user){
107 | res.status(201)
108 | res.json(
109 | {_id: user._id,
110 | name: user.name,
111 | email: user.email,
112 | isAdmin: user.isAdmin,
113 | token: generateToken(user._id)}
114 | )
115 | }else{
116 | res.status(400)
117 | throw new Error('Invalid User Data')
118 | }
119 | });
120 |
121 | //@desc Get User List
122 | //@route GET /api/users
123 | //@access Private/Admin
124 |
125 | export const getUsers = asyncHandler( async( req, res )=> {
126 | const users = await User.find({})
127 | res.json(users)
128 | });
129 |
130 | //@desc Delete User
131 | //@route DELETE /api/users/:id
132 | //@access Private/Admin
133 |
134 | export const deleteUser = asyncHandler( async( req, res )=> {
135 | const user = await User.findById(req.params.id)
136 | if(user){
137 | await user.remove()
138 | res.json({ message: 'User Deleted' })
139 | }else{
140 | res.status(404);
141 | throw new Error('User Not Found')
142 | }
143 | res.json(users)
144 | });
145 |
--------------------------------------------------------------------------------
/client/src/pages/PlaceOrder/index.js:
--------------------------------------------------------------------------------
1 | import { Button, Card, CardContent, Container, Divider, Grid } from '@material-ui/core';
2 | import { Alert } from '@material-ui/lab';
3 | import React, { useEffect } from 'react';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { createOrder } from '../../actions/orderActions';
6 | import Meta from '../../components/Meta';
7 | import StepperNav from '../../components/StepperNav'
8 | import { OrderItem, ShippingMessage, SummaryItem } from './PlaceOrder.elements';
9 |
10 | const PlaceOrder = ({history}) => {
11 |
12 | const cart = useSelector( state => state.cart );
13 |
14 | cart.itemsPrice = cart.cartItems.reduce( (acc,item) => acc + item.price * item.qty , 0).toFixed(2)
15 |
16 | cart.shippingPrice = cart.itemsPrice > 50 ? 0.00 : 10.00
17 |
18 | cart.totalPrice = Number(cart.itemsPrice + cart.shippingPrice).toFixed(2);
19 |
20 | const orderCreate = useSelector(state => state.orderCreate)
21 |
22 | const { order, success, error } = orderCreate;
23 |
24 | useEffect(() => {
25 | if(success){
26 | history.push(`/order/${order._id}`)
27 | }
28 | // eslint-disable-next-line
29 | }, [history, success])
30 |
31 | const dispatch = useDispatch();
32 | const handleSubmit = () => {
33 | dispatch(createOrder({
34 | orderItems: cart.cartItems,
35 | shippingAddress: cart.shippingAddress,
36 | paymentMethod: cart.paymentMethod,
37 | itemsPrice: cart.itemsPrice,
38 | shippingPrice: cart.shippingPrice,
39 | totalPrice: cart.totalPrice
40 | }))
41 | }
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
49 |
50 | SHIPPING
51 |
52 | Address:
53 | {`${cart.shippingAddress.address}, ${cart.shippingAddress.city}, ${cart.shippingAddress.postalCode} ${cart.shippingAddress.country}`
54 | }
55 |
56 |
57 | PAYMENT
58 |
59 | Method:
60 | {cart.paymentMethod}
61 |
62 |
63 | Order Items
64 | {
65 | cart.cartItems.length === 0 ? Your Cart is Empty :
66 |
67 | cart.cartItems.map( item => (
68 |
69 |
70 |
71 | {item.name}
72 | {`${item.qty} x ${item.price} = ${item.qty*item.price}`}
73 |
74 |
75 |
76 | ))
77 | }
78 |
79 |
80 |
81 |
82 |
83 | ORDER SUMMARY
84 |
85 | Items: ${cart.itemsPrice}
86 | Shipping: ${cart.shippingPrice}
87 | Total: ${cart.totalPrice}
88 | (Orders above $50 have free shipping)
89 | {error ? {error} : <>>}
90 |
91 | PLACE ORDER
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | )
100 | }
101 |
102 | export default PlaceOrder;
103 |
--------------------------------------------------------------------------------
/client/src/pages/Register/index.js:
--------------------------------------------------------------------------------
1 | import { Button, Container, Paper, TextField } from '@material-ui/core';
2 | import { Alert } from '@material-ui/lab';
3 | import React, { useState, useEffect, useRef } from 'react';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { Link } from 'react-router-dom';
6 | import { registerUser } from '../../actions/userActions'
7 | import { FormContainer, RegisterContainer, ImgContainer } from './Register.elements';
8 | import { TweenMax, Power3 } from 'gsap';
9 | import Meta from '../../components/Meta';
10 |
11 | const RegisterPage = ({location, history}) => {
12 |
13 | const redirect = location.search ? location.search.split('=')[1] : '/';
14 |
15 | const dispatch = useDispatch();
16 |
17 | const registeredUser = useSelector(state => state.registeredUser);
18 |
19 | const {loading, error, userInfo } = registeredUser;
20 |
21 |
22 | const submitHandler = (e) => {
23 | e.preventDefault();
24 | if(password !== confirmPassword){
25 | setMessage('Passwords do not match')
26 | } else {
27 | dispatch(registerUser(name, email, password))
28 | }
29 |
30 | }
31 |
32 | const [name, setName] = useState('');
33 | const [email, setEmail] = useState('');
34 | const [password, setPassword] = useState('');
35 | const [confirmPassword, setConfirmPassword] = useState('');
36 | const [message, setMessage] = useState(null)
37 |
38 | //refs for GSAP
39 | let formRef = useRef(null);
40 | let imgRef = useRef(null);
41 |
42 | useEffect(() => {
43 |
44 | TweenMax.from(formRef, 1 , {opacity: 0, y: 30, ease: Power3.easeOut});
45 | TweenMax.from(imgRef, 1 , {opacity: 0, x: 50, ease: Power3.easeOut, delay: 0.2});
46 | if(userInfo){
47 | history.push(redirect)
48 | }
49 | }, [history, userInfo, redirect])
50 |
51 | return (
52 |
53 |
54 |
55 |
56 | formRef = el}>
57 | NEW USER
58 | {message && {message} }
59 | {error && {error} }
60 | {loading && {error} }
61 |
99 |
100 | Already have an account? Login Here
101 |
102 |
103 | imgRef = el} src={process.env.PUBLIC_URL + '/icons/illustration2.jpg'} alt="img" />
104 |
105 |
106 |
107 |
108 | )
109 | }
110 |
111 | export default RegisterPage
112 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Varela+Round&display=swap');
2 |
3 | html, body {
4 | box-sizing: border-box;
5 | margin: 0;
6 | padding: 0;
7 | font-family: 'Varela Round', sans-serif;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | min-height: 100vh;
11 | position: relative;
12 | }
13 |
14 | ::-webkit-scrollbar
15 | {
16 | width: 10px; /* for vertical scrollbars */
17 | height: 10px; /* for horizontal scrollbars */
18 | }
19 |
20 |
21 | ::-webkit-scrollbar-thumb
22 | {
23 | background: rgba(99, 99, 99, 0.568);
24 | border-radius: 25px;
25 | }
26 |
27 | ::-webkit-scrollbar-thumb:hover{
28 | background-color: rgba(54, 54, 54, 0.651) ;
29 | }
30 |
31 |
32 | code {
33 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
34 | monospace;
35 | }
36 |
37 | main {
38 | margin-bottom: 100px;
39 | min-height: 80vh;
40 | }
41 |
42 | .App{
43 | display: flex;
44 | flex-direction: column;
45 | justify-content: space-between;
46 | /* background: linear-gradient(155deg, #2200FF, #8472FF, #FFFFFF);
47 | background-size: 600% 600%;
48 | animation: GradientBackground 8s ease infinite; */
49 |
50 | }
51 |
52 |
53 | .home-main{
54 | background: linear-gradient( 114deg, #bdb4fa, #7965fd,#3113f8 );
55 | background-size: 600% 600%;
56 | animation: GradientBackground 10s ease infinite;
57 | margin: -70px auto 0 auto;
58 | padding: 145px 0 0 0;
59 | height: 400px;
60 | }
61 |
62 | .back-blur{
63 | content: ' ';
64 | position: relative;
65 | top: 0;
66 | pointer-events: none;
67 | left: 0;
68 | width: 100%;
69 | height: 100.2%;
70 | background: linear-gradient(0deg, rgba(255,255,255,1) 0%,rgba(255, 255, 255, 0) 25%);
71 | }
72 |
73 | .genre-page{
74 | /* background: linear-gradient( 114deg, #bdb4fa, #7965fd,#3113f8 ); */
75 | /* background-size: 600% 600%;
76 | animation: GradientBackground 12s ease infinite; */
77 | margin: -70px auto 0 auto;
78 | padding: 125px 0 0 0;
79 | height: 250px;
80 | }
81 |
82 | .romance-page{
83 | background: linear-gradient(114deg, #e4a7af, #E95C70, #E71D3A);
84 | background-size: 600% 600%;
85 | animation: GradientBackground 10s ease infinite;
86 | }
87 |
88 | .thriller-page{
89 | background: linear-gradient(114deg, #b899ce, #8F3FC8, #7E13CA);
90 | background-size: 600% 600%;
91 | animation: GradientBackground 10s ease infinite;
92 | }
93 |
94 | .young-adult-page{
95 | background: linear-gradient(93deg, #e9bcac, #EA7C54, #E8521B);
96 | background-size: 600% 600%;
97 | animation: GradientBackground 10s ease infinite;
98 | }
99 |
100 | .science-fiction-page{
101 | background: linear-gradient(80deg, #dfcf84, #EAD25D, #E8C51B);
102 | background-size: 600% 600%;
103 | animation: GradientBackground 10s ease infinite;
104 | }
105 |
106 | .fantasy-page{
107 | background: linear-gradient(80deg, #9fc0c7, #45ACC1, #0FA7C6);
108 | background-size: 600% 600%;
109 | animation: GradientBackground 10s ease infinite;
110 | }
111 |
112 | .poetry-page{
113 | background: linear-gradient(80deg, #b6c0a7, #95C153, #7EC514);
114 | background-size: 600% 600%;
115 | animation: GradientBackground 10s ease infinite;
116 | }
117 |
118 | .biography-page{
119 | background: linear-gradient(80deg, #B6B6B6, #7F8B93, #576670);
120 | background-size: 600% 600%;
121 | animation: GradientBackground 10s ease infinite;
122 | }
123 |
124 | .self-help-page{
125 | background: linear-gradient(80deg, #a4b7e0, #5780DC, #1B54D3);
126 | background-size: 600% 600%;
127 | animation: GradientBackground 10s ease infinite;
128 | }
129 |
130 | .back-blur-genre {
131 | content: ' ';
132 | position: relative;
133 | pointer-events: none;
134 | left: 0;
135 | width: 100%;
136 | height: 100.3%;
137 | background: linear-gradient(0deg, rgba(255,255,255,1) 0%,rgba(255, 255, 255, 0) 25%);
138 | }
139 |
140 |
141 | @keyframes GradientBackground {
142 | 0% {
143 | background-position: 0% 50%;
144 | }
145 |
146 | 50% {
147 | background-position: 100% 50%;
148 | }
149 |
150 | 100% {
151 | background-position: 0% 50%;
152 | }
153 | }
154 |
155 | footer{
156 | position: absolute;
157 | display: flex;
158 | align-items: center;
159 | justify-content: center;
160 | bottom: 0;
161 | left:0;
162 | height: 100px;
163 | width: 100%;
164 | }
165 |
166 | @media (max-width: 500px){
167 | .home-main{
168 | margin-top: -65px;
169 | padding: 100px 0 0 0;
170 | height: 55vh;
171 | }
172 | }
173 |
174 | @media (max-height: 800px) and (orientation: portrait){
175 |
176 | .home-main{
177 | height: 450px;
178 | }
179 | }
--------------------------------------------------------------------------------
/client/src/actions/productActions.js:
--------------------------------------------------------------------------------
1 | import types from './types';
2 | import axios from 'axios';
3 |
4 | export const fetchProductsList = (page = '') => async(dispatch) => {
5 | try {
6 | dispatch({type: types.FETCH_PRODUCTSLIST_REQUEST})
7 |
8 | const {data} = await axios.get(`/api/products?page=${page}`);
9 |
10 | dispatch({
11 | type: types.FETCH_PRODUCTSLIST_SUCCESS,
12 | payload: data
13 | })
14 | } catch (error) {
15 | dispatch({
16 | type: types.FETCH_PRODUCTSLIST_FAIL,
17 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
18 | })
19 | }
20 | };
21 |
22 |
23 | export const fetchProduct = (id) => async (dispatch) => {
24 | try {
25 | dispatch({ type:types.FETCH_PRODUCT_REQUEST })
26 |
27 | const {data} = await axios.get(`/api/products/${id}`);
28 |
29 | dispatch({
30 | type: types.FETCH_PRODUCT_SUCCESS,
31 | payload: data
32 | })
33 | } catch (error) {
34 | dispatch({
35 | type: types.FETCH_PRODUCT_FAIL,
36 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
37 | })
38 | }
39 | }
40 |
41 | export const deleteProductAdmin = (id) => async(dispatch, getState) => {
42 |
43 | try {
44 | dispatch({ type: types.PRODUCT_DELETE_REQUEST });
45 |
46 | const {currentUser: {userInfo}} = getState();
47 |
48 | const config ={
49 | headers: {
50 | Authorization: `Bearer ${userInfo.token}`
51 | }
52 | }
53 |
54 | await axios.delete(`/api/products/${id}`, config);
55 |
56 | dispatch({ type: types.PRODUCT_DELETE_SUCCESS });
57 |
58 | } catch (error) {
59 | dispatch({
60 | type: types.PRODUCT_DELETE_FAIL,
61 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
62 | })
63 | }
64 | };
65 |
66 | export const createProductAdmin = () => async(dispatch, getState) => {
67 |
68 | try {
69 | dispatch({ type: types.PRODUCT_CREATE_REQUEST });
70 |
71 | const {currentUser: {userInfo}} = getState();
72 |
73 | const config ={
74 | headers: {
75 | Authorization: `Bearer ${userInfo.token}`
76 | }
77 | }
78 |
79 | const { data } = await axios.post(`/api/products`,{}, config);
80 |
81 | dispatch({
82 | type: types.PRODUCT_CREATE_SUCCESS,
83 | payload: data
84 | });
85 |
86 | } catch (error) {
87 | dispatch({
88 | type: types.PRODUCT_CREATE_FAIL,
89 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
90 | })
91 | }
92 | };
93 |
94 | export const updateProductAdmin = (product) => async(dispatch, getState) => {
95 |
96 | try {
97 | dispatch({ type: types.PRODUCT_UPDATE_REQUEST });
98 |
99 | const {currentUser: {userInfo}} = getState();
100 |
101 | const config ={
102 | headers: {
103 | 'Cntent-Type': 'application/json',
104 | Authorization: `Bearer ${userInfo.token}`
105 | }
106 | }
107 |
108 | const { data } = await axios.put(`/api/products/${product._id}`,product, config);
109 |
110 | dispatch({
111 | type: types.PRODUCT_UPDATE_SUCCESS,
112 | payload: data
113 | });
114 |
115 | } catch (error) {
116 | dispatch({
117 | type: types.PRODUCT_UPDATE_FAIL,
118 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
119 | })
120 | }
121 | };
122 |
123 | export const createProductReview = (productId, review) => async(dispatch, getState) => {
124 |
125 | try {
126 | dispatch({ type: types.PRODUCT_CREATE_REVIEW_REQUEST });
127 |
128 | const {currentUser: {userInfo}} = getState();
129 |
130 | const config ={
131 | headers: {
132 | 'Cntent-Type': 'application/json',
133 | Authorization: `Bearer ${userInfo.token}`
134 | }
135 | }
136 |
137 | await axios.post(`/api/products/${productId}/reviews`, review , config);
138 |
139 | dispatch({
140 | type: types.PRODUCT_CREATE_REVIEW_SUCCESS
141 | });
142 |
143 | } catch (error) {
144 | dispatch({
145 | type: types.PRODUCT_CREATE_REVIEW_FAIL,
146 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
147 | })
148 | }
149 | };
--------------------------------------------------------------------------------
/client/src/components/Loader/Loader.css:
--------------------------------------------------------------------------------
1 | .loader {
2 | --background: linear-gradient(135deg, #23C4F8, #275EFE);
3 | --shadow: rgba(39, 94, 254, 0.28);
4 | --text: #6C7486;
5 | --page: rgba(255, 255, 255, 0.36);
6 | --page-fold: rgba(255, 255, 255, 0.52);
7 | --duration: 3s;
8 | width: 200px;
9 | height: 140px;
10 | position: relative;
11 | margin: 100px auto;
12 | }
13 | .loader:before, .loader:after {
14 | --r: -6deg;
15 | content: "";
16 | position: absolute;
17 | bottom: 8px;
18 | width: 120px;
19 | top: 80%;
20 | box-shadow: 0 16px 12px var(--shadow);
21 | transform: rotate(var(--r));
22 | }
23 | .loader:before {
24 | left: 4px;
25 | }
26 | .loader:after {
27 | --r: 6deg;
28 | right: 4px;
29 | }
30 | .loader div {
31 | width: 100%;
32 | height: 100%;
33 | border-radius: 13px;
34 | position: relative;
35 | z-index: 1;
36 | perspective: 600px;
37 | box-shadow: 0 4px 6px var(--shadow);
38 | background-image: var(--background);
39 | }
40 | .loader div ul {
41 | margin: 0;
42 | padding: 0;
43 | list-style: none;
44 | position: relative;
45 | }
46 | .loader div ul li {
47 | --r: 180deg;
48 | --o: 0;
49 | --c: var(--page);
50 | position: absolute;
51 | top: 10px;
52 | left: 10px;
53 | transform-origin: 100% 50%;
54 | color: var(--c);
55 | opacity: var(--o);
56 | transform: rotateY(var(--r));
57 | -webkit-animation: var(--duration) ease infinite;
58 | animation: var(--duration) ease infinite;
59 | }
60 | .loader div ul li:nth-child(2) {
61 | --c: var(--page-fold);
62 | -webkit-animation-name: page-2;
63 | animation-name: page-2;
64 | }
65 | .loader div ul li:nth-child(3) {
66 | --c: var(--page-fold);
67 | -webkit-animation-name: page-3;
68 | animation-name: page-3;
69 | }
70 | .loader div ul li:nth-child(4) {
71 | --c: var(--page-fold);
72 | -webkit-animation-name: page-4;
73 | animation-name: page-4;
74 | }
75 | .loader div ul li:nth-child(5) {
76 | --c: var(--page-fold);
77 | -webkit-animation-name: page-5;
78 | animation-name: page-5;
79 | }
80 | .loader div ul li svg {
81 | width: 90px;
82 | height: 120px;
83 | display: block;
84 | }
85 | .loader div ul li:first-child {
86 | --r: 0deg;
87 | --o: 1;
88 | }
89 | .loader div ul li:last-child {
90 | --o: 1;
91 | }
92 | .loader span {
93 | display: block;
94 | left: 0;
95 | right: 0;
96 | top: 100%;
97 | margin-top: 20px;
98 | text-align: center;
99 | color: var(--text);
100 | font-size: 20px;
101 | font-weight: bold;
102 | }
103 |
104 | @-webkit-keyframes page-2 {
105 | 0% {
106 | transform: rotateY(180deg);
107 | opacity: 0;
108 | }
109 | 20% {
110 | opacity: 1;
111 | }
112 | 35%, 100% {
113 | opacity: 0;
114 | }
115 | 50%, 100% {
116 | transform: rotateY(0deg);
117 | }
118 | }
119 |
120 | @keyframes page-2 {
121 | 0% {
122 | transform: rotateY(180deg);
123 | opacity: 0;
124 | }
125 | 20% {
126 | opacity: 1;
127 | }
128 | 35%, 100% {
129 | opacity: 0;
130 | }
131 | 50%, 100% {
132 | transform: rotateY(0deg);
133 | }
134 | }
135 | @-webkit-keyframes page-3 {
136 | 15% {
137 | transform: rotateY(180deg);
138 | opacity: 0;
139 | }
140 | 35% {
141 | opacity: 1;
142 | }
143 | 50%, 100% {
144 | opacity: 0;
145 | }
146 | 65%, 100% {
147 | transform: rotateY(0deg);
148 | }
149 | }
150 | @keyframes page-3 {
151 | 15% {
152 | transform: rotateY(180deg);
153 | opacity: 0;
154 | }
155 | 35% {
156 | opacity: 1;
157 | }
158 | 50%, 100% {
159 | opacity: 0;
160 | }
161 | 65%, 100% {
162 | transform: rotateY(0deg);
163 | }
164 | }
165 | @-webkit-keyframes page-4 {
166 | 30% {
167 | transform: rotateY(180deg);
168 | opacity: 0;
169 | }
170 | 50% {
171 | opacity: 1;
172 | }
173 | 65%, 100% {
174 | opacity: 0;
175 | }
176 | 80%, 100% {
177 | transform: rotateY(0deg);
178 | }
179 | }
180 | @keyframes page-4 {
181 | 30% {
182 | transform: rotateY(180deg);
183 | opacity: 0;
184 | }
185 | 50% {
186 | opacity: 1;
187 | }
188 | 65%, 100% {
189 | opacity: 0;
190 | }
191 | 80%, 100% {
192 | transform: rotateY(0deg);
193 | }
194 | }
195 | @-webkit-keyframes page-5 {
196 | 45% {
197 | transform: rotateY(180deg);
198 | opacity: 0;
199 | }
200 | 65% {
201 | opacity: 1;
202 | }
203 | 80%, 100% {
204 | opacity: 0;
205 | }
206 | 95%, 100% {
207 | transform: rotateY(0deg);
208 | }
209 | }
210 | @keyframes page-5 {
211 | 45% {
212 | transform: rotateY(180deg);
213 | opacity: 0;
214 | }
215 | 65% {
216 | opacity: 1;
217 | }
218 | 80%, 100% {
219 | opacity: 0;
220 | }
221 | 95%, 100% {
222 | transform: rotateY(0deg);
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/client/src/actions/orderActions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import types from './types';
3 |
4 | export const createOrder = (order) => async (dispatch, getState) => {
5 | try {
6 | dispatch({type: types.ORDER_CREATE_REQUEST});
7 |
8 | const { currentUser: { userInfo }} = getState();
9 |
10 | const config = {
11 | headers: {
12 | 'Content-Type': 'application/json',
13 | Authorization: `Bearer ${userInfo.token}`
14 | }
15 | }
16 |
17 | const { data } = await axios.post('/api/orders', order, config)
18 |
19 | dispatch({
20 | type: types.ORDER_CREATE_SUCCESS,
21 | payload: data
22 | });
23 |
24 | } catch (error) {
25 | dispatch({
26 | type: types.ORDER_CREATE_FAIL,
27 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
28 | });
29 | }
30 | };
31 |
32 | export const getOrderDetails = (id) => async(dispatch, getState) => {
33 | try {
34 | dispatch({ type: types.ORDER_DETAILS_REQUEST })
35 |
36 | const { currentUser: { userInfo }} = getState();
37 |
38 | const config = {
39 | headers: {
40 | 'Content-Type': 'application/json',
41 | Authorization: `Bearer ${userInfo.token}`
42 | }
43 | }
44 |
45 | const { data } = await axios.get(`/api/orders/${id}`, config)
46 |
47 | dispatch({
48 | type: types.ORDER_DETAILS_SUCCESS,
49 | payload: data
50 | })
51 |
52 | } catch (error) {
53 | dispatch({
54 | type: types.ORDER_DETAILS_FAIL,
55 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
56 | });
57 | }
58 | };
59 |
60 | export const payOrder = (id, paymentResult) => async(dispatch, getState) => {
61 | try {
62 | dispatch({ type: types.ORDER_PAY_REQUEST })
63 |
64 | const { currentUser: { userInfo }} = getState();
65 |
66 | const config = {
67 | headers: {
68 | 'Content-Type': 'application/json',
69 | Authorization: `Bearer ${userInfo.token}`
70 | }
71 | }
72 |
73 | const { data } = await axios.put(`/api/orders/${id}/pay`, paymentResult, config)
74 |
75 | dispatch({
76 | type: types.ORDER_PAY_SUCCESS,
77 | payload: data
78 | })
79 |
80 | } catch (error) {
81 | dispatch({
82 | type: types.ORDER_PAY_FAIL,
83 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
84 | });
85 | }
86 | }
87 |
88 | export const getMyOrders = () => async(dispatch, getState) => {
89 | try {
90 | dispatch({ type: types.ORDER_LIST_REQUEST })
91 |
92 | const { currentUser: { userInfo }} = getState();
93 |
94 | const config = {
95 | headers: {
96 | Authorization: `Bearer ${userInfo.token}`
97 | }
98 | }
99 |
100 | const { data } = await axios.get(`/api/orders/myorders`, config)
101 |
102 | dispatch({
103 | type: types.ORDER_LIST_SUCCESS,
104 | payload: data
105 | })
106 |
107 | } catch (error) {
108 | dispatch({
109 | type: types.ORDER_LIST_FAIL,
110 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
111 | });
112 | }
113 | };
114 |
115 | export const getOrders = () => async(dispatch, getState) => {
116 | try {
117 | dispatch({ type: types.ORDERS_LIST_ADMIN_REQUEST })
118 |
119 | const { currentUser: { userInfo }} = getState();
120 |
121 | const config = {
122 | headers: {
123 | Authorization: `Bearer ${userInfo.token}`
124 | }
125 | }
126 |
127 | const { data } = await axios.get(`/api/orders`, config)
128 |
129 | dispatch({
130 | type: types.ORDERS_LIST_ADMIN_SUCCESS,
131 | payload: data
132 | })
133 |
134 | } catch (error) {
135 | dispatch({
136 | type: types.ORDERS_LIST_ADMIN_FAIL,
137 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
138 | });
139 | }
140 | };
141 |
142 | export const deliverOrder = (id) => async(dispatch, getState) => {
143 | try {
144 | dispatch({ type: types.ORDER_DELIVER_REQUEST })
145 |
146 | const { currentUser: { userInfo }} = getState();
147 |
148 | const config = {
149 | headers: {
150 | Authorization: `Bearer ${userInfo.token}`
151 | }
152 | }
153 |
154 | const { data } = await axios.put(`/api/orders/${id}/deliver`,{}, config)
155 |
156 | dispatch({
157 | type: types.ORDER_DELIVER_SUCCESS,
158 | payload: data
159 | })
160 |
161 | } catch (error) {
162 | dispatch({
163 | type: types.ORDER_DELIVER_FAIL,
164 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
165 | });
166 | }
167 | }
--------------------------------------------------------------------------------
/server/controllers/productController.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from 'express-async-handler'
2 | import Product from '../models/productModel.js';
3 |
4 |
5 | // @desc Fetch all products
6 | // @route GET /api/products
7 | // @access Public
8 | export const getProducts = asyncHandler( async (req,res) => {
9 | const pageSize = 8;
10 | const page = Number(req.query.page) || 1;
11 | const count = await Product.countDocuments({});
12 | const products = await Product.find({}).sort({ rating: -1}).limit(pageSize).skip(pageSize * (page - 1))
13 | res.json({products, page, totalPages: Math.ceil(count / pageSize)})
14 | });
15 |
16 |
17 | // @desc Fetch single product
18 | // @route GET /api/products/:id
19 | // @access Public
20 | export const getProductById = asyncHandler( async (req,res) => {
21 | const product = await Product.findById(req.params.id);
22 |
23 | if(product){
24 | res.json(product);
25 | } else {
26 | res.status(404)
27 | throw new Error('Product not found')
28 | }
29 | });
30 |
31 | // @desc Search products
32 | // @route GET /api/products/search/:keyword
33 | // @access Public
34 | export const searchProducts = asyncHandler( async (req,res) => {
35 |
36 | const keyword = req.params.keyword
37 | const regex = new RegExp(keyword, 'ig')
38 | const products = await Product.find({$or: [{name: regex}, {author: regex}]});
39 | res.json(products)
40 | });
41 |
42 | // @desc Get products by genre
43 | // @route GET /api/products/genre/:genre
44 | // @access Public
45 | export const getProductsByGenre = asyncHandler( async (req,res) => {
46 |
47 | const genre = req.params.genre
48 | const products = await Product.find({category : genre });
49 | res.json(products)
50 | });
51 |
52 |
53 | // @desc Delete Product
54 | // @route DELETE /api/products/:id
55 | // @access Private/Admin
56 | export const deleteProduct = asyncHandler( async (req,res) => {
57 | const product = await Product.findById(req.params.id);
58 |
59 | if(product){
60 | await product.remove();
61 | res.json({message: 'Product deleted'})
62 | } else {
63 | res.status(404)
64 | throw new Error('Product not found')
65 | }
66 | });
67 |
68 | // @desc Create Product
69 | // @route POST /api/products/
70 | // @access Private/Admin
71 | export const createProduct = asyncHandler( async (req,res) => {
72 |
73 | const product = new Product({
74 | name: 'Book Name',
75 | price: 0,
76 | user: req.user._id,
77 | image: '/images/sample.jpg',
78 | author: 'Author',
79 | category: 'Category',
80 | ISBN: '0000000000',
81 | publication: 'Publication',
82 | countInStock: 0,
83 | numReviews: 0,
84 | description: 'Book Description'
85 | });
86 |
87 | const createdProduct = await product.save();
88 | res.status(201).json(createdProduct)
89 |
90 | });
91 |
92 | // @desc Update Product
93 | // @route PUT /api/products/:id
94 | // @access Private/Admin
95 | export const updateProduct = asyncHandler( async (req,res) => {
96 |
97 | const { name, price, description,
98 | image, publication, author, category, countInStock } = req.body;
99 |
100 | const product = await Product.findById(req.params.id);
101 |
102 | if(product){
103 |
104 | product.name = name,
105 | product.price = price,
106 | product.description = description,
107 | product.image = image,
108 | product.publication = publication,
109 | product.author = author,
110 | product.category = category,
111 | product.countInStock = countInStock
112 |
113 | const updatedProduct = await product.save()
114 | res.status(201).json(updatedProduct);
115 | } else {
116 | res.status(404)
117 | throw new Error('Product Not Found')
118 | }
119 |
120 | });
121 |
122 | // @desc Add new review
123 | // @route POST /api/products/:id/reviews
124 | // @access Private
125 | export const addProductReview = asyncHandler( async (req,res) => {
126 |
127 | const { rating, comment } = req.body;
128 |
129 | const product = await Product.findById(req.params.id);
130 |
131 | if(product){
132 | const alreadyReviewed = product.reviews.find(r => r.user.toString() === req.user._id.toString());
133 | if(alreadyReviewed){
134 | res.status(400)
135 | throw new Error('Already Reviewed')
136 | }
137 |
138 | const review = {
139 | name: req.user.name,
140 | rating: Number(rating),
141 | comment,
142 | user: req.user._id
143 | }
144 | product.reviews.push(review)
145 |
146 | product.numReviews = product.numReviews + 1
147 |
148 | product.rating = Number(product.reviews.reduce((acc, item) => ( acc + item.rating), 0) / product.reviews.length).toFixed(2);
149 |
150 | await product.save()
151 | res.status(201).json({message: 'Review Added'});
152 | } else {
153 | res.status(404)
154 | throw new Error('Product Not Found')
155 | }
156 |
157 | });
158 |
159 | // @desc Get Top Products
160 | // @route GET /api/products/top
161 | // @access Public
162 | export const getTopProducts = asyncHandler( async (req,res) => {
163 |
164 | const products = await Product.find({}).sort({ rating: -1}).limit(4);
165 | res.json(products)
166 | });
--------------------------------------------------------------------------------
/client/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react'
2 | import { AppBar, Toolbar, Typography, Button, Menu, MenuItem } from '@material-ui/core'
3 | import ShoppingCartIcon from '@material-ui/icons/ShoppingCart';
4 | import PersonIcon from '@material-ui/icons/Person';
5 | import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
6 | import {NavContainer, ButtonContainer, StyledLink} from './Header.elements'
7 | import { useDispatch, useSelector } from 'react-redux';
8 | import { logout } from '../../actions/userActions';
9 | import SearchBox from '../SearchBox';
10 | import {TweenMax, Power3} from 'gsap';
11 |
12 | function Header() {
13 |
14 | const dispatch = useDispatch();
15 | const userInfo = useSelector(state => state.currentUser.userInfo);
16 |
17 | const handleLogout = () => {
18 | console.log('clicked');
19 | dispatch(logout());
20 | }
21 |
22 | //ProfileMenu Toggle
23 | const [anchorEl, setAnchorEl] = useState(null);
24 | const handleMenuOpen = (event) => {
25 | setAnchorEl(event.currentTarget);
26 | };
27 | const handleMenuClose = () => {
28 | setAnchorEl(null);
29 | };
30 |
31 | //AdminMenu Toggle
32 | const [anchorElAdmin, setAnchorElAdmin] = useState(null);
33 | const handleAdminMenuOpen = (event) => {
34 | setAnchorElAdmin(event.currentTarget);
35 | };
36 | const handleAdminMenuClose = () => {
37 | setAnchorElAdmin(null);
38 | };
39 |
40 | let headerRef = useRef(null)
41 | useEffect(() => {
42 | TweenMax.from(headerRef, .8, {opacity: 0, y: -50, ease: Power3.easeOut,delay: 1.2})
43 | }, [])
44 |
45 | return (
46 |
47 |
48 |
49 | headerRef = el}>
50 |
51 |
52 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | Cart
61 |
62 |
63 | {userInfo ?
64 |
65 |
66 | {userInfo.name.split(' ')[0]}
67 |
68 |
74 |
75 | Profile
76 |
77 |
78 | Logout
79 |
80 |
81 | :
82 |
83 |
84 | Login
85 |
86 |
87 | }
88 | {
89 | userInfo && userInfo.isAdmin && (
90 | <>
91 |
92 | Admin
93 |
94 |
100 |
101 | Users
102 |
103 |
104 | Products
105 |
106 |
107 | Orders
108 |
109 |
110 | >
111 | )
112 | }
113 |
114 |
115 |
116 |
117 |
118 | )
119 | }
120 |
121 | export default Header
122 |
--------------------------------------------------------------------------------
/client/src/components/Loader/Loader.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Loader.css'
3 |
4 | function Loader() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
Loading...
43 |
44 |
45 | )
46 | }
47 |
48 | export default Loader
49 |
--------------------------------------------------------------------------------
/client/src/actions/userActions.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import types from "./types"
3 |
4 | export const login = (email, password) => async (dispatch) =>{
5 | try{
6 | dispatch({
7 | type: types.USER_LOGIN_REQUEST
8 | });
9 |
10 | const config = {
11 | headers: {
12 | 'Content-Type': 'application/json '
13 | }
14 | }
15 |
16 | const {data} = await axios.post('/api/users/login', {email, password}, config);
17 |
18 | dispatch({
19 | type: types.USER_LOGIN_SUCCESS,
20 | payload: data
21 | });
22 |
23 | localStorage.setItem('userInfo', JSON.stringify(data))
24 | } catch(error) {
25 | dispatch({
26 | type: types.USER_LOGIN_FAIL,
27 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
28 | })
29 | }
30 |
31 | };
32 |
33 | export const logout = ()=> (dispatch) => {
34 | localStorage.removeItem('userInfo');
35 | dispatch({ type: types.USER_LOGOUT });
36 | dispatch({ type: types.ORDER_LIST_RESET });
37 | dispatch({ type: types.USER_LIST_RESET });
38 | dispatch({type: types.ORDERS_LIST_ADMIN_RESET});
39 | }
40 |
41 | export const registerUser = (name, email, password) => async (dispatch) =>{
42 | try{
43 | dispatch({
44 | type: types.USER_REGISTER_REQUEST
45 | });
46 |
47 | const config = {
48 | headers: {
49 | 'Content-Type': 'application/json '
50 | }
51 | }
52 |
53 | const {data} = await axios.post('/api/users', {name, email, password}, config);
54 |
55 | dispatch({
56 | type: types.USER_REGISTER_SUCCESS,
57 | payload: data
58 | });
59 |
60 | dispatch({
61 | type: types.USER_LOGIN_SUCCESS,
62 | payload: data
63 | });
64 |
65 | localStorage.setItem('userInfo', JSON.stringify(data))
66 | } catch(error) {
67 | dispatch({
68 | type: types.USER_REGISTER_FAIL,
69 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
70 | })
71 | }
72 |
73 | };
74 |
75 | export const getUserDetails = (id) => async (dispatch, getState) =>{
76 | try{
77 | dispatch({
78 | type: types.USER_DETAILS_REQUEST
79 | });
80 |
81 | const { currentUser: { userInfo }} = getState();
82 |
83 | const config = {
84 | headers: {
85 | 'Content-Type': 'application/json',
86 | Authorization: `Bearer ${userInfo.token}`
87 | }
88 | }
89 |
90 | const {data} = await axios.get(`/api/users/${id}`, config);
91 |
92 | dispatch({
93 | type: types.USER_DETAILS_SUCCESS,
94 | payload: data
95 | });
96 |
97 |
98 | } catch(error) {
99 | dispatch({
100 | type: types.USER_DETAILS_FAIL,
101 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
102 | })
103 | }
104 |
105 | };
106 |
107 | export const updateUserProfile = (user) => async (dispatch, getState) =>{
108 | try{
109 | dispatch({
110 | type: types.USER_UPDATE_PROFILE_REQUEST
111 | });
112 |
113 | const { currentUser: { userInfo }} = getState();
114 |
115 | const config = {
116 | headers: {
117 | 'Content-Type': 'application/json',
118 | Authorization: `Bearer ${userInfo.token}`
119 | }
120 | }
121 |
122 | const {data} = await axios.put(`/api/users/profile`, user, config);
123 |
124 | dispatch({
125 | type: types.USER_UPDATE_PROFILE_SUCCESS,
126 | payload: data
127 | });
128 | dispatch({
129 | type: types.USER_LOGIN_SUCCESS,
130 | payload: data
131 | });
132 |
133 | localStorage.setItem('userInfo', JSON.stringify(data))
134 |
135 |
136 | } catch(error) {
137 | dispatch({
138 | type: types.USER_UPDATE_PROFILE_FAIL,
139 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
140 | })
141 | }
142 |
143 | };
144 |
145 | export const getUserList = () => async (dispatch, getState) => {
146 |
147 | try {
148 | dispatch({ type: types.USER_LIST_REQUEST });
149 |
150 | const { currentUser: { userInfo }} = getState();
151 |
152 | const config = {
153 | headers: {
154 | 'Content-Type': 'application/json',
155 | Authorization: `Bearer ${userInfo.token}`
156 | }
157 | }
158 |
159 | const { data } = await axios.get('/api/users', config)
160 |
161 | dispatch({
162 | type: types.USER_LIST_SUCCESS,
163 | payload: data
164 | })
165 | } catch (error) {
166 | dispatch({
167 | type: types.USER_LIST_FAIL,
168 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
169 | })
170 | }
171 | }
172 |
173 | export const deleteUser = (id) => async(dispatch, getState) => {
174 |
175 | try {
176 |
177 | dispatch({ type: types.USER_DELETE_REQUEST });
178 |
179 | const { currentUser: { userInfo }} = getState();
180 |
181 | const config = {
182 | headers: {
183 | 'Content-Type': 'application/json',
184 | Authorization: `Bearer ${userInfo.token}`
185 | }
186 | };
187 |
188 | await axios.delete(`/api/users/${id}`, config)
189 |
190 | dispatch({ type: types.USER_DELETE_SUCCESS })
191 |
192 | } catch (error) {
193 | dispatch({
194 | type: types.USER_DELETE_FAIL,
195 | payload: error.response && error.response.data.message ? error.response.data.message : error.message
196 | })
197 | }
198 | }
--------------------------------------------------------------------------------
/client/src/pages/ProductList/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { Button, CircularProgress, Container, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@material-ui/core';
4 | import { Alert, Pagination } from '@material-ui/lab';
5 | import Loader from '../../components/Loader/Loader';
6 | import { fetchProductsList, deleteProductAdmin, createProductAdmin } from '../../actions/productActions';
7 | import { Edit, Delete, ButtonContainer, PaginationContainer } from './ProductList.elements';
8 | import types from '../../actions/types';
9 | import Meta from '../../components/Meta';
10 |
11 |
12 | const ProductList = ({history, match}) => {
13 |
14 | const pageNumber = Number(match.params.page) || 1;
15 |
16 | const currentUser = useSelector( state => state.currentUser);
17 | const { userInfo } = currentUser;
18 |
19 | const productList = useSelector( state => state.productList);
20 | const { products, loading, error, totalPages, page } = productList;
21 |
22 | const deleteProduct = useSelector( state => state.deleteProduct);
23 | const { loading: loadingDelete, error: errorDelete, success: successDelete } = deleteProduct;
24 |
25 | const createProduct = useSelector( state => state.createProduct);
26 | const { loading: loadingCreate, error: errorCreate, success: successCreate, product: createdProduct} = createProduct;
27 |
28 | const dispatch = useDispatch();
29 |
30 | const deleteHandler = (id) => {
31 | if(window.confirm('Are you sure?')){
32 | dispatch(deleteProductAdmin(id));
33 | }
34 | };
35 |
36 | const createProductHandler = () => {
37 | dispatch(createProductAdmin())
38 | }
39 |
40 | const handlePagination = (e, v) => {
41 | history.push(`/admin/productlist/page/${v}`)
42 | }
43 |
44 | useEffect(() => {
45 | dispatch({
46 | type: types.PRODUCT_CREATE_RESET
47 | });
48 |
49 | if(!userInfo || !userInfo.isAdmin){
50 | history.push('/login')
51 | }
52 |
53 | if(successCreate){
54 | history.push(`/admin/product/${createdProduct._id}/edit`)
55 | }else{
56 | dispatch(fetchProductsList(pageNumber))
57 | }
58 | }, [dispatch, history, userInfo, successDelete, successCreate, createdProduct, pageNumber])
59 |
60 | return (
61 |
62 |
63 |
64 | {
65 | loading ? : error ? {error} : (
66 | <>
67 | Products
68 |
69 | { loadingCreate ? :
70 | Add Product
75 |
76 | }
77 |
78 | {errorDelete && {errorDelete} }
79 | {errorCreate && {errorCreate} }
80 |
81 |
82 |
83 |
84 | ID
85 | NAME
86 | AUTHOR
87 | PRICE
88 | CATEGORY
89 | STOCK
90 |
91 |
92 |
93 |
94 | {products.map( product => (
95 |
96 | {product._id}
97 | {product.name}
98 | {product.author}
99 | ${product.price}
100 | {product.category}
101 | {product.countInStock}
102 |
103 | { history.push(`/admin/product/${product._id}/edit`)}}
105 | >
106 |
107 |
108 | { loadingDelete ? :
109 | deleteHandler(product._id)}>
110 |
111 |
112 | }
113 |
114 |
115 | ))}
116 |
117 |
118 |
119 |
120 |
127 |
128 | >
129 | )
130 | }
131 |
132 |
133 | )
134 | }
135 |
136 | export default ProductList;
137 |
--------------------------------------------------------------------------------
/client/src/components/QuoteGenerator/index.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import React, { useEffect, useRef, useState } from 'react'
3 |
4 | const QuoteGenerator = ({genre}) => {
5 |
6 | const quotes = {
7 | thriller: [
8 | {quote: '“Someone doesn’t always need a gun to kill you. Sometimes, their actions are enough. You don’t need an assassin to kill you. Sometimes, a lover is enough.”' , by: '― Namrata Gupta, Together We Were (W)hole'},
9 | {quote: '“The sweetest smiles hold the darkest secrets...”' , by: '― Sara Shepard, Flawless'},
10 | {quote: '“Nobody’s ever been arrested for a murder; they have only ever been arrested for not planning it properly.”' , by: '― Terry Hayes, I Am Pilgrim'}
11 | ],
12 | romance: [
13 | {quote: '“You should be kissed and often, and by someone who knows how.”' , by: '― Margaret Mitchell, Gone With The Wind'},
14 | {quote: '“In vain I have struggled. It will not do. My feelings will not be repressed. You must allow me to tell you how ardently I admire and love you.”' , by: '― Jane Austen, Pride And Prejudice'},
15 | {quote: '“I cannot let you burn me up, nor can I resist you. No mere human can stand in a fire and not be consumed.”' , by: '― A.S. Byatt, Possession'}
16 | ],
17 | youngadult: [
18 | {quote: "“I like the night. Without the dark, we'd never see the stars.”" , by: '― Stephen Meyer, Twilight'},
19 | {quote: "“You don't get to choose if you get hurt in this world...but you do have some say in who hurts you. I like my choices.”" , by: '― John Green, The Fault in Our Stars'},
20 | {quote: "“There's nothing like deep breaths after laughing that hard. Nothing in the world like a sore stomach for the right reasons.”" , by: '― Stephen Chbosky, The Perks if Being a Wallflower'}
21 | ],
22 | sciencefiction: [
23 | {quote: "“That makes me a pirate! A space pirate!”" , by: '― Andy Weir, The Martian'},
24 | {quote: "“Who controls the past controls the future. Who controls the present controls the past.”" , by: '― George Orwell, 1984'},
25 | {quote: "“Time is an illusion. Lunchtime doubly so.”" , by: "― Douglas Adams, Hitchhiker's Guide to the Galaxy"}
26 | ],
27 | fantasy : [
28 | {quote: "“It is our choices, Harry, that show what we truly are, far more than our abilities.”" , by: '― J.K. Rowling, Harry Potter and the Chamber of Secrets'},
29 | {quote: "“Not all those who wander are lost.”" , by: '― J.R.R. Tolkien, The Lord of the Rings'},
30 | {quote: "“... a mind needs books as a sword needs a whetstone, if it is to keep its edge.”" , by: '― George R.R. Martin, A Game of Thrones'}
31 | ],
32 | poetry: [
33 | {quote: "“Do I dare Disturb the universe?" , by: '― T.S. Eliot, The Wasteland and Other Poems'},
34 | {quote: "“Love comforteth like sunshine after rain”" , by: "― William Shakespeare, Shakespeare's Sonnets"},
35 | {quote: "“Hope is the thing with feathers. That perches in the soul. And sings the tune without the words. And never stops at all.”" , by: '― Emily Dickinson, The Complete Poems of Emily Dickinson'},
36 | ],
37 | biography: [
38 | {quote: "“Everything can be taken from a man but one thing: the last of the human freedoms—to choose one’s attitude in any given set of circumstances, to choose one’s own way.”" , by: "― Viktor E. Frankl, Man's Search for Meaning"},
39 | {quote: "“Because the people who are crazy enough to think they can change the world, are the ones who do.”" , by: "― Steve Jobs, Steve Jobs"},
40 | {quote: "“An eye for an eye will only make the whole world blind.”" , by: "― Mahatama Gandhi, Gandhi: An Autobiography"}
41 | ],
42 | selfhelp: [
43 | {quote: "“Who you are is defined by what you’re willing to struggle for.”" , by: "― Mark Manson, The Subtle Art of Not Giving a F*ck"},
44 | {quote: "“Winners are not afraid of losing. But losers are. Failure is part of the process of success. People who avoid failure also avoid success.”" , by: "― Robert T. Kiyosaki, Rich Dad, Poor Dad"},
45 | {quote: "“The starting point of all achievement is DESIRE. Keep this constantly in mind. Weak desire brings weak results, just as a small fire makes a small amount of heat.”" , by: "― Napoleon Hill, Think and Grow Rich"}
46 | ]
47 | }
48 |
49 | const QuoteRef = useRef();
50 |
51 | const [currentQuote, setCurrentQuote] = useState(quotes[genre][0]);
52 |
53 | useEffect(() => {
54 | let isMounted = true;
55 | let count = 0;
56 | const interval = setInterval(() => {
57 | if(count < 2){
58 | QuoteRef.current.style.opacity = '0'
59 | count++;
60 | setTimeout(() => {
61 | setCurrentQuote(quotes[genre][count]);
62 | if(isMounted){
63 | QuoteRef.current.style.opacity = '1'
64 | }
65 | }, 1000);
66 | }else{
67 | QuoteRef.current.style.opacity = '0'
68 | count = 0;
69 | setTimeout(() => {
70 | setCurrentQuote(quotes[genre][count]);
71 | if(isMounted){
72 | QuoteRef.current.style.opacity = '1'
73 | }
74 | }, 1000);
75 | }
76 |
77 | return(() => {
78 | isMounted = false;
79 | clearInterval(interval);
80 | })
81 | }, 7000);
82 | return () => clearInterval(interval);
83 | // eslint-disable-next-line
84 | }, [genre])
85 | return (
86 |
87 | {currentQuote.quote}
88 | {currentQuote.by}
89 |
90 | )
91 | }
92 |
93 | export default QuoteGenerator;
94 |
95 | const QuotesContainer = styled.div`
96 | left: 0;
97 | display: flex;
98 | flex-direction: column;
99 | justify-content: center;
100 | position: absolute;
101 | align-items: center;
102 | width: 100%;
103 | color: #ffffff;
104 | transition: all .5s ease-out;
105 | @media (max-width: 450px){
106 | margin-top: -20px;
107 | };
108 | & h1{
109 | font-style: italic;
110 | font-weight: 500;
111 | padding: 0 200px;
112 | margin: 0 auto;
113 | @media (max-width: 1290px){
114 | padding: 0 150px;
115 | };
116 | @media (max-width: 1000px){
117 | padding: 0 100px;
118 | };
119 | @media (max-width: 850px){
120 | padding: 0px 40px;
121 | font-size: 22px;
122 | };
123 | };
124 | & p{
125 | width: 80%;
126 | margin: 30px auto;
127 | text-align: right;
128 | @media (max-width: 850px){
129 | font-size: 15px;
130 | width: 75%;
131 | };
132 | }
133 | `;
134 |
--------------------------------------------------------------------------------
/client/src/pages/Profile/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Alert } from '@material-ui/lab';
3 | import { Button, Container, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField } from '@material-ui/core';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { getUserDetails, updateUserProfile } from '../../actions/userActions'
6 | import { FormContainer, StyledTableRow } from './Profile.elements'
7 | import { Grid } from '@material-ui/core';
8 | import ClearIcon from '@material-ui/icons/Clear';
9 | import { getMyOrders } from '../../actions/orderActions';
10 | import Loader from '../../components/Loader/Loader';
11 | import Meta from '../../components/Meta';
12 |
13 | const ProfilePage = ({location, history}) => {
14 |
15 | const [name, setName] = useState('');
16 | const [email, setEmail] = useState('');
17 | const [password, setPassword] = useState('');
18 | const [confirmPassword, setConfirmPassword] = useState('');
19 |
20 | const [message, setMessage] = useState(null);
21 |
22 |
23 | const dispatch = useDispatch();
24 |
25 | const userDetails = useSelector(state => state.userDetails);
26 | const {loading, error, user } = userDetails;
27 |
28 | const currentUser = useSelector(state => state.currentUser);
29 | const { userInfo } = currentUser;
30 |
31 | const userUpdateProfile = useSelector(state => state.userUpdateProfile);
32 |
33 | const { success } = userUpdateProfile;
34 |
35 | const myOrders = useSelector(state => state.myOrders);
36 |
37 | const { loading:ordersLoading, orders, error:ordersError } = myOrders;
38 |
39 | const submitHandler = (e) => {
40 | e.preventDefault();
41 | if(password !== confirmPassword){
42 | setMessage('Passwords do not match')
43 | } else {
44 | dispatch(updateUserProfile({ id: user._id, name, email, password }))
45 | }
46 |
47 | }
48 |
49 | useEffect(() => {
50 |
51 | if(!userInfo){
52 | history.push('/login')
53 | }else{
54 | if(!user || !user.hasOwnProperty('name')){
55 | dispatch(getUserDetails('profile'))
56 | dispatch(getMyOrders());
57 | } else {
58 | setName(user.name);
59 | setEmail(user.email);
60 | document.title = user.name;
61 | }
62 |
63 | }
64 | }, [history, userInfo, user, dispatch])
65 | return (
66 |
67 |
68 |
69 |
70 |
71 | Profile
72 |
73 | {message && {message} }
74 | {success && {'Profile Updated'} }
75 | {error && {error} }
76 | {loading && {'Updating...'} }
77 |
112 |
113 |
114 |
115 |
116 | Your Orders
117 | { ordersLoading? :
118 | ordersError ? {error} :
119 | orders.length === 0 ? You have no orders :
120 |
121 |
122 |
123 |
124 |
125 | ID
126 | Date
127 | Total
128 | Items
129 | Paid
130 | Delivered
131 |
132 |
133 |
134 | {orders.map( order => (
135 | {history.push(`/order/${order._id}`)}}
138 | >
139 | {order._id}
140 | {order.createdAt.slice(0,10)}
141 | ${order.totalPrice}
142 | {order.orderItems.reduce((total, item) => (total+item.qty), 0)}
143 | {order.isPaid ? {order.paidAt.slice(0,10)}
: }
144 | {order.isDelivered ? {order.deliveredAt.slice(0,10)}
: }
145 |
146 | ))}
147 |
148 |
149 |
150 | }
151 |
152 |
153 |
154 |
155 | )
156 | }
157 |
158 | export default ProfilePage
159 |
--------------------------------------------------------------------------------