├── 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 | 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 |
24 | 25 |
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 | {product.name}/ 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 |
37 | 38 | setPaymentMethod(e.target.value)}> 39 | } label='PayPal or Credit Card'/> 40 | 41 | 42 | 43 |
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 | 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 | {product.name} 57 |
58 |
59 |
60 |

{product.name}

61 |

{`by ${product.author}`}

62 |
63 |
64 | 65 | 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 | 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 |
50 | { 54 | setKeyword(e.target.value); 55 | }} 56 | > 57 | 58 | 59 | 60 | 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 |
35 | setAddress(e.target.value)}/> 43 | setCity(e.target.value)}/> 51 | setPostalCode(e.target.value)}/> 59 | setCountry(e.target.value)}/> 68 | 69 | 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 |
53 | setEmail(e.target.value)}/> 56 | setPassword(e.target.value)}/> 60 | 61 | 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 | {item.name} 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 | 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 |
62 | setName(e.target.value)} 69 | /> 70 | setEmail(e.target.value)} 78 | /> 79 | setPassword(e.target.value)} 87 | /> 88 | setConfirmPassword(e.target.value)} 96 | /> 97 | 98 | 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 | logo 54 | 55 | 56 | 57 | 58 | 59 | 62 | 63 | {userInfo ? 64 |
65 | 68 | 74 | 75 | Profile 76 | 77 | 78 | Logout 79 | 80 |
81 | : 82 | 83 | 86 | 87 | } 88 | { 89 | userInfo && userInfo.isAdmin && ( 90 | <> 91 | 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 | 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 | 108 | { loadingDelete ? : 109 | 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 |
78 | setName(e.target.value)} 84 | /> 85 | setEmail(e.target.value)} 93 | /> 94 | setPassword(e.target.value)} 101 | /> 102 | setConfirmPassword(e.target.value)} 109 | /> 110 | 111 | 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 | --------------------------------------------------------------------------------