├── .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 | {" "}
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 |

11 |
12 |
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 | Photo |
46 | Name |
47 | Quantity |
48 | Price / Unit |
49 | |
50 |
51 |
52 |
53 | {cartItems}
54 |
55 |
62 | Total
63 | |
64 | |
65 | |
66 |
67 | ${totalPrice(this.props.cart)}
68 |
69 | Checkout
70 |
71 | |
72 |
73 |
74 |
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 |
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 }`));
--------------------------------------------------------------------------------