├── .gitignore ├── README.md ├── client ├── .gitignore ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.js │ ├── components │ ├── CartItem │ │ ├── CartItem.css │ │ └── CartItem.jsx │ ├── Product │ │ ├── Product.css │ │ └── Product.jsx │ └── Products │ │ ├── Products.css │ │ └── Products.jsx │ ├── index.css │ ├── index.js │ ├── pages │ ├── Cart │ │ ├── Cart.css │ │ └── Cart.jsx │ ├── Checkout │ │ ├── Checkout.css │ │ └── Checkout.jsx │ └── Home │ │ └── Home.jsx │ ├── registerServiceWorker.js │ ├── routes │ └── Routes.jsx │ ├── store │ ├── actions │ │ ├── cart.js │ │ ├── products.js │ │ └── types.js │ ├── index.js │ └── reducers │ │ ├── cart.js │ │ ├── index.js │ │ └── products.js │ └── ui │ ├── BuySomething │ ├── BuySomething.css │ └── BuySomething.jsx │ └── NavBar │ ├── NavBar.css │ └── NavBar.jsx ├── config └── keys.js ├── libs └── db-connection.js ├── models └── Product.js ├── package.json ├── routes └── products.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shopping Cart - React.js 2 | 3 | 💸 Simple Online Shopping Cart made with the MERN Stack 4 | 5 | ## Installation 6 | 7 | ```bash 8 | # Install dependencies for server 9 | npm install 10 | 11 | # Install dependencies for client 12 | npm run client-install 13 | 14 | # Run the client & server with concurrently 15 | npm run dev 16 | 17 | # Run the Express server only 18 | npm run server 19 | 20 | # Run the React client only 21 | npm run client 22 | 23 | # Server runs on http://localhost:5000 and client on http://localhost:3000 24 | ``` 25 | 26 | ## More 27 | 28 | - If you want to run this project locally, you must change the value of **MONGO_URL** on the config/keys.js file, with your own 29 | MongoDB Database. 30 | 31 | ## Author 32 | 33 | **germancutraro** 34 | 35 | ## Why 36 | 37 | * Practice 38 | * MERN Lover 39 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.18.0", 7 | "react": "^16.4.1", 8 | "react-dom": "^16.4.1", 9 | "react-redux": "^5.0.7", 10 | "react-router-dom": "^4.3.1", 11 | "react-scripts": "1.1.4", 12 | "redux": "^4.0.0", 13 | "redux-persist": "^5.10.0", 14 | "redux-thunk": "^2.3.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | }, 22 | "proxy": "http://localhost:5000" 23 | } 24 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutonurmux/shopping-cart-react/d53e0b29aab9843ebe53b5e1e286aede7ce3b8bb/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Online Shopping Cart React.js 12 | 13 | 14 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Routes from './routes/Routes'; 4 | import NavBar from './ui/NavBar/NavBar'; 5 | 6 | class App extends Component { 7 | render() { 8 | return ( 9 |
10 | 11 | 12 |
13 | ); 14 | } 15 | } 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /client/src/components/CartItem/CartItem.css: -------------------------------------------------------------------------------- 1 | .Cart-Item-Photo { 2 | width: 100px; 3 | height: 100px; 4 | padding: 1rem; 5 | } 6 | 7 | .Quantity-Button { 8 | border: none; 9 | border-radius: 5px; 10 | margin: .3rem; 11 | height: 30px; 12 | width: 30px; 13 | font-size: 15px; 14 | cursor: pointer; 15 | outline: none; 16 | transition: ease .5s all; 17 | } 18 | 19 | 20 | @media(max-width: 450px) { 21 | .Quantity-Button { 22 | height: 20px; 23 | width: 20px; 24 | margin: 0; 25 | } 26 | } 27 | 28 | 29 | @media(max-width: 350px) { 30 | .Cart-Item-Photo { 31 | width: 60px; 32 | height: 60px; 33 | } 34 | } -------------------------------------------------------------------------------- /client/src/components/CartItem/CartItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import "./CartItem.css"; 3 | import PropTypes from "prop-types"; 4 | 5 | const CartItem = props => { 6 | return ( 7 | 8 | 9 | 10 | {" "} 11 | {props.name}{" "} 16 | 17 | {props.name} 18 | 19 | 22 | {props.quantity} 23 | 26 | 27 | 28 | ${props.price} 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | CartItem.propTypes = { 37 | name: PropTypes.string.isRequired, 38 | photo: PropTypes.string.isRequired, 39 | price: PropTypes.number.isRequired, 40 | quantity: PropTypes.number.isRequired, 41 | addItem: PropTypes.func.isRequired, 42 | removeItem: PropTypes.func.isRequired, 43 | } 44 | 45 | export default CartItem; 46 | -------------------------------------------------------------------------------- /client/src/components/Product/Product.css: -------------------------------------------------------------------------------- 1 | .Product-Wrapper { 2 | width: 25%; 3 | display: flex; 4 | justify-content: center; 5 | } 6 | 7 | @media(max-width: 1750px) { 8 | .Product-Wrapper { width: 33.33% } 9 | } 10 | 11 | @media(max-width: 1400px) { 12 | .Product-Wrapper { width: 50% } 13 | } 14 | 15 | @media(max-width: 900px) { 16 | .Product-Wrapper { width: 100% } 17 | } 18 | 19 | .Product-Image-Wrapper { 20 | display: flex; 21 | justify-content: center; 22 | } 23 | 24 | .Product-Image { 25 | padding: 3rem 3rem 1.3rem 3rem; 26 | } 27 | 28 | .Product-Title { 29 | display: flex; 30 | justify-content: center; 31 | } 32 | 33 | .Product { 34 | width: 250px; 35 | -webkit-box-shadow: 10px 10px 5px -6px rgba(0,3,51,0.04); 36 | -moz-box-shadow: 10px 10px 5px -6px rgba(0,3,51,0.04); 37 | box-shadow: 10px 10px 5px -6px rgba(0,3,51,0.04); 38 | margin-bottom: 2.7rem; 39 | display: flex; 40 | flex-flow: column wrap; 41 | transition: .3s ease all; 42 | } 43 | 44 | .Product:hover { 45 | transform: scale(1.1); 46 | } 47 | 48 | .Product-Title { 49 | color: #6f6f6f; 50 | font-size: 14px; 51 | } 52 | 53 | .Product-Price { 54 | font-size: 16px; 55 | font-weight: 700; 56 | color: #a864a8; 57 | } 58 | 59 | .Product-Data { 60 | display: flex; 61 | justify-content: space-around; 62 | align-items: center; 63 | padding: 2rem; 64 | } 65 | 66 | .Product-Add { 67 | width: 60%; 68 | } -------------------------------------------------------------------------------- /client/src/components/Product/Product.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import "./Product.css"; 3 | import PropTypes from 'prop-types'; 4 | 5 | const Product = props => { 6 | return ( 7 |
8 |
9 |
10 | {props.name} 11 |
12 |
13 |

{props.name}

14 |
15 |
16 | ${props.price} 17 | 18 |
19 |
20 |
21 | ); 22 | }; 23 | 24 | Product.propTypes = { 25 | name: PropTypes.string.isRequired, 26 | photo: PropTypes.string.isRequired, 27 | price: PropTypes.number.isRequired, 28 | addToCart: PropTypes.func.isRequired 29 | }; 30 | 31 | export default Product; -------------------------------------------------------------------------------- /client/src/components/Products/Products.css: -------------------------------------------------------------------------------- 1 | .Products-Container { 2 | width: 100%; 3 | display: flex; 4 | justify-content: center; 5 | padding: 5rem; 6 | } 7 | .Products-Wrapper { 8 | display: flex; 9 | flex-wrap: wrap; 10 | justify-content: space-between; 11 | width: 70%; 12 | } 13 | -------------------------------------------------------------------------------- /client/src/components/Products/Products.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Products.css"; 3 | import PropTypes from 'prop-types'; 4 | import Product from "../Product/Product"; 5 | import { connect } from "react-redux"; 6 | 7 | // Actions 8 | import { addToCart } from "../../store/actions/cart"; 9 | 10 | const Products = props => { 11 | const productList = props.products.map( (product, i) => ( 12 | props.addToCart(product, i)} 16 | /> 17 | )); 18 | return
19 |
20 | {productList} 21 |
22 |
; 23 | }; 24 | 25 | Products.propTypes = { 26 | products: PropTypes.array.isRequired, 27 | addToCart: PropTypes.func.isRequired 28 | }; 29 | 30 | const mapStateToProps = state => ({ 31 | cart: state.cart 32 | }); 33 | 34 | export default connect( 35 | mapStateToProps, 36 | { addToCart } 37 | )(Products); 38 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | html, body { 10 | width: 100%; 11 | min-height: 100vh; 12 | line-height: 1.15; 13 | font-family: 'Nunito Sans', sans-serif; 14 | } 15 | 16 | .App { 17 | min-height: 100vh; 18 | display: flex; 19 | flex-direction: column; 20 | } 21 | 22 | h1, h2, h3, h4, h4, h5, h6 { 23 | color: #252525; 24 | font-weight: 400; 25 | } 26 | 27 | a { 28 | color: #252525; 29 | text-decoration: none; 30 | } 31 | 32 | table { 33 | border-collapse: collapse; 34 | } 35 | 36 | button.product-button { 37 | background-color: #222; 38 | border: none; 39 | border-radius: 4px; 40 | color: #fff; 41 | text-transform: uppercase; 42 | font-weight: bold; 43 | font-size: 11px; 44 | outline: none; 45 | cursor: pointer; 46 | height: 36px; 47 | } 48 | 49 | /* width: 10%; */ 50 | 51 | button:active { 52 | transform: scale(1.01); 53 | } -------------------------------------------------------------------------------- /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 registerServiceWorker from './registerServiceWorker'; 6 | 7 | import { BrowserRouter } from 'react-router-dom'; 8 | 9 | // Main Reducer 10 | import rootReducer from './store/reducers'; 11 | // Redux 12 | import { createStore, applyMiddleware, compose } from 'redux'; 13 | import { Provider } from 'react-redux'; 14 | import thunk from 'redux-thunk'; 15 | 16 | // redux-persist 17 | import { persistStore, persistReducer } from 'redux-persist'; 18 | import storage from 'redux-persist/lib/storage'; 19 | import { PersistGate } from 'redux-persist/integration/react'; 20 | 21 | // history and middlewares 22 | const middlewares = [thunk]; 23 | 24 | const persistConfig = { 25 | key: 'root', 26 | storage 27 | }; 28 | 29 | // persist reducer 30 | const persistedReducer = persistReducer(persistConfig, rootReducer); 31 | // store creation and persist 32 | const store = createStore(persistedReducer, compose( 33 | applyMiddleware(...middlewares), 34 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 35 | )); 36 | const persistor = persistStore(store); 37 | 38 | ReactDOM.render( 39 | 40 | 41 | 42 | 43 | 44 | 45 | , 46 | document.getElementById('root') 47 | ); 48 | registerServiceWorker(); -------------------------------------------------------------------------------- /client/src/pages/Cart/Cart.css: -------------------------------------------------------------------------------- 1 | .Cart-Table { 2 | width: 80%; 3 | text-align: center; 4 | transition: ease .5s all; 5 | } 6 | 7 | .Cart-Products-Wrapper { 8 | display: flex; 9 | justify-content: center; 10 | flex-direction: column; 11 | align-items: center; 12 | } 13 | 14 | tr { 15 | height: 53px; 16 | } 17 | 18 | tr:nth-child(even), tr:first-child { 19 | border-bottom: 1px solid #eee; 20 | } 21 | 22 | tr:nth-child(even) { 23 | border-top: 1px solid #eee; 24 | } 25 | 26 | tr:last-child { 27 | border-bottom: none; 28 | } 29 | 30 | .My-Cart-Title { 31 | text-align: center; 32 | margin: 3rem; 33 | } 34 | 35 | /* Total Price & Checkout button */ 36 | .Total-Price { 37 | display: inline-block; 38 | font-weight: bold; 39 | padding: 1rem; 40 | } 41 | 42 | .Checkout-Button { 43 | background-color: #222; 44 | border: none; 45 | border-radius: 4px; 46 | margin: .3rem; 47 | outline: none; 48 | cursor: pointer; 49 | display: flex; 50 | justify-content: center; 51 | align-items: center; 52 | padding: 1rem; 53 | height: 36px; 54 | } 55 | 56 | .Checkout-Button-Text { 57 | color: #fff; 58 | text-align: center; 59 | text-transform: uppercase; 60 | font-size: 11px; 61 | } 62 | 63 | /* Media's */ 64 | 65 | @media(max-width: 750px) { 66 | .Cart-Table { 67 | width: 100%; 68 | } 69 | } 70 | 71 | @media(max-width: 450px) { 72 | .Cart-Table { 73 | font-size: 14px; 74 | } 75 | .Checkout-Button { 76 | width: 83%; 77 | } 78 | } 79 | 80 | 81 | -------------------------------------------------------------------------------- /client/src/pages/Cart/Cart.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./Cart.css"; 3 | import PropTypes from 'prop-types'; 4 | import { Link } from "react-router-dom"; 5 | 6 | import { connect } from "react-redux"; 7 | import { addToCart, removeFromCart, removeWholeItem } from "../../store/actions/cart"; 8 | // Components 9 | import CartItem from "../../components/CartItem/CartItem"; 10 | import BuySomething from "../../ui/BuySomething/BuySomething"; 11 | 12 | const sort = items => { 13 | return items.sort((a, b) => a._id < b._id); 14 | }; 15 | 16 | const totalPrice = cart => { 17 | return cart.reduce( 18 | (accum, product) => accum + product.price * product.quantity, 19 | 0 20 | ); 21 | }; 22 | 23 | class Cart extends Component { 24 | render() { 25 | const cartItems = sort(this.props.cart).map((product, i) => ( 26 | this.props.addToCart(product)} 30 | removeItem={() => this.props.removeFromCart(product)} 31 | removeWholeItem={() => this.props.removeWholeItem(product)} 32 | /> 33 | )); 34 | 35 | if (!cartItems.length) 36 | return 37 | 38 | return ( 39 |
40 |

My Cart:

41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {cartItems} 54 | 55 | 64 | 72 | 73 | 74 |
PhotoNameQuantityPrice / Unit
62 | Total 63 | 65 | 66 | 67 |

${totalPrice(this.props.cart)}

68 | 69 |
Checkout
70 | 71 |
75 |
76 |
77 | ); 78 | } 79 | } 80 | 81 | Cart.propTypes = { 82 | addToCart: PropTypes.func.isRequired, 83 | removeFromCart: PropTypes.func.isRequired 84 | }; 85 | 86 | const mapStateToProps = state => ({ 87 | cart: state.cart 88 | }); 89 | 90 | export default connect( 91 | mapStateToProps, 92 | { addToCart, removeFromCart, removeWholeItem } 93 | )(Cart); 94 | -------------------------------------------------------------------------------- /client/src/pages/Checkout/Checkout.css: -------------------------------------------------------------------------------- 1 | .Checkout-Wrapper { 2 | margin-top: 10%; 3 | } 4 | 5 | .Checkout-Form { 6 | display: flex; 7 | flex-flow: column wrap; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | 12 | .Checkout-Title { 13 | text-align: center; 14 | margin: 1rem; 15 | } 16 | .Checkout-Input { 17 | width: 53%; 18 | height: 45px; 19 | display: block; 20 | border: 1px solid #EDEDED; 21 | padding: 3px 0 3px 10px; 22 | margin: 1rem; 23 | outline: none; 24 | } 25 | 26 | .Checkout-Button { 27 | width: 53%; 28 | padding: 0; 29 | margin: 1rem; 30 | } 31 | 32 | .Close-Modal { 33 | position: absolute; 34 | top: 11px; 35 | right: 15px; 36 | color: #000; 37 | cursor: pointer; 38 | } 39 | 40 | @media(max-width: 350px) { 41 | .Checkout-Button { 42 | width: 93%; 43 | } 44 | } -------------------------------------------------------------------------------- /client/src/pages/Checkout/Checkout.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./Checkout.css"; 3 | import Modal from "react-modal"; 4 | import BuySomething from "../../ui/BuySomething/BuySomething"; 5 | 6 | import { connect } from 'react-redux'; 7 | 8 | const customStyles = { 9 | content: { 10 | backgroundColor: "#fff", 11 | top: "50%", 12 | left: "50%", 13 | right: "auto", 14 | bottom: "auto", 15 | marginRight: "-50%", 16 | transform: "translate(-50%, -50%)" 17 | } 18 | }; 19 | 20 | class Checkout extends Component { 21 | state = { 22 | name: "you", 23 | email: "", 24 | country: "", 25 | modalIsOpen: false 26 | }; 27 | 28 | onChangeHandler = e => { 29 | this.setState({ [e.target.name]: e.target.value }); 30 | }; 31 | 32 | openModal = e => { 33 | e.preventDefault(); 34 | this.setState({ modalIsOpen: true }); 35 | }; 36 | 37 | closeModal = () => { 38 | this.setState({ modalIsOpen: false }); 39 | }; 40 | 41 | render() { 42 | 43 | if (!this.props.cart.length) 44 | return 45 | 46 | return ( 47 |
48 |

Checkout

49 |
50 | 57 | 64 | 71 | 77 |
78 | 79 | 86 | 87 |

88 | Thanks {this.state.name} for testing my simple Online Shopping Cart! 89 |

90 |
91 |
92 | ); 93 | } 94 | } 95 | 96 | const mapStateToProps = state => ({ 97 | cart: state.cart 98 | }) 99 | 100 | export default connect(mapStateToProps, null)(Checkout); 101 | -------------------------------------------------------------------------------- /client/src/pages/Home/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | // Redux 4 | import { connect } from 'react-redux'; 5 | import { fetchProducts } from '../../store/actions/products'; 6 | // Components 7 | import Products from '../../components/Products/Products'; 8 | 9 | class Home extends Component { 10 | 11 | componentDidMount() { 12 | this.props.fetchProducts(); 13 | } 14 | 15 | render() { 16 | return ( 17 |
18 | 19 |
20 | ); 21 | } 22 | } 23 | 24 | const mapStateToProps = state => ({ 25 | products: state.products, 26 | cart: state.cart 27 | }); 28 | 29 | export default connect(mapStateToProps, {fetchProducts})(Home); 30 | -------------------------------------------------------------------------------- /client/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /client/src/routes/Routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Switch, Route } from 'react-router-dom'; 4 | 5 | // Components 6 | import Home from '../pages/Home/Home'; 7 | import Cart from '../pages/Cart/Cart'; 8 | import Checkout from '../pages/Checkout/Checkout'; 9 | 10 | const Routes = () => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default Routes; -------------------------------------------------------------------------------- /client/src/store/actions/cart.js: -------------------------------------------------------------------------------- 1 | import { ADD_TO_CART, REMOVE_FROM_CART, REMOVE_WHOLE_ITEM } from './types'; 2 | 3 | export const addToCart = (item, i) => ({ 4 | type: ADD_TO_CART, 5 | payload: item, 6 | index: i 7 | }); 8 | 9 | export const removeFromCart = item => ({ 10 | type: REMOVE_FROM_CART, 11 | payload: item 12 | }); 13 | 14 | export const removeWholeItem = item => ({ 15 | type: REMOVE_WHOLE_ITEM, 16 | payload: item 17 | }) 18 | 19 | -------------------------------------------------------------------------------- /client/src/store/actions/products.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { FETCH_PRODUCTS } from './types'; 3 | 4 | export const fetchProducts = () => async dispatch => { 5 | try { 6 | const res = await axios("/api/products/") 7 | dispatch({ 8 | type: FETCH_PRODUCTS, 9 | payload: res.data.products 10 | }) 11 | } catch (err) { 12 | console.log(err.message); 13 | } 14 | }; -------------------------------------------------------------------------------- /client/src/store/actions/types.js: -------------------------------------------------------------------------------- 1 | // cart 2 | export const ADD_TO_CART = 'ADD_TO_CART'; 3 | export const REMOVE_FROM_CART = 'REMOVE_FROM_CART'; 4 | export const REMOVE_WHOLE_ITEM = 'REMOVE_WHOLE_ITEM'; 5 | // products 6 | export const FETCH_PRODUCTS = 'FETCH_PRODUCTS'; -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from './reducers'; 4 | 5 | const initialState = {}; 6 | 7 | const middleware = [thunk]; 8 | 9 | const store = createStore( 10 | rootReducer, 11 | initialState, 12 | compose( 13 | applyMiddleware(...middleware), 14 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 15 | ) 16 | ); 17 | 18 | export default store; 19 | -------------------------------------------------------------------------------- /client/src/store/reducers/cart.js: -------------------------------------------------------------------------------- 1 | import { ADD_TO_CART, REMOVE_FROM_CART, REMOVE_WHOLE_ITEM } from '../actions/types'; 2 | 3 | const initialState = []; 4 | 5 | const cartWithoutItem = (cart, item) => cart.filter(cartItem => cartItem._id !== item._id); 6 | const itemInCart = (cart, item) => cart.filter(cartItem => cartItem._id === item._id)[0]; 7 | 8 | const addToCart = (cart, item) => { 9 | const cartItem = itemInCart(cart, item); 10 | return cartItem === undefined 11 | ? [...cartWithoutItem(cart, item), { ...item, quantity: 1 }] 12 | : [...cartWithoutItem(cart, item), { ...cartItem, quantity: cartItem.quantity + 1 }] 13 | }; 14 | 15 | const removeFromCart = (cart, item) => { 16 | return item.quantity === 1 17 | ? [...cartWithoutItem(cart, item)] 18 | : [...cartWithoutItem(cart, item), {...item, quantity: item.quantity - 1}] 19 | }; 20 | 21 | const removeWholeItem = (cart, item) => { 22 | return [...cartWithoutItem(cart, item)] 23 | }; 24 | export default (state = initialState, action) => { 25 | switch (action.type) { 26 | case ADD_TO_CART: 27 | return addToCart(state, action.payload); 28 | case REMOVE_FROM_CART: 29 | return removeFromCart(state, action.payload); 30 | case REMOVE_WHOLE_ITEM: 31 | return removeWholeItem(state, action.payload) 32 | default: 33 | return state; 34 | } 35 | }; 36 | 37 | 38 | -------------------------------------------------------------------------------- /client/src/store/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import productsReducer from './products'; 3 | import cartReducer from './cart'; 4 | 5 | const rootReducer = combineReducers({ 6 | products: productsReducer, 7 | cart: cartReducer 8 | }); 9 | 10 | export default rootReducer; -------------------------------------------------------------------------------- /client/src/store/reducers/products.js: -------------------------------------------------------------------------------- 1 | import { FETCH_PRODUCTS } from '../actions/types'; 2 | 3 | const initialState = []; 4 | 5 | export default (state = initialState, action) => { 6 | switch (action.type) { 7 | case FETCH_PRODUCTS: 8 | return action.payload; 9 | default: 10 | return state; 11 | } 12 | } -------------------------------------------------------------------------------- /client/src/ui/BuySomething/BuySomething.css: -------------------------------------------------------------------------------- 1 | .Cart-Empty-Wrapper { 2 | display: flex; 3 | justify-content: center; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .Cart-Empty-Wrapper { 9 | flex-grow: 1; 10 | } 11 | 12 | .Cart-Empty { 13 | font-size: 3rem; 14 | } 15 | 16 | .Go-To-Products { 17 | text-decoration: underline; 18 | } -------------------------------------------------------------------------------- /client/src/ui/BuySomething/BuySomething.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./BuySomething.css"; 3 | import PropTypes from 'prop-types'; 4 | import { Link } from "react-router-dom"; 5 | 6 | const BuySomething = ({message="Cart empty!"}) => ( 7 |
8 | {" "} 9 |

{message}

{" "} 10 | 11 | Try to buy something 12 | 13 |
14 | ); 15 | 16 | BuySomething.propTypes = { 17 | message: PropTypes.string 18 | }; 19 | 20 | export default BuySomething; -------------------------------------------------------------------------------- /client/src/ui/NavBar/NavBar.css: -------------------------------------------------------------------------------- 1 | .NavBar-Wrapper { 2 | height: 70px; 3 | display: flex; 4 | justify-content: space-around; 5 | padding: 1.5rem; 6 | font-size: 14px; 7 | } 8 | 9 | .Cart-Info { 10 | position: relative; 11 | } 12 | 13 | .Cart-Item-Counter { 14 | position: absolute; 15 | width: 15px; 16 | height: 15px; 17 | border-radius: 100%; 18 | text-align: center; 19 | color: #fff; 20 | background-color: #a864a8; 21 | font-size: 9px; 22 | line-height: 14px; 23 | top: -5px; 24 | left: 13px; 25 | } -------------------------------------------------------------------------------- /client/src/ui/NavBar/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './NavBar.css'; 3 | 4 | import { connect } from 'react-redux'; 5 | import { Link } from 'react-router-dom'; 6 | 7 | const totalPrice = cart => { 8 | return cart.reduce( 9 | (accum, product) => accum + product.price * product.quantity, 10 | 0 11 | ); 12 | }; 13 | 14 | const NavBar = props => ( 15 | 29 | ); 30 | 31 | const mapStateToProps = state => ({ 32 | cart: state.cart 33 | }); 34 | 35 | export default connect(mapStateToProps, null)(NavBar); -------------------------------------------------------------------------------- /config/keys.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | MONGO_URL: process.env.MONGO_URL 3 | }; -------------------------------------------------------------------------------- /libs/db-connection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { MONGO_URL } = require('../config/keys'); 3 | // Allow Promises 4 | mongoose.Promise = global.Promise; 5 | // Connection 6 | mongoose.connect(MONGO_URL, { useNewUrlParser: true }); 7 | // Validation 8 | mongoose.connection 9 | .on('open', () => console.info('Database connected!')) 10 | .on('error', err => console.info('Create a database and put the link into config/index.js/MONGO_URL')); -------------------------------------------------------------------------------- /models/Product.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { Schema } = mongoose; 3 | 4 | const productSchema = new Schema({ 5 | name: { 6 | type: String, 7 | required: true 8 | }, 9 | price: { 10 | type: Number, 11 | required: true 12 | }, 13 | photo: { 14 | type: String, 15 | required: true 16 | } 17 | }); 18 | 19 | module.exports = mongoose.model('products', productSchema); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MERN-eCommerce", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "client-install": "cd client && npm install", 8 | "start": "node server.js", 9 | "server": "nodemon server.js", 10 | "client": "npm start --prefix client", 11 | "dev": "concurrently \"npm run server\" \"npm run client\"", 12 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "body-parser": "^1.18.3", 19 | "concurrently": "^3.6.1", 20 | "cors": "^2.8.4", 21 | "express": "^4.16.3", 22 | "helmet": "^3.13.0", 23 | "mongoose": "^5.2.5", 24 | "react-modal": "^3.5.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /routes/products.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const Products = require('../models/Product'); 5 | 6 | router.get('/', async (req, res) => { 7 | const products = await Products.find({}); 8 | res.json({ products }); 9 | }); 10 | 11 | module.exports = router; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const path = require('path'); 4 | // App Init 5 | const app = express(); 6 | 7 | // db 8 | require('./libs/db-connection'); 9 | 10 | const PORT = process.env.PORT || 5000; 11 | 12 | // Middlewares 13 | app.use(require('helmet')()); 14 | app.use(require('cors')()); 15 | app.use(bodyParser.urlencoded({ extended: false })); 16 | app.use(bodyParser.json()); 17 | // Routes 18 | app.use('/api/products', require('./routes/products')); 19 | 20 | // Production 21 | if (process.env.NODE_ENV === 'production') { 22 | // Set static folder 23 | app.use(express.static('client/build')); 24 | 25 | app.get('*', (req, res) => { 26 | res.sendfile(path.resolve(__dirname, 'client', 'build', 'index.html')); 27 | }); 28 | } 29 | 30 | app.listen(PORT, () => console.log(`Server running on port ${ PORT }`)); --------------------------------------------------------------------------------