Deployed on: (No longer available due to heroku free dyno plan has deprecated) https://ecommerce-ak.herokuapp.com/
84 | 7. raise a star to support me
85 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^4.11.0",
7 | "@material-ui/icons": "^4.9.1",
8 | "@testing-library/jest-dom": "^4.2.4",
9 | "@testing-library/react": "^9.5.0",
10 | "@testing-library/user-event": "^7.2.1",
11 | "braintree-web-drop-in-react": "^1.1.1",
12 | "config": "^3.3.1",
13 | "fontsource-roboto": "^3.0.3",
14 | "moment": "^2.27.0",
15 | "query-string": "^6.13.1",
16 | "react": "^16.13.1",
17 | "react-dom": "^16.13.1",
18 | "react-router-dom": "^5.2.0",
19 | "react-scripts": "5.0.1"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject"
26 | },
27 | "eslintConfig": {
28 | "extends": "react-app"
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">0.2%",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashraf-kabir/mern-ecommerce/152bd41eab387997fed84e8e4f1ff0e151a15066/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
21 |
22 |
26 |
27 |
31 |
32 | MERN ECOMMERCE
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashraf-kabir/mern-ecommerce/152bd41eab387997fed84e8e4f1ff0e151a15066/client/public/logo192.png
--------------------------------------------------------------------------------
/client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashraf-kabir/mern-ecommerce/152bd41eab387997fed84e8e4f1ff0e151a15066/client/public/logo512.png
--------------------------------------------------------------------------------
/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 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const App = () => Hello from ashraf kabir
;
4 |
5 | export default App;
6 |
--------------------------------------------------------------------------------
/client/src/Routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter, Switch, Route } from 'react-router-dom';
3 | import Signup from './user/Signup';
4 | import Signin from './user/Signin';
5 | import Home from './core/Home';
6 | import PrivateRoute from './auth/PrivateRoute';
7 | import Dashboard from './user/UserDashboard';
8 | import AdminRoute from './auth/AdminRoute';
9 | import AdminDashboard from './user/AdminDashboard';
10 | import AddCategory from './admin/AddCategory';
11 | import AddProduct from './admin/AddProduct';
12 | import Shop from './core/Shop';
13 | import Product from './core/Product';
14 | import Cart from './core/Cart';
15 | import Orders from './admin/Orders';
16 | import Profile from './user/Profile';
17 | import ManageProducts from './admin/ManageProducts';
18 | import UpdateProduct from './admin/UpdateProduct';
19 | import CategoryList from './admin/CategoryList';
20 | import NotFound from './core/NotFound';
21 |
22 | const Routes = () => {
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default Routes;
52 |
--------------------------------------------------------------------------------
/client/src/admin/AddCategory.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Layout from '../core/Layout';
3 | import { isAuthenticated } from '../auth';
4 | import { Link } from 'react-router-dom';
5 | import { createCategory } from './apiAdmin';
6 |
7 | const AddCategory = () => {
8 | const [name, setName] = useState('');
9 | const [error, setError] = useState(false);
10 | const [success, setSuccess] = useState(false);
11 |
12 | // destructure user and token from localstorage
13 | const { user, token } = isAuthenticated();
14 |
15 | const handleChange = (e) => {
16 | setError('');
17 | setName(e.target.value);
18 | };
19 |
20 | const clickSubmit = (e) => {
21 | e.preventDefault();
22 | setError('');
23 | setSuccess(false);
24 | // make request to api to create category
25 | createCategory(user._id, token, { name }).then((data) => {
26 | if (data.error) {
27 | setError(data.error);
28 | } else {
29 | setError('');
30 | setSuccess(true);
31 | }
32 | });
33 | };
34 |
35 | const newCategoryForm = () => (
36 |
50 | );
51 |
52 | const showSuccess = () => {
53 | if (success) {
54 | return {name} is created
;
55 | }
56 | };
57 |
58 | const showError = () => {
59 | if (error) {
60 | return Category should be unique
;
61 | }
62 | };
63 |
64 | const goBack = () => (
65 |
66 |
67 | Back to Dashboard
68 |
69 |
70 | );
71 |
72 | return (
73 |
77 |
78 |
79 | {showSuccess()}
80 | {showError()}
81 | {newCategoryForm()}
82 | {goBack()}
83 |
84 |
85 |
86 | );
87 | };
88 |
89 | export default AddCategory;
90 |
--------------------------------------------------------------------------------
/client/src/admin/AddProduct.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Layout from '../core/Layout';
3 | import { isAuthenticated } from '../auth';
4 | import { createProduct, getCategories } from './apiAdmin';
5 |
6 | const AddProduct = () => {
7 | const [values, setValues] = useState({
8 | name: '',
9 | description: '',
10 | price: '',
11 | categories: [],
12 | category: '',
13 | shipping: '',
14 | quantity: '',
15 | photo: '',
16 | loading: false,
17 | error: '',
18 | createdProduct: '',
19 | redirectToProfile: false,
20 | formData: '',
21 | });
22 |
23 | const { user, token } = isAuthenticated();
24 |
25 | const {
26 | name,
27 | description,
28 | price,
29 | categories,
30 | category,
31 | shipping,
32 | quantity,
33 | photo,
34 | loading,
35 | error,
36 | createdProduct,
37 | redirectToProfile,
38 | formData,
39 | } = values;
40 |
41 | // load categories and set form data
42 | const init = () => {
43 | getCategories().then((data) => {
44 | if (data.error) {
45 | setValues({ ...values, error: data.error });
46 | } else {
47 | setValues({
48 | ...values,
49 | categories: data,
50 | formData: new FormData(),
51 | });
52 | }
53 | });
54 | };
55 |
56 | useEffect(() => {
57 | init();
58 | }, []);
59 |
60 | const handleChange = (name) => (event) => {
61 | const value = name === 'photo' ? event.target.files[0] : event.target.value;
62 | formData.set(name, value);
63 | setValues({ ...values, [name]: value });
64 | };
65 |
66 | const clickSubmit = (event) => {
67 | event.preventDefault();
68 | setValues({ ...values, error: '', loading: true });
69 |
70 | createProduct(user._id, token, formData).then((data) => {
71 | if (data.error) {
72 | setValues({ ...values, error: data.error });
73 | } else {
74 | setValues({
75 | ...values,
76 | name: '',
77 | description: '',
78 | photo: '',
79 | price: '',
80 | quantity: '',
81 | loading: false,
82 | createdProduct: data.name,
83 | });
84 | }
85 | });
86 | };
87 |
88 | const newPostForm = () => (
89 |
165 | );
166 |
167 | const showError = () => (
168 |
172 | {error}
173 |
174 | );
175 |
176 | const showSuccess = () => (
177 |
181 |
{`${createdProduct}`} is created!
182 |
183 | );
184 |
185 | const showLoading = () =>
186 | loading && (
187 |
188 |
Loading...
189 |
190 | );
191 |
192 | return (
193 |
197 |
198 |
199 | {showLoading()}
200 | {showSuccess()}
201 | {showError()}
202 | {newPostForm()}
203 |
204 |
205 |
206 | );
207 | };
208 |
209 | export default AddProduct;
210 |
--------------------------------------------------------------------------------
/client/src/admin/CategoryList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Layout from '../core/Layout';
3 | import { isAuthenticated } from '../auth';
4 | import { getCategories } from './apiAdmin';
5 |
6 | const CategoryList = () => {
7 | const { user } = isAuthenticated();
8 |
9 | const [categories, setCategories] = React.useState([]);
10 |
11 | const loadCategories = () => {
12 | getCategories().then((data) => {
13 | if (data.error) {
14 | console.log(data.error);
15 | } else {
16 | setCategories(data);
17 | }
18 | });
19 | };
20 |
21 | React.useEffect(() => {
22 | loadCategories();
23 | }, []);
24 |
25 | return (
26 |
30 |
31 |
32 |
Total {categories.length} categories
33 |
34 |
35 | {categories.length > 0 ? (
36 | categories.map((c, i) => (
37 | -
38 | {c.name}
39 |
40 | ))
41 | ) : (
42 | No categories found
43 | )}
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default CategoryList;
52 |
--------------------------------------------------------------------------------
/client/src/admin/ManageProducts.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Layout from '../core/Layout';
3 | import { isAuthenticated } from '../auth';
4 | import { Link } from 'react-router-dom';
5 | import { getProducts, deleteProduct } from './apiAdmin';
6 |
7 | const ManageProducts = () => {
8 | const [products, setProducts] = useState([]);
9 |
10 | const { user, token } = isAuthenticated();
11 |
12 | const loadProducts = () => {
13 | getProducts().then((data) => {
14 | if (data.error) {
15 | console.log(data.error);
16 | } else {
17 | setProducts(data);
18 | }
19 | });
20 | };
21 |
22 | const destroy = (productId) => {
23 | deleteProduct(productId, user._id, token).then((data) => {
24 | if (data.error) {
25 | console.log(data.error);
26 | } else {
27 | loadProducts();
28 | }
29 | });
30 | };
31 |
32 | useEffect(() => {
33 | loadProducts();
34 | }, []);
35 |
36 | return (
37 |
42 |
43 |
44 |
Total {products.length} products
45 |
46 |
47 | {products.map((p, i) => (
48 | -
52 | {p.name}
53 |
54 | Update
55 |
56 |
57 | destroy(p._id)}
59 | className='badge badge-danger badge-pill'
60 | >
61 | Delete
62 |
63 |
64 |
65 | ))}
66 |
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | export default ManageProducts;
74 |
--------------------------------------------------------------------------------
/client/src/admin/Orders.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Layout from '../core/Layout';
3 | import { isAuthenticated } from '../auth';
4 | import { listOrders, getStatusValues, updateOrderStatus } from './apiAdmin';
5 | import moment from 'moment';
6 |
7 | const Orders = () => {
8 | const [orders, setOrders] = useState([]);
9 | const [statusValues, setStatusValues] = useState([]);
10 | const { user, token } = isAuthenticated();
11 |
12 | const loadOrders = () => {
13 | listOrders(user._id, token).then((data) => {
14 | if (data.error) {
15 | console.log(data.error);
16 | } else {
17 | setOrders(data);
18 | }
19 | });
20 | };
21 |
22 | const loadStatusValues = () => {
23 | getStatusValues(user._id, token).then((data) => {
24 | if (data.error) {
25 | console.log(data.error);
26 | } else {
27 | setStatusValues(data);
28 | }
29 | });
30 | };
31 |
32 | useEffect(() => {
33 | loadOrders();
34 | loadStatusValues();
35 | }, []);
36 |
37 | const showOrdersLength = () => {
38 | if (orders.length > 0) {
39 | return (
40 | Total orders: {orders.length}
41 | );
42 | } else {
43 | return No orders
;
44 | }
45 | };
46 |
47 | const showInput = (key, value) => (
48 |
54 | );
55 |
56 | const handleStatusChange = (e, orderId) => {
57 | updateOrderStatus(user._id, token, orderId, e.target.value).then((data) => {
58 | if (data.error) {
59 | console.log('Status update failed');
60 | } else {
61 | loadOrders();
62 | }
63 | });
64 | // console.log('update order status');
65 | };
66 |
67 | const showStatus = (o) => (
68 |
69 |
Status: {o.status}
70 |
81 |
82 | );
83 |
84 | return (
85 |
89 |
90 |
91 | {showOrdersLength()}
92 |
93 | {orders.map((o, oIndex) => {
94 | return (
95 |
100 |
101 | Order ID: {o._id}
102 |
103 |
104 |
105 | - {showStatus(o)}
106 | -
107 | Transaction ID: {o.transaction_id}
108 |
109 | - Amount: ${o.amount}
110 | - Ordered by: {o.user.name}
111 | -
112 | Ordered on: {moment(o.createdAt).fromNow()}
113 |
114 | -
115 | Delivery address: {o.address}
116 |
117 |
118 |
119 |
120 | Total products in the order: {o.products.length}
121 |
122 |
123 | {o.products.map((p, pIndex) => (
124 |
129 | {showInput('Product name', p.name)}
130 | {showInput('Product price', p.price)}
131 | {showInput('Product total', p.count)}
132 | {showInput('Product Id', p._id)}
133 |
134 | ))}
135 |
136 | );
137 | })}
138 |
139 |
140 |
141 | );
142 | };
143 |
144 | export default Orders;
145 |
--------------------------------------------------------------------------------
/client/src/admin/ProductList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Layout from '../core/Layout';
3 | import { isAuthenticated } from '../auth';
4 | import { getProducts } from './apiAdmin';
5 |
6 | const ProductList = () => {
7 | const { user } = isAuthenticated();
8 | const [products, setProducts] = React.useState([]);
9 |
10 | const loadProducts = () => {
11 | getProducts().then((data) => {
12 | if (data.error) {
13 | console.log(data.error);
14 | } else {
15 | setProducts(data);
16 | }
17 | });
18 | };
19 |
20 | React.useEffect(() => {
21 | loadProducts();
22 | }, []);
23 |
24 | return (
25 |
29 |
30 |
31 |
Total {products.length} products
32 |
33 |
34 | {products.length > 0 ? (
35 | products.map((p, i) => (
36 | -
37 | {p.name}
38 |
39 | ))
40 | ) : (
41 | No products found
42 | )}
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default ProductList;
51 |
--------------------------------------------------------------------------------
/client/src/admin/UpdateProduct.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Layout from '../core/Layout';
3 | import { isAuthenticated } from '../auth';
4 | import { Redirect } from 'react-router-dom';
5 | import { getProduct, getCategories, updateProduct } from './apiAdmin';
6 |
7 | const UpdateProduct = ({ match }) => {
8 | const [values, setValues] = useState({
9 | name: '',
10 | description: '',
11 | price: '',
12 | categories: [],
13 | category: '',
14 | shipping: '',
15 | quantity: '',
16 | photo: '',
17 | loading: false,
18 | error: false,
19 | createdProduct: '',
20 | redirectToProfile: false,
21 | formData: '',
22 | });
23 | const [categories, setCategories] = useState([]);
24 |
25 | const { user, token } = isAuthenticated();
26 | const {
27 | name,
28 | description,
29 | price,
30 | // categories,
31 | // category,
32 | // shipping,
33 | quantity,
34 | loading,
35 | error,
36 | createdProduct,
37 | redirectToProfile,
38 | formData,
39 | } = values;
40 |
41 | const init = (productId) => {
42 | getProduct(productId).then((data) => {
43 | if (data.error) {
44 | setValues({ ...values, error: data.error });
45 | } else {
46 | // populate the state
47 | setValues({
48 | ...values,
49 | name: data.name,
50 | description: data.description,
51 | price: data.price,
52 | category: data.category._id,
53 | shipping: data.shipping,
54 | quantity: data.quantity,
55 | formData: new FormData(),
56 | });
57 | // load categories
58 | initCategories();
59 | }
60 | });
61 | };
62 |
63 | // load categories and set form data
64 | const initCategories = () => {
65 | getCategories().then((data) => {
66 | if (data.error) {
67 | setValues({ ...values, error: data.error });
68 | } else {
69 | setCategories(data);
70 | }
71 | });
72 | };
73 |
74 | useEffect(() => {
75 | init(match.params.productId);
76 | }, []);
77 |
78 | const handleChange = (name) => (event) => {
79 | const value = name === 'photo' ? event.target.files[0] : event.target.value;
80 | formData.set(name, value);
81 | setValues({ ...values, [name]: value });
82 | };
83 |
84 | const clickSubmit = (event) => {
85 | event.preventDefault();
86 | setValues({ ...values, error: '', loading: true });
87 |
88 | updateProduct(match.params.productId, user._id, token, formData).then(
89 | (data) => {
90 | if (data.error) {
91 | setValues({ ...values, error: data.error });
92 | } else {
93 | setValues({
94 | ...values,
95 | name: '',
96 | description: '',
97 | photo: '',
98 | price: '',
99 | quantity: '',
100 | loading: false,
101 | error: false,
102 | redirectToProfile: true,
103 | createdProduct: data.name,
104 | });
105 | }
106 | }
107 | );
108 | };
109 |
110 | const newPostForm = () => (
111 |
187 | );
188 |
189 | const showError = () => (
190 |
194 | {error}
195 |
196 | );
197 |
198 | const showSuccess = () => (
199 |
203 |
{`${createdProduct}`} is updated!
204 |
205 | );
206 |
207 | const showLoading = () =>
208 | loading && (
209 |
210 |
Loading...
211 |
212 | );
213 |
214 | const redirectUser = () => {
215 | if (redirectToProfile) {
216 | if (!error) {
217 | return ;
218 | }
219 | }
220 | };
221 |
222 | return (
223 |
227 |
228 |
229 | {showLoading()}
230 | {showSuccess()}
231 | {showError()}
232 | {newPostForm()}
233 | {redirectUser()}
234 |
235 |
236 |
237 | );
238 | };
239 |
240 | export default UpdateProduct;
241 |
--------------------------------------------------------------------------------
/client/src/admin/apiAdmin.js:
--------------------------------------------------------------------------------
1 | import { API } from '../config';
2 |
3 | export const createCategory = (userId, token, category) => {
4 | return fetch(`${API}/category/create/${userId}`, {
5 | method: 'POST',
6 | headers: {
7 | Accept: 'application/json',
8 | 'Content-Type': 'application/json',
9 | Authorization: `Bearer ${token}`,
10 | },
11 | body: JSON.stringify(category),
12 | })
13 | .then((response) => {
14 | return response.json();
15 | })
16 | .catch((err) => {
17 | console.log(err);
18 | });
19 | };
20 |
21 | export const createProduct = (userId, token, product) => {
22 | return fetch(`${API}/product/create/${userId}`, {
23 | method: 'POST',
24 | headers: {
25 | Accept: 'application/json',
26 | Authorization: `Bearer ${token}`,
27 | },
28 | body: product,
29 | })
30 | .then((response) => {
31 | return response.json();
32 | })
33 | .catch((err) => {
34 | console.log(err);
35 | });
36 | };
37 |
38 | export const getCategories = () => {
39 | return fetch(`${API}/categories`, {
40 | method: 'GET',
41 | })
42 | .then((response) => {
43 | return response.json();
44 | })
45 | .catch((err) => console.log(err));
46 | };
47 |
48 | export const listOrders = (userId, token) => {
49 | return fetch(`${API}/order/list/${userId}`, {
50 | method: 'GET',
51 | headers: {
52 | Accept: 'application/json',
53 | Authorization: `Bearer ${token}`,
54 | },
55 | })
56 | .then((response) => {
57 | return response.json();
58 | })
59 | .catch((err) => console.log(err));
60 | };
61 |
62 | export const getStatusValues = (userId, token) => {
63 | return fetch(`${API}/order/status-values/${userId}`, {
64 | method: 'GET',
65 | headers: {
66 | Accept: 'application/json',
67 | Authorization: `Bearer ${token}`,
68 | },
69 | })
70 | .then((response) => {
71 | return response.json();
72 | })
73 | .catch((err) => console.log(err));
74 | };
75 |
76 | export const updateOrderStatus = (userId, token, orderId, status) => {
77 | return fetch(`${API}/order/${orderId}/status/${userId}`, {
78 | method: 'PUT',
79 | headers: {
80 | Accept: 'application/json',
81 | 'Content-Type': 'application/json',
82 | Authorization: `Bearer ${token}`,
83 | },
84 | body: JSON.stringify({ status, orderId }),
85 | })
86 | .then((response) => {
87 | return response.json();
88 | })
89 | .catch((err) => console.log(err));
90 | };
91 |
92 | /**
93 | * to perform crud on product
94 | * get all products
95 | * get a single product
96 | * update single product
97 | * delete single product
98 | */
99 |
100 | export const getProducts = () => {
101 | return fetch(`${API}/products?limit=undefined`, {
102 | method: 'GET',
103 | })
104 | .then((response) => {
105 | return response.json();
106 | })
107 | .catch((err) => console.log(err));
108 | };
109 |
110 | export const deleteProduct = (productId, userId, token) => {
111 | return fetch(`${API}/product/${productId}/${userId}`, {
112 | method: 'DELETE',
113 | headers: {
114 | Accept: 'application/json',
115 | 'Content-Type': 'application/json',
116 | Authorization: `Bearer ${token}`,
117 | },
118 | })
119 | .then((response) => {
120 | return response.json();
121 | })
122 | .catch((err) => console.log(err));
123 | };
124 |
125 | export const getProduct = (productId) => {
126 | return fetch(`${API}/product/${productId}`, {
127 | method: 'GET',
128 | })
129 | .then((response) => {
130 | return response.json();
131 | })
132 | .catch((err) => console.log(err));
133 | };
134 |
135 | export const updateProduct = (productId, userId, token, product) => {
136 | return fetch(`${API}/product/${productId}/${userId}`, {
137 | method: 'PUT',
138 | headers: {
139 | Accept: 'application/json',
140 | Authorization: `Bearer ${token}`,
141 | },
142 | body: product,
143 | })
144 | .then((response) => {
145 | return response.json();
146 | })
147 | .catch((err) => console.log(err));
148 | };
149 |
--------------------------------------------------------------------------------
/client/src/auth/AdminRoute.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Route, Redirect } from 'react-router-dom';
3 | import { isAuthenticated } from './index';
4 |
5 | const AdminRoute = ({ component: Component, ...rest }) => (
6 |
9 | isAuthenticated() && isAuthenticated().user.role === 1 ? (
10 |
11 | ) : (
12 |
18 | )
19 | }
20 | />
21 | );
22 |
23 | export default AdminRoute;
24 |
--------------------------------------------------------------------------------
/client/src/auth/PrivateRoute.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Route, Redirect } from 'react-router-dom';
3 | import { isAuthenticated } from './index';
4 |
5 | const PrivateRoute = ({ component: Component, ...rest }) => (
6 |
9 | isAuthenticated() ? (
10 |
11 | ) : (
12 |
18 | )
19 | }
20 | />
21 | );
22 |
23 | export default PrivateRoute;
24 |
--------------------------------------------------------------------------------
/client/src/auth/index.js:
--------------------------------------------------------------------------------
1 | import { API } from '../config';
2 |
3 | export const signup = (user) => {
4 | // console.log(name, email, password);
5 | return fetch(`${API}/signup`, {
6 | method: 'POST',
7 | headers: {
8 | Accept: 'application/json',
9 | 'Content-Type': 'application/json',
10 | },
11 | body: JSON.stringify(user),
12 | })
13 | .then((response) => {
14 | return response.json();
15 | })
16 | .catch((err) => {
17 | console.log(err);
18 | });
19 | };
20 |
21 | export const signin = (user) => {
22 | // console.log(name, email, password);
23 | return fetch(`${API}/signin`, {
24 | method: 'POST',
25 | headers: {
26 | Accept: 'application/json',
27 | 'Content-Type': 'application/json',
28 | },
29 | body: JSON.stringify(user),
30 | })
31 | .then((response) => {
32 | return response.json();
33 | })
34 | .catch((err) => {
35 | console.log(err);
36 | });
37 | };
38 |
39 | export const authenticate = (data, next) => {
40 | if (typeof window !== 'undefined') {
41 | localStorage.setItem('jwt', JSON.stringify(data));
42 | next();
43 | }
44 | };
45 |
46 | export const signout = (next) => {
47 | if (typeof window !== 'undefined') {
48 | localStorage.removeItem('jwt');
49 | next();
50 | return fetch(`${API}/signout`, {
51 | method: 'GET',
52 | })
53 | .then((response) => {
54 | console.log('signout', response);
55 | })
56 | .catch((err) => console.log(err));
57 | }
58 | };
59 |
60 | export const isAuthenticated = () => {
61 | if (typeof window === 'undefined') {
62 | return false;
63 | }
64 | if (localStorage.getItem('jwt')) {
65 | return JSON.parse(localStorage.getItem('jwt'));
66 | } else {
67 | return false;
68 | }
69 | };
70 |
--------------------------------------------------------------------------------
/client/src/config.js:
--------------------------------------------------------------------------------
1 | export const API = process.env.REACT_APP_API_URL || 'https://ecommerce-ak.herokuapp.com/api';
2 |
--------------------------------------------------------------------------------
/client/src/core/Card.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Redirect } from 'react-router-dom';
3 | import ShowImage from './ShowImage';
4 | import moment from 'moment';
5 |
6 | import AppBar from '@material-ui/core/AppBar';
7 | import Button from '@material-ui/core/Button';
8 | import CameraIcon from '@material-ui/icons/PhotoCamera';
9 | import CardM from '@material-ui/core/Card';
10 | import CardActions from '@material-ui/core/CardActions';
11 | import CardContent from '@material-ui/core/CardContent';
12 | import CardMedia from '@material-ui/core/CardMedia';
13 | import CssBaseline from '@material-ui/core/CssBaseline';
14 | import DeleteIcon from '@material-ui/icons/Delete';
15 | import Grid from '@material-ui/core/Grid';
16 | import Toolbar from '@material-ui/core/Toolbar';
17 | import Typography from '@material-ui/core/Typography';
18 | import { makeStyles } from '@material-ui/core/styles';
19 | import Container from '@material-ui/core/Container';
20 | import Link from '@material-ui/core/Link';
21 |
22 | import { addItem, updateItem, removeItem } from './cartHelpers';
23 |
24 | const useStyles = makeStyles((theme) => ({
25 | icon: {
26 | marginRight: theme.spacing(2),
27 | },
28 | heroContent: {
29 | backgroundColor: theme.palette.background.paper,
30 | padding: theme.spacing(8, 0, 6),
31 | },
32 | heroButtons: {
33 | marginTop: theme.spacing(4),
34 | },
35 | cardGrid: {
36 | paddingTop: theme.spacing(4),
37 | paddingBottom: theme.spacing(4),
38 | },
39 | card: {
40 | height: '100%',
41 | display: 'flex',
42 | flexDirection: 'column',
43 | },
44 | cardMedia: {
45 | paddingTop: '56.25%', // 16:9
46 | },
47 | cardContent: {
48 | flexGrow: 1,
49 | },
50 | productDescription: {
51 | height: '100px',
52 | },
53 | footer: {
54 | backgroundColor: theme.palette.background.paper,
55 | padding: theme.spacing(6),
56 | },
57 | }));
58 |
59 | const Card = ({
60 | product,
61 | showViewProductButton = true,
62 | showAddToCartButton = true,
63 | cartUpdate = false,
64 | showRemoveProductButton = false,
65 | setRun = (f) => f, // default value of function
66 | run = undefined, // default value of undefined
67 | }) => {
68 | const [redirect, setRedirect] = useState(false);
69 | const [count, setCount] = useState(product.count);
70 |
71 | const showViewButton = (showViewProductButton) => {
72 | return (
73 | showViewProductButton && (
74 |
75 |
78 |
79 | )
80 | );
81 | };
82 |
83 | const addToCart = () => {
84 | // console.log('added');
85 | addItem(product, setRedirect(true));
86 | };
87 |
88 | const shouldRedirect = (redirect) => {
89 | if (redirect) {
90 | return ;
91 | }
92 | };
93 |
94 | const showAddToCartBtn = (showAddToCartButton) => {
95 | return (
96 | showAddToCartButton && (
97 |
100 | )
101 | );
102 | };
103 |
104 | const showStock = (quantity) => {
105 | return quantity > 0 ? (
106 | In Stock
107 | ) : (
108 | Out of Stock
109 | );
110 | };
111 |
112 | const handleChange = (productId) => (event) => {
113 | setRun(!run); // run useEffect in parent Cart
114 | setCount(event.target.value < 1 ? 1 : event.target.value);
115 | if (event.target.value >= 1) {
116 | updateItem(productId, event.target.value);
117 | }
118 | };
119 |
120 | const showCartUpdateOptions = (cartUpdate) => {
121 | return (
122 | cartUpdate && (
123 |
124 |
125 |
126 | Adjust Quantity
127 |
128 |
134 |
135 |
136 | )
137 | );
138 | };
139 |
140 | const showRemoveButton = (showRemoveProductButton) => {
141 | return (
142 | showRemoveProductButton && (
143 |
155 | )
156 | );
157 | };
158 |
159 | const classes = useStyles();
160 |
161 | return (
162 | //
163 | //
{product.name}
164 | //
165 | // {shouldRedirect(redirect)}
166 | //
167 | //
{product.description.substring(0, 100)}
168 | //
${product.price}
169 | //
170 | // Category: {product.category && product.category.name}
171 | //
172 | //
173 | // Added on {moment(product.createdAt).fromNow()}
174 | //
175 |
176 | // {showStock(product.quantity)}
177 | //
178 |
179 | // {showViewButton(showViewProductButton)}
180 |
181 | // {showAddToCartBtn(showAddToCartButton)}
182 |
183 | // {showRemoveButton(showRemoveProductButton)}
184 |
185 | // {showCartUpdateOptions(cartUpdate)}
186 | //
187 | //
188 |
189 |
190 |
191 |
192 |
193 |
194 | {shouldRedirect(redirect)}
195 |
196 |
197 |
198 | {product.name}
199 |
200 | {product.description.substring(0, 100)}
201 | Price: ${product.price}
202 |
203 | Category: {product.category && product.category.name}{' '}
204 |
{' '}
205 |
206 | Added on {moment(product.createdAt).fromNow()}{' '}
207 |
208 | {showStock(product.quantity)}
209 |
210 |
211 | {showViewButton(showViewProductButton)}
212 | {showAddToCartBtn(showAddToCartButton)}
213 | {showRemoveButton(showRemoveProductButton)}
214 |
215 | {showCartUpdateOptions(cartUpdate)}
216 |
217 |
218 |
219 |
220 |
221 | );
222 | };
223 |
224 | export default Card;
225 |
--------------------------------------------------------------------------------
/client/src/core/Cart.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import Layout from './Layout';
4 | import { getCart } from './cartHelpers';
5 | import Card from './Card';
6 | import Checkout from './Checkout';
7 |
8 | import Copyright from './Copyright';
9 |
10 | const Cart = () => {
11 | const [items, setItems] = useState([]);
12 | const [run, setRun] = useState(false);
13 |
14 | useEffect(() => {
15 | setItems(getCart());
16 | }, [run]);
17 |
18 | const showItems = (items) => {
19 | return (
20 |
21 |
Your cart has {`${items.length}`} items
22 |
23 | {items.map((product, i) => (
24 |
33 | ))}
34 |
35 | );
36 | };
37 |
38 | const noItemsMessage = () => (
39 |
40 | Your cart is empty.
Continue shopping
41 |
42 | );
43 |
44 | return (
45 |
50 |
51 |
52 |
53 | {items.length > 0 ? showItems(items) : noItemsMessage()}
54 |
55 |
56 |
Your cart summary
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default Cart;
68 |
--------------------------------------------------------------------------------
/client/src/core/Checkbox.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import CheckboxM from '@material-ui/core/Checkbox';
3 |
4 | const Checkbox = ({ categories, handleFilters }) => {
5 | const [checked, setCheked] = useState([]);
6 |
7 | const handleToggle = (c) => () => {
8 | // return the first index or -1
9 | const currentCategoryId = checked.indexOf(c);
10 | const newCheckedCategoryId = [...checked];
11 | // if currently checked was not already in checked state > push
12 | // else pull/take off
13 | if (currentCategoryId === -1) {
14 | newCheckedCategoryId.push(c);
15 | } else {
16 | newCheckedCategoryId.splice(currentCategoryId, 1);
17 | }
18 | // console.log(newCheckedCategoryId);
19 | setCheked(newCheckedCategoryId);
20 | handleFilters(newCheckedCategoryId);
21 | };
22 |
23 | return categories.map((c, i) => (
24 |
25 |
29 |
30 |
31 | ));
32 | };
33 |
34 | export default Checkbox;
35 |
--------------------------------------------------------------------------------
/client/src/core/Checkout.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Button from '@material-ui/core/Button';
3 | import {
4 | getProducts,
5 | getBraintreeClientToken,
6 | processPayment,
7 | createOrder,
8 | } from './apiCore';
9 | import { emptyCart } from './cartHelpers';
10 | import Card from './Card';
11 | import { isAuthenticated } from '../auth';
12 | import { Link } from 'react-router-dom';
13 | import DropIn from 'braintree-web-drop-in-react';
14 |
15 | const Checkout = ({ products, setRun = (f) => f, run = undefined }) => {
16 | const [data, setData] = useState({
17 | loading: false,
18 | success: false,
19 | clientToken: null,
20 | error: '',
21 | instance: {},
22 | address: '',
23 | });
24 |
25 | const userId = isAuthenticated() && isAuthenticated().user._id;
26 | const token = isAuthenticated() && isAuthenticated().token;
27 |
28 | const getToken = (userId, token) => {
29 | getBraintreeClientToken(userId, token).then((data) => {
30 | if (data.error) {
31 | console.log(data.error);
32 | setData({ ...data, error: data.error });
33 | } else {
34 | console.log(data);
35 | setData({ clientToken: data.clientToken });
36 | }
37 | });
38 | };
39 |
40 | useEffect(() => {
41 | getToken(userId, token);
42 | }, []);
43 |
44 | const handleAddress = (event) => {
45 | setData({ ...data, address: event.target.value });
46 | };
47 |
48 | const getTotal = () => {
49 | return products.reduce((currentValue, nextValue) => {
50 | return currentValue + nextValue.count * nextValue.price;
51 | }, 0);
52 | };
53 |
54 | const showCheckout = () => {
55 | return isAuthenticated() ? (
56 | {showDropIn()}
57 | ) : (
58 |
59 |
62 |
63 | );
64 | };
65 |
66 | let deliveryAddress = data.address;
67 |
68 | const buy = () => {
69 | setData({ loading: true });
70 | // send the nonce to your server
71 | // nonce = data.instance.requestPaymentMethod()
72 | let nonce;
73 | let getNonce = data.instance
74 | .requestPaymentMethod()
75 | .then((data) => {
76 | // console.log(data);
77 | nonce = data.nonce;
78 | // once you have nonce (card type, card number) send nonce as 'paymentMethodNonce'
79 | // and also total to be charged
80 | // console.log(
81 | // "send nonce and total to process: ",
82 | // nonce,
83 | // getTotal(products)
84 | // );
85 | const paymentData = {
86 | paymentMethodNonce: nonce,
87 | amount: getTotal(products),
88 | };
89 |
90 | processPayment(userId, token, paymentData)
91 | .then((response) => {
92 | console.log(response);
93 | // empty cart
94 | // create order
95 |
96 | const createOrderData = {
97 | products: products,
98 | transaction_id: response.transaction.id,
99 | amount: response.transaction.amount,
100 | address: deliveryAddress,
101 | };
102 |
103 | createOrder(userId, token, createOrderData)
104 | .then((response) => {
105 | emptyCart(() => {
106 | setRun(!run); // run useEffect in parent Cart
107 | console.log('payment success and empty cart');
108 | setData({
109 | loading: false,
110 | success: true,
111 | });
112 | });
113 | })
114 | .catch((error) => {
115 | console.log(error);
116 | setData({ loading: false });
117 | });
118 | })
119 | .catch((error) => {
120 | console.log(error);
121 | setData({ loading: false });
122 | });
123 | })
124 | .catch((error) => {
125 | // console.log("dropin error: ", error);
126 | setData({ ...data, error: error.message });
127 | });
128 | };
129 |
130 | const showDropIn = () => (
131 | setData({ ...data, error: '' })}>
132 | {data.clientToken !== null && products.length > 0 ? (
133 |
134 |
135 |
136 |
142 |
143 |
144 |
(data.instance = instance)}
152 | />
153 |
156 |
157 | ) : null}
158 |
159 | );
160 |
161 | const showError = (error) => (
162 |
166 | {error}
167 |
168 | );
169 |
170 | const showSuccess = (success) => (
171 |
175 | Thanks! Your payment was successful!
176 |
177 | );
178 |
179 | const showLoading = (loading) =>
180 | loading && Loading...
;
181 |
182 | return (
183 |
184 |
Total: ${getTotal()}
185 | {showLoading(data.loading)}
186 | {showSuccess(data.success)}
187 | {showError(data.error)}
188 | {showCheckout()}
189 |
190 | );
191 | };
192 |
193 | export default Checkout;
194 |
--------------------------------------------------------------------------------
/client/src/core/Copyright.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Typography from '@material-ui/core/Typography';
3 | import Link from '@material-ui/core/Link';
4 | import Box from '@material-ui/core/Box';
5 |
6 | export default function Copyright() {
7 | return (
8 |
9 |
10 | {'Copyright © '}
11 |
12 | Ashraf Kabir
13 | {' '}
14 | {new Date().getFullYear()}
15 | {'.'}
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/core/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Layout from './Layout';
3 | import { getProducts } from './apiCore';
4 | import Card from './Card';
5 | import Search from './Search';
6 | import 'fontsource-roboto';
7 | import Copyright from './Copyright';
8 |
9 | const Home = () => {
10 | const [productsBySell, setProductsBySell] = useState([]);
11 | const [productsByArrival, setProductsByArrival] = useState([]);
12 | const [error, setError] = useState([]);
13 |
14 | const loadProductsBySell = () => {
15 | getProducts('sold').then((data) => {
16 | if (data.error) {
17 | setError(data.error);
18 | } else {
19 | setProductsBySell(data);
20 | }
21 | });
22 | };
23 |
24 | const loadProductsByArrival = () => {
25 | getProducts('createdAt').then((data) => {
26 | if (data.error) {
27 | setError(data.error);
28 | } else {
29 | setProductsByArrival(data);
30 | }
31 | });
32 | };
33 |
34 | useEffect(() => {
35 | loadProductsByArrival();
36 | loadProductsBySell();
37 | }, []);
38 |
39 | return (
40 |
45 |
46 |
47 |
48 |
49 |
New Arrivals
50 |
51 | {productsByArrival.map((product, i) => (
52 |
53 |
54 |
55 | ))}
56 |
57 |
58 |
Best Sellers
59 |
60 | {productsBySell.map((product, i) => (
61 |
62 |
63 |
64 | ))}
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | export default Home;
76 |
--------------------------------------------------------------------------------
/client/src/core/Layout.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Menu from './Menu';
3 | import '../styles.css';
4 |
5 | const Layout = ({
6 | title = 'Title',
7 | description = 'Description',
8 | className,
9 | children,
10 | }) => (
11 |
12 |
13 |
14 |
{title}
15 |
{description}
16 |
17 |
{children}
18 |
19 | );
20 |
21 | export default Layout;
22 |
--------------------------------------------------------------------------------
/client/src/core/Menu.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Link, withRouter, forceUpdate } from 'react-router-dom';
3 | import { signout, isAuthenticated } from '../auth';
4 | import { itemTotal } from './cartHelpers';
5 |
6 | import { fade, makeStyles } from '@material-ui/core/styles';
7 | import AppBar from '@material-ui/core/AppBar';
8 | import Toolbar from '@material-ui/core/Toolbar';
9 | import IconButton from '@material-ui/core/IconButton';
10 | import Typography from '@material-ui/core/Typography';
11 | import InputBase from '@material-ui/core/InputBase';
12 | import Badge from '@material-ui/core/Badge';
13 | import MenuItem from '@material-ui/core/MenuItem';
14 | import Menu from '@material-ui/core/Menu';
15 | import MenuIcon from '@material-ui/icons/Menu';
16 | import MoreIcon from '@material-ui/icons/MoreVert';
17 |
18 | import ShoppingCartIcon from '@material-ui/icons/ShoppingCart';
19 | import Button from '@material-ui/core/Button';
20 | import HomeIcon from '@material-ui/icons/Home';
21 | import StorefrontIcon from '@material-ui/icons/Storefront';
22 | import DashboardIcon from '@material-ui/icons/Dashboard';
23 | import AccountCircleIcon from '@material-ui/icons/AccountCircle';
24 | import PersonAddIcon from '@material-ui/icons/PersonAdd';
25 | import ExitToAppIcon from '@material-ui/icons/ExitToApp';
26 | import StoreIcon from '@material-ui/icons/Store';
27 |
28 | const isActive = (history, path) => {
29 | if (history.location.pathname === path) {
30 | return { color: '#ff9900', textDecoration: 'none' };
31 | } else {
32 | return { color: '#ffffff', textDecoration: 'none' };
33 | }
34 | };
35 |
36 | const useStyles = makeStyles((theme) => ({
37 | grow: {
38 | flexGrow: 1,
39 | },
40 | menuButton: {
41 | marginRight: theme.spacing(2),
42 | },
43 | title: {
44 | [theme.breakpoints.up('sm')]: {
45 | display: 'block',
46 | },
47 | },
48 | search: {
49 | position: 'relative',
50 | borderRadius: theme.shape.borderRadius,
51 | backgroundColor: fade(theme.palette.common.white, 0.15),
52 | '&:hover': {
53 | backgroundColor: fade(theme.palette.common.white, 0.25),
54 | },
55 | marginRight: theme.spacing(2),
56 | marginLeft: 0,
57 | width: '100%',
58 | [theme.breakpoints.up('sm')]: {
59 | marginLeft: theme.spacing(3),
60 | width: 'auto',
61 | },
62 | },
63 | searchIcon: {
64 | padding: theme.spacing(0, 2),
65 | height: '100%',
66 | position: 'absolute',
67 | pointerEvents: 'none',
68 | display: 'flex',
69 | alignItems: 'center',
70 | justifyContent: 'center',
71 | },
72 | inputRoot: {
73 | color: 'inherit',
74 | },
75 | inputInput: {
76 | padding: theme.spacing(1, 1, 1, 0),
77 | // vertical padding + font size from searchIcon
78 | paddingLeft: `calc(1em + ${theme.spacing(4)}px)`,
79 | transition: theme.transitions.create('width'),
80 | width: '100%',
81 | [theme.breakpoints.up('md')]: {
82 | width: '20ch',
83 | },
84 | },
85 | sectionDesktop: {
86 | display: 'none',
87 | [theme.breakpoints.up('md')]: {
88 | display: 'flex',
89 | },
90 | },
91 | sectionMobile: {
92 | display: 'flex',
93 | [theme.breakpoints.up('md')]: {
94 | display: 'none',
95 | },
96 | },
97 | }));
98 |
99 | const MaterialAppBar = ({ history }) => {
100 | const classes = useStyles();
101 | const [anchorEl, setAnchorEl] = React.useState(null);
102 | const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = React.useState(null);
103 |
104 | const isMenuOpen = Boolean(anchorEl);
105 | const isMobileMenuOpen = Boolean(mobileMoreAnchorEl);
106 |
107 | const handleProfileMenuOpen = (event) => {
108 | setAnchorEl(event.currentTarget);
109 | };
110 |
111 | const handleMobileMenuClose = () => {
112 | setMobileMoreAnchorEl(null);
113 | };
114 |
115 | const handleMenuClose = () => {
116 | setAnchorEl(null);
117 | handleMobileMenuClose();
118 | };
119 |
120 | const handleMobileMenuOpen = (event) => {
121 | setMobileMoreAnchorEl(event.currentTarget);
122 | };
123 |
124 | const menuId = 'primary-search-account-menu';
125 | const renderMenu = (
126 |
138 | );
139 |
140 | const mobileMenuId = 'primary-search-account-menu-mobile';
141 | const renderMobileMenu = (
142 |
250 | );
251 |
252 | return (
253 |
254 |
255 |
256 |
257 |
263 |
264 |
265 |
266 |
267 |
268 | BRAND
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 | Home
278 |
279 |
280 |
281 |
282 |
283 |
284 | Shop
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 | Cart
294 |
295 |
296 |
297 | {isAuthenticated() && isAuthenticated().user.role === 0 && (
298 |
302 |
303 |
304 | Dashboard
305 |
306 |
307 | )}
308 |
309 | {isAuthenticated() && isAuthenticated().user.role === 1 && (
310 |
314 |
315 |
316 | Dashboard
317 |
318 |
319 | )}
320 |
321 | {!isAuthenticated() && (
322 |
323 |
324 |
325 |
326 | Signin
327 |
328 |
329 |
330 |
331 |
332 |
333 | Signup
334 |
335 |
336 |
337 | )}
338 |
339 | {isAuthenticated() && (
340 |
343 | signout(() => {
344 | history.push('/');
345 | })
346 | }
347 | >
348 |
349 |
350 | Signout
351 |
352 |
353 | )}
354 |
355 |
356 |
363 |
364 |
365 |
366 |
367 |
368 | {renderMobileMenu}
369 | {renderMenu}
370 |
371 | );
372 | };
373 |
374 | export default withRouter(MaterialAppBar);
375 |
--------------------------------------------------------------------------------
/client/src/core/NotFound.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import Layout from './Layout';
3 | import Copyright from './Copyright';
4 | import WarningIcon from '@material-ui/icons/Warning';
5 | import Typography from '@material-ui/core/Typography';
6 |
7 | const NotFound = () => {
8 | return (
9 |
14 |
15 | Sorry, this page does not
16 | exist!
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default NotFound;
24 |
--------------------------------------------------------------------------------
/client/src/core/Product.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Layout from './Layout';
3 | import { read, listRelated } from './apiCore';
4 | import Card from './Card';
5 |
6 | const Product = (props) => {
7 | const [product, setProduct] = useState({});
8 | const [relatedProduct, setRelatedProduct] = useState([]);
9 | const [error, setError] = useState(false);
10 |
11 | const loadSingleProduct = (productId) => {
12 | read(productId).then((data) => {
13 | if (data.error) {
14 | setError(data.error);
15 | } else {
16 | setProduct(data);
17 | // fetch related products
18 | listRelated(data._id).then((data) => {
19 | if (data.error) {
20 | setError(data.error);
21 | } else {
22 | setRelatedProduct(data);
23 | }
24 | });
25 | }
26 | });
27 | };
28 |
29 | useEffect(() => {
30 | const productId = props.match.params.productId;
31 | loadSingleProduct(productId);
32 | }, [props]);
33 |
34 | return (
35 |
42 |
43 |
44 |
45 |
Product Details
46 | {product && product.description && (
47 |
48 | )}
49 |
50 |
51 |
52 |
Related products
53 | {relatedProduct.map((p, i) => (
54 |
55 |
56 |
57 | ))}
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default Product;
66 |
--------------------------------------------------------------------------------
/client/src/core/RadioBox.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Radio from '@material-ui/core/Radio';
3 |
4 | const RadioBox = ({ prices, handleFilters }) => {
5 | const [value, setValue] = useState(0);
6 |
7 | const handleChange = (event) => {
8 | handleFilters(event.target.value);
9 | setValue(event.target.value);
10 | };
11 |
12 | return prices.map((p, i) => (
13 |
14 |
21 |
22 |
23 | ));
24 | };
25 |
26 | export default RadioBox;
27 |
--------------------------------------------------------------------------------
/client/src/core/Search.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import { makeStyles } from '@material-ui/core/styles';
4 | import InputLabel from '@material-ui/core/InputLabel';
5 | import MenuItem from '@material-ui/core/MenuItem';
6 | import FormHelperText from '@material-ui/core/FormHelperText';
7 | import FormControl from '@material-ui/core/FormControl';
8 | import Select from '@material-ui/core/Select';
9 | import TextField from '@material-ui/core/TextField';
10 | import Button from '@material-ui/core/Button';
11 | import SearchIcon from '@material-ui/icons/Search';
12 |
13 | import { getCategories, list } from './apiCore';
14 | import Card from './Card';
15 |
16 | const useStyles = makeStyles((theme) => ({
17 | formControl: {
18 | margin: theme.spacing(1),
19 | minWidth: 120,
20 | },
21 | selectEmpty: {
22 | marginTop: theme.spacing(2),
23 | },
24 | tField: {
25 | width: 800,
26 | marginTop: 2,
27 | },
28 | root: {
29 | '& > *': {
30 | margin: theme.spacing(2),
31 | },
32 | },
33 | }));
34 |
35 | const Search = () => {
36 | const [data, setData] = useState({
37 | categories: [],
38 | category: '',
39 | search: '',
40 | results: [],
41 | searched: false,
42 | });
43 |
44 | const { categories, category, search, results, searched } = data;
45 |
46 | const loadCategories = () => {
47 | getCategories().then((data) => {
48 | if (data.error) {
49 | console.log(data.error);
50 | } else {
51 | setData({ ...data, categories: data });
52 | }
53 | });
54 | };
55 |
56 | useEffect(() => {
57 | loadCategories();
58 | }, []);
59 |
60 | const searchData = () => {
61 | // console.log(search, category);
62 | if (search) {
63 | list({ search: search || undefined, category: category }).then(
64 | (response) => {
65 | if (response.error) {
66 | console.log(response.error);
67 | } else {
68 | setData({ ...data, results: response, searched: true });
69 | }
70 | }
71 | );
72 | }
73 | };
74 |
75 | const searchSubmit = (e) => {
76 | e.preventDefault();
77 | searchData();
78 | };
79 |
80 | const handleChange = (name) => (event) => {
81 | setData({ ...data, [name]: event.target.value, searched: false });
82 | };
83 |
84 | const searchMessage = (searched, results) => {
85 | if (searched && results.length > 0) {
86 | return `Found ${results.length} products`;
87 | }
88 | if (searched && results.length < 1) {
89 | return `Search: No products found`;
90 | }
91 | };
92 |
93 | const searchedProducts = (results = []) => {
94 | return (
95 |
96 |
97 |
98 |
{searchMessage(searched, results)}
99 |
100 | {results.map((product, i) => (
101 |
102 |
103 |
104 | ))}
105 |
106 |
107 |
108 |
109 | );
110 | };
111 |
112 | const classes = useStyles();
113 |
114 | const searchForm = () => (
115 |
160 | );
161 |
162 | return (
163 |
164 |
{searchForm()}
165 |
{searchedProducts(results)}
166 |
167 | );
168 | };
169 |
170 | export default Search;
171 |
--------------------------------------------------------------------------------
/client/src/core/Shop.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Layout from './Layout';
3 | import Button from '@material-ui/core/Button';
4 | import Card from './Card';
5 | import { getCategories, getFilteredProducts } from './apiCore';
6 | import Checkbox from './Checkbox';
7 | import RadioBox from './RadioBox';
8 | import { makeStyles } from '@material-ui/core/styles';
9 |
10 | import Search from './Search';
11 | import { prices } from './fixedPrices';
12 | import Copyright from './Copyright';
13 |
14 | const Shop = () => {
15 | const [myFilters, setMyFilters] = useState({
16 | filters: { category: [], price: [] },
17 | });
18 |
19 | const [categories, setCategories] = useState([]);
20 | const [error, setError] = useState(false);
21 | const [limit, setLimit] = useState(6);
22 | const [skip, setSkip] = useState(0);
23 | const [size, setSize] = useState(0);
24 | const [filteredResults, setFilteredResults] = useState([]);
25 |
26 | const init = () => {
27 | getCategories().then((data) => {
28 | if (data.error) {
29 | setError(data.error);
30 | } else {
31 | setCategories(data);
32 | }
33 | });
34 | };
35 |
36 | const loadFilteredResults = (newFilters) => {
37 | // console.log(newFilters);
38 | getFilteredProducts(skip, limit, newFilters).then((data) => {
39 | if (data.error) {
40 | setError(data.error);
41 | } else {
42 | setFilteredResults(data.data);
43 | setSize(data.size);
44 | setSkip(0);
45 | }
46 | });
47 | };
48 |
49 | const loadMore = () => {
50 | let toSkip = skip + limit;
51 | // console.log(newFilters);
52 | getFilteredProducts(toSkip, limit, myFilters.filters).then((data) => {
53 | if (data.error) {
54 | setError(data.error);
55 | } else {
56 | setFilteredResults([...filteredResults, ...data.data]);
57 | setSize(data.size);
58 | setSkip(toSkip);
59 | }
60 | });
61 | };
62 |
63 | const useStyles = makeStyles((theme) => ({
64 | btn: {
65 | background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
66 | borderRadius: 3,
67 | border: 0,
68 | color: 'white',
69 | height: 48,
70 | padding: '0 20px',
71 | boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
72 | },
73 | }));
74 |
75 | const classes = useStyles();
76 |
77 | const loadMoreButton = () => {
78 | return (
79 | size > 0 &&
80 | size >= limit && (
81 | //
84 |
87 | )
88 | );
89 | };
90 |
91 | useEffect(() => {
92 | init();
93 | loadFilteredResults(skip, limit, myFilters.filters);
94 | }, []);
95 |
96 | const handleFilters = (filters, filterBy) => {
97 | // console.log("SHOP", filters, filterBy);
98 | const newFilters = { ...myFilters };
99 | newFilters.filters[filterBy] = filters;
100 |
101 | if (filterBy === 'price') {
102 | let priceValues = handlePrice(filters);
103 | newFilters.filters[filterBy] = priceValues;
104 | }
105 | loadFilteredResults(myFilters.filters);
106 | setMyFilters(newFilters);
107 | };
108 |
109 | const handlePrice = (value) => {
110 | const data = prices;
111 | let array = [];
112 |
113 | for (let key in data) {
114 | if (data[key]._id === parseInt(value)) {
115 | array = data[key].array;
116 | }
117 | }
118 | return array;
119 | };
120 |
121 | return (
122 |
127 |
128 |
129 |
130 |
Filter by categories
131 |
132 | handleFilters(filters, 'category')}
135 | />
136 |
137 |
138 |
Filter by price range
139 |
140 | handleFilters(filters, 'price')}
143 | />
144 |
145 |
146 |
147 |
148 |
Products
149 |
150 | {filteredResults.map((product, i) => (
151 |
152 |
153 |
154 | ))}
155 |
156 |
157 | {loadMoreButton()}
158 |
159 |
160 |
161 |
162 | );
163 | };
164 |
165 | export default Shop;
166 |
--------------------------------------------------------------------------------
/client/src/core/ShowImage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { API } from '../config';
3 |
4 | const ShowImage = ({ item, url }) => (
5 |
6 |

12 |
13 | );
14 |
15 | export default ShowImage;
16 |
--------------------------------------------------------------------------------
/client/src/core/apiCore.js:
--------------------------------------------------------------------------------
1 | import { API } from '../config';
2 | import queryString from 'query-string';
3 |
4 | export const getProducts = (sortBy) => {
5 | return fetch(`${API}/products?sortBy=${sortBy}&order=desc&limit=6`, {
6 | method: 'GET',
7 | })
8 | .then((response) => {
9 | return response.json();
10 | })
11 | .catch((err) => console.log(err));
12 | };
13 |
14 | export const getCategories = () => {
15 | return fetch(`${API}/categories`, {
16 | method: 'GET',
17 | })
18 | .then((response) => {
19 | return response.json();
20 | })
21 | .catch((err) => console.log(err));
22 | };
23 |
24 | export const getFilteredProducts = (skip, limit, filters = {}) => {
25 | const data = {
26 | limit,
27 | skip,
28 | filters,
29 | };
30 | return fetch(`${API}/products/by/search`, {
31 | method: 'POST',
32 | headers: {
33 | Accept: 'application/json',
34 | 'Content-Type': 'application/json',
35 | },
36 | body: JSON.stringify(data),
37 | })
38 | .then((response) => {
39 | return response.json();
40 | })
41 | .catch((err) => {
42 | console.log(err);
43 | });
44 | };
45 |
46 | export const list = (params) => {
47 | const query = queryString.stringify(params);
48 | console.log('query', query);
49 | return fetch(`${API}/products/search?${query}`, {
50 | method: 'GET',
51 | })
52 | .then((response) => {
53 | return response.json();
54 | })
55 | .catch((err) => console.log(err));
56 | };
57 |
58 | export const read = (productId) => {
59 | return fetch(`${API}/product/${productId}`, {
60 | method: 'GET',
61 | })
62 | .then((response) => {
63 | return response.json();
64 | })
65 | .catch((err) => console.log(err));
66 | };
67 |
68 | export const listRelated = (productId) => {
69 | return fetch(`${API}/products/related/${productId}`, {
70 | method: 'GET',
71 | })
72 | .then((response) => {
73 | return response.json();
74 | })
75 | .catch((err) => console.log(err));
76 | };
77 |
78 | export const getBraintreeClientToken = (userId, token) => {
79 | return fetch(`${API}/braintree/getToken/${userId}`, {
80 | method: 'GET',
81 | headers: {
82 | Accept: 'application/json',
83 | 'Content-Type': 'application/json',
84 | Authorization: `Bearer ${token}`,
85 | },
86 | })
87 | .then((response) => {
88 | return response.json();
89 | })
90 | .catch((err) => console.log(err));
91 | };
92 |
93 | export const processPayment = (userId, token, paymentData) => {
94 | return fetch(`${API}/braintree/payment/${userId}`, {
95 | method: 'POST',
96 | headers: {
97 | Accept: 'application/json',
98 | 'Content-Type': 'application/json',
99 | Authorization: `Bearer ${token}`,
100 | },
101 | body: JSON.stringify(paymentData),
102 | })
103 | .then((response) => {
104 | return response.json();
105 | })
106 | .catch((err) => console.log(err));
107 | };
108 |
109 | export const createOrder = (userId, token, createOrderData) => {
110 | return fetch(`${API}/order/create/${userId}`, {
111 | method: 'POST',
112 | headers: {
113 | Accept: 'application/json',
114 | 'Content-Type': 'application/json',
115 | Authorization: `Bearer ${token}`,
116 | },
117 | body: JSON.stringify({ order: createOrderData }),
118 | })
119 | .then((response) => {
120 | return response.json();
121 | })
122 | .catch((err) => console.log(err));
123 | };
124 |
--------------------------------------------------------------------------------
/client/src/core/cartHelpers.js:
--------------------------------------------------------------------------------
1 | export const addItem = (item = [], count = 0, next = (f) => f) => {
2 | let cart = [];
3 | if (typeof window !== 'undefined') {
4 | if (localStorage.getItem('cart')) {
5 | cart = JSON.parse(localStorage.getItem('cart'));
6 | }
7 | cart.push({
8 | ...item,
9 | count: 1,
10 | });
11 |
12 | // remove duplicates
13 | // build an Array from new Set and turn it back into array using Array.from
14 | // so that later we can re-map it
15 | // new set will only allow unique values in it
16 | // so pass the ids of each object/product
17 | // If the loop tries to add the same value again, it'll get ignored
18 | // ...with the array of ids we got on when first map() was used
19 | // run map() on it again and return the actual product from the cart
20 |
21 | cart = Array.from(new Set(cart.map((p) => p._id))).map((id) => {
22 | return cart.find((p) => p._id === id);
23 | });
24 |
25 | localStorage.setItem('cart', JSON.stringify(cart));
26 | next();
27 | }
28 | };
29 |
30 | export const itemTotal = () => {
31 | if (typeof window !== 'undefined') {
32 | if (localStorage.getItem('cart')) {
33 | return JSON.parse(localStorage.getItem('cart')).length;
34 | }
35 | }
36 | return 0;
37 | };
38 |
39 | export const getCart = () => {
40 | if (typeof window !== 'undefined') {
41 | if (localStorage.getItem('cart')) {
42 | return JSON.parse(localStorage.getItem('cart'));
43 | }
44 | }
45 | return [];
46 | };
47 |
48 | export const updateItem = (productId, count) => {
49 | let cart = [];
50 | if (typeof window !== 'undefined') {
51 | if (localStorage.getItem('cart')) {
52 | cart = JSON.parse(localStorage.getItem('cart'));
53 | }
54 |
55 | cart.map((product, i) => {
56 | if (product._id === productId) {
57 | cart[i].count = count;
58 | }
59 | });
60 |
61 | localStorage.setItem('cart', JSON.stringify(cart));
62 | }
63 | };
64 |
65 | export const removeItem = (productId) => {
66 | let cart = [];
67 | if (typeof window !== 'undefined') {
68 | if (localStorage.getItem('cart')) {
69 | cart = JSON.parse(localStorage.getItem('cart'));
70 | }
71 |
72 | cart.map((product, i) => {
73 | if (product._id === productId) {
74 | cart.splice(i, 1);
75 | }
76 | });
77 |
78 | localStorage.setItem('cart', JSON.stringify(cart));
79 | }
80 | return cart;
81 | };
82 |
83 | export const emptyCart = (next) => {
84 | if (typeof window !== 'undefined') {
85 | localStorage.removeItem('cart');
86 | next();
87 | }
88 | };
89 |
--------------------------------------------------------------------------------
/client/src/core/fixedPrices.js:
--------------------------------------------------------------------------------
1 | export const prices = [
2 | {
3 | _id: 0,
4 | name: 'Any',
5 | array: [],
6 | },
7 | {
8 | _id: 1,
9 | name: '$0 to $9',
10 | array: [0, 9],
11 | },
12 | {
13 | _id: 2,
14 | name: '$10 to $19',
15 | array: [10, 19],
16 | },
17 | {
18 | _id: 3,
19 | name: '$20 to $29',
20 | array: [20, 29],
21 | },
22 | {
23 | _id: 4,
24 | name: '$30 to $39',
25 | array: [30, 39],
26 | },
27 | {
28 | _id: 5,
29 | name: 'More than 40',
30 | array: [40, 99],
31 | },
32 | ];
33 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Routes from './Routes';
4 |
5 | ReactDOM.render(, document.getElementById('root'));
6 |
--------------------------------------------------------------------------------
/client/src/styles.css:
--------------------------------------------------------------------------------
1 | /**
2 | * border radius
3 | */
4 |
5 | .btn,
6 | .jumbotron,
7 | .nav :hover {
8 | border-radius: 0px;
9 | }
10 |
11 | /* cart badge */
12 |
13 | .cart-badge,
14 | .cart-badge:hover {
15 | border-radius: 50%;
16 | padding: 2px;
17 | font-size: 12px;
18 | font-style: italic;
19 | background: #000;
20 | }
21 | /**
22 | * single product page - product name
23 | */
24 |
25 | .name {
26 | background: indigo;
27 | color: #fff;
28 | font-weight: bold;
29 | }
30 |
31 | /* black shade form 10-1 */
32 | .black-10 {
33 | background: #f2f2f2;
34 | }
35 | .black-9 {
36 | background: #e6e6e6;
37 | }
38 | .black-8 {
39 | background: #d9d9d9;
40 | }
41 | .black-7 {
42 | background: #cccccc;
43 | }
44 | .black-6 {
45 | background: #bfbfbf;
46 | }
47 | .black-5 {
48 | background: #b3b3b3;
49 | }
50 |
51 | /**
52 | * product image on card
53 | */
54 |
55 | .product-img {
56 | min-height: 100px;
57 | }
58 |
59 | .jumbotron h2 {
60 | margin-top: -20px;
61 | }
62 |
63 | @media only screen and (max-width: 600px) {
64 | .jumbotron h2 {
65 | margin-top: 10px;
66 | }
67 | }
68 |
69 | /**
70 | * jumbotron animation
71 | */
72 |
73 | .jumbotron {
74 | height: 180px;
75 | color: #fff;
76 | background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
77 | background-size: 400% 400%;
78 | -webkit-animation: Gradient 15s ease infinite;
79 | -moz-animation: Gradient 15s ease infinite;
80 | animation: Gradient 15s ease infinite;
81 | }
82 |
83 | @-webkit-keyframes Gradient {
84 | 0% {
85 | background-position: 0% 50%;
86 | }
87 | 50% {
88 | background-position: 100% 50%;
89 | }
90 | 100% {
91 | background-position: 0% 50%;
92 | }
93 | }
94 |
95 | @-moz-keyframes Gradient {
96 | 0% {
97 | background-position: 0% 50%;
98 | }
99 | 50% {
100 | background-position: 100% 50%;
101 | }
102 | 100% {
103 | background-position: 0% 50%;
104 | }
105 | }
106 |
107 | @keyframes Gradient {
108 | 0% {
109 | background-position: 0% 50%;
110 | }
111 | 50% {
112 | background-position: 100% 50%;
113 | }
114 | 100% {
115 | background-position: 0% 50%;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/client/src/user/AdminDashboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Layout from '../core/Layout';
3 | import { isAuthenticated } from '../auth';
4 | import { Link } from 'react-router-dom';
5 |
6 | const AdminDashboard = () => {
7 | const {
8 | user: { _id, name, email, role },
9 | } = isAuthenticated();
10 |
11 | const adminLinks = () => {
12 | return (
13 |
14 |
Admin Links
15 |
16 | -
17 |
18 | Category List
19 |
20 |
21 | -
22 |
23 | Add Category
24 |
25 |
26 | -
27 |
28 | Add Product
29 |
30 |
31 | -
32 |
33 | View Orders
34 |
35 |
36 | -
37 |
38 | Manage Products
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | const adminInfo = () => {
47 | return (
48 |
49 |
User information
50 |
51 | - {name}
52 | - {email}
53 | -
54 | {role === 1 ? 'Admin' : 'Registered user'}
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | return (
62 |
67 |
68 |
{adminLinks()}
69 |
{adminInfo()}
70 |
71 |
72 | );
73 | };
74 |
75 | export default AdminDashboard;
76 |
--------------------------------------------------------------------------------
/client/src/user/Profile.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Layout from '../core/Layout';
3 | import { isAuthenticated } from '../auth';
4 | import { Redirect } from 'react-router-dom';
5 | import { read, update, updateUser } from './apiUser';
6 |
7 | const Profile = ({ match }) => {
8 | const [values, setValues] = useState({
9 | name: '',
10 | email: '',
11 | password: '',
12 | error: false,
13 | success: false,
14 | });
15 |
16 | const { token } = isAuthenticated();
17 | const { name, email, password, success } = values;
18 |
19 | const init = (userId) => {
20 | // console.log(userId);
21 | read(userId, token).then((data) => {
22 | if (data.error) {
23 | setValues({ ...values, error: true });
24 | } else {
25 | setValues({ ...values, name: data.name, email: data.email });
26 | }
27 | });
28 | };
29 |
30 | useEffect(() => {
31 | init(match.params.userId);
32 | }, []);
33 |
34 | const handleChange = (name) => (e) => {
35 | setValues({ ...values, error: false, [name]: e.target.value });
36 | };
37 |
38 | const clickSubmit = (e) => {
39 | e.preventDefault();
40 | update(match.params.userId, token, { name, email, password }).then(
41 | (data) => {
42 | if (data.error) {
43 | // console.log(data.error);
44 | alert(data.error);
45 | } else {
46 | updateUser(data, () => {
47 | setValues({
48 | ...values,
49 | name: data.name,
50 | email: data.email,
51 | success: true,
52 | });
53 | });
54 | }
55 | }
56 | );
57 | };
58 |
59 | const redirectUser = (success) => {
60 | if (success) {
61 | return ;
62 | }
63 | };
64 |
65 | const profileUpdate = (name, email, password) => (
66 |
99 | );
100 |
101 | return (
102 |
107 | Profile update
108 | {profileUpdate(name, email, password)}
109 | {redirectUser(success)}
110 |
111 | );
112 | };
113 |
114 | export default Profile;
115 |
--------------------------------------------------------------------------------
/client/src/user/Signin.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Redirect, Link } from 'react-router-dom';
3 | import Layout from '../core/Layout';
4 | import Avatar from '@material-ui/core/Avatar';
5 | import Button from '@material-ui/core/Button';
6 | import CssBaseline from '@material-ui/core/CssBaseline';
7 | import TextField from '@material-ui/core/TextField';
8 | import FormControlLabel from '@material-ui/core/FormControlLabel';
9 | import Checkbox from '@material-ui/core/Checkbox';
10 | import Grid from '@material-ui/core/Grid';
11 | import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
12 | import Typography from '@material-ui/core/Typography';
13 | import { makeStyles } from '@material-ui/core/styles';
14 | import Container from '@material-ui/core/Container';
15 |
16 | import Copyright from '../core/Copyright';
17 | import { signin, authenticate, isAuthenticated } from '../auth';
18 |
19 | const useStyles = makeStyles((theme) => ({
20 | paper: {
21 | marginTop: theme.spacing(8),
22 | display: 'flex',
23 | flexDirection: 'column',
24 | alignItems: 'center',
25 | },
26 | avatar: {
27 | margin: theme.spacing(1),
28 | backgroundColor: theme.palette.secondary.main,
29 | },
30 | form: {
31 | width: '100%', // Fix IE 11 issue.
32 | marginTop: theme.spacing(1),
33 | },
34 | submit: {
35 | margin: theme.spacing(3, 0, 2),
36 | },
37 | }));
38 |
39 | export default function Signin() {
40 | const [values, setValues] = useState({
41 | email: '',
42 | password: '',
43 | error: '',
44 | loading: false,
45 | redirectToReferrer: false,
46 | });
47 |
48 | const { email, password, loading, error, redirectToReferrer } = values;
49 | const { user } = isAuthenticated();
50 |
51 | const handleChange = (name) => (event) => {
52 | setValues({ ...values, error: false, [name]: event.target.value });
53 | };
54 |
55 | const clickSubmit = (event) => {
56 | event.preventDefault(); // so that browser does not reload
57 | setValues({ ...values, error: false, loading: true });
58 | signin({ email, password }).then((data) => {
59 | if (data.error) {
60 | setValues({ ...values, error: data.error, loading: false });
61 | } else {
62 | authenticate(data, () => {
63 | setValues({
64 | ...values,
65 | redirectToReferrer: true,
66 | });
67 | });
68 | }
69 | });
70 | };
71 |
72 | const showError = () => (
73 |
77 | {error}
78 |
79 | );
80 |
81 | const showLoading = () =>
82 | loading && (
83 |
84 |
Loading...
85 |
86 | );
87 |
88 | const redirectUser = () => {
89 | if (redirectToReferrer) {
90 | if (user && user.role === 1) {
91 | return ;
92 | } else {
93 | return ;
94 | }
95 | }
96 | if (isAuthenticated()) {
97 | return ;
98 | }
99 | };
100 |
101 | const classes = useStyles();
102 |
103 | const signInForm = () => (
104 |
105 | {showError()}
106 | {showLoading()}
107 | {redirectUser()}
108 |
109 |
110 |
111 |
112 |
113 |
114 | Sign in
115 |
116 |
171 |
172 |
173 | );
174 |
175 | return (
176 |
181 | {signInForm()}
182 |
183 |
184 | );
185 | }
186 |
--------------------------------------------------------------------------------
/client/src/user/Signup.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import Avatar from '@material-ui/core/Avatar';
4 | import Button from '@material-ui/core/Button';
5 | import CssBaseline from '@material-ui/core/CssBaseline';
6 | import TextField from '@material-ui/core/TextField';
7 | // import Link from '@material-ui/core/Link';
8 | import Grid from '@material-ui/core/Grid';
9 | import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
10 | import Typography from '@material-ui/core/Typography';
11 | import { makeStyles } from '@material-ui/core/styles';
12 | import Container from '@material-ui/core/Container';
13 |
14 | import Copyright from '../core/Copyright';
15 |
16 | import Layout from '../core/Layout';
17 | import { signup } from '../auth';
18 |
19 | const useStyles = makeStyles((theme) => ({
20 | paper: {
21 | marginTop: theme.spacing(8),
22 | display: 'flex',
23 | flexDirection: 'column',
24 | alignItems: 'center',
25 | },
26 | avatar: {
27 | margin: theme.spacing(1),
28 | backgroundColor: theme.palette.secondary.main,
29 | },
30 | form: {
31 | width: '100%', // Fix IE 11 issue.
32 | marginTop: theme.spacing(1),
33 | },
34 | submit: {
35 | margin: theme.spacing(3, 0, 2),
36 | },
37 | }));
38 |
39 | export default function Signup() {
40 | const [values, setValues] = useState({
41 | name: '',
42 | email: '',
43 | password: '',
44 | error: '',
45 | success: false,
46 | });
47 |
48 | const { name, email, password, success, error } = values;
49 |
50 | const handleChange = (name) => (event) => {
51 | setValues({ ...values, error: false, [name]: event.target.value });
52 | };
53 |
54 | const clickSubmit = (event) => {
55 | event.preventDefault(); // so that browser does not reload
56 | setValues({ ...values, error: false });
57 | signup({ name, email, password }).then((data) => {
58 | if (data.error) {
59 | setValues({ ...values, error: data.error, success: false });
60 | } else {
61 | setValues({
62 | ...values,
63 | name: '',
64 | email: '',
65 | password: '',
66 | error: '',
67 | success: true,
68 | });
69 | }
70 | }); // sending js object
71 | };
72 |
73 | const showError = () => (
74 |
78 | {error}
79 |
80 | );
81 |
82 | const showSuccess = () => (
83 |
87 | New account is created. Please Signin.
88 |
89 | );
90 |
91 | const classes = useStyles();
92 |
93 | const signUpForm = () => (
94 |
95 | {showSuccess()}
96 | {showError()}
97 |
98 |
99 |
100 |
101 |
102 |
103 | Sign up
104 |
105 |
169 |
170 |
171 | );
172 |
173 | return (
174 |
179 | {signUpForm()}
180 |
181 |
182 | );
183 | }
184 |
--------------------------------------------------------------------------------
/client/src/user/UserDashboard.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Layout from '../core/Layout';
3 | import { isAuthenticated } from '../auth';
4 | import { Link } from 'react-router-dom';
5 | import { getPurchaseHistory } from './apiUser';
6 | import moment from 'moment';
7 |
8 | const Dashboard = () => {
9 | const [history, setHistory] = useState([]);
10 |
11 | const {
12 | user: { _id, name, email, role },
13 | } = isAuthenticated();
14 |
15 | const token = isAuthenticated().token;
16 |
17 | const init = (userId, token) => {
18 | getPurchaseHistory(userId, token).then((data) => {
19 | if (data.error) {
20 | console.log(data.error);
21 | } else {
22 | setHistory(data);
23 | }
24 | });
25 | };
26 |
27 | useEffect(() => {
28 | init(_id, token);
29 | }, []);
30 |
31 | const userLinks = () => {
32 | return (
33 |
34 |
User links
35 |
36 | -
37 |
38 | My cart
39 |
40 |
41 | -
42 |
43 | Update profile
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | const userInfo = () => {
52 | return (
53 |
54 |
User information
55 |
56 | - {name}
57 | - {email}
58 | -
59 | {role === 1 ? 'Admin' : 'Registered user'}
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | const purchaseHistory = (history) => {
67 | return (
68 |
69 |
Purchase history
70 |
71 | -
72 | {history.map((h, i) => {
73 | return (
74 |
75 |
76 | {h.products.map((p, i) => {
77 | return (
78 |
79 |
Product name: {p.name}
80 | Product price: ${p.price}
81 | Purchased date: {moment(p.createdAt).fromNow()}
82 |
83 | );
84 | })}
85 |
86 | );
87 | })}
88 |
89 |
90 |
91 | );
92 | };
93 |
94 | return (
95 |
100 |
101 |
{userLinks()}
102 |
103 | {userInfo()}
104 | {purchaseHistory(history)}
105 |
106 |
107 |
108 | );
109 | };
110 |
111 | export default Dashboard;
112 |
--------------------------------------------------------------------------------
/client/src/user/apiUser.js:
--------------------------------------------------------------------------------
1 | import { API } from '../config';
2 |
3 | export const read = (userId, token) => {
4 | return fetch(`${API}/user/${userId}`, {
5 | method: 'GET',
6 | headers: {
7 | Accept: 'application/json',
8 | 'Content-Type': 'application/json',
9 | Authorization: `Bearer ${token}`,
10 | },
11 | })
12 | .then((response) => {
13 | return response.json();
14 | })
15 | .catch((err) => console.log(err));
16 | };
17 |
18 | export const update = (userId, token, user) => {
19 | return fetch(`${API}/user/${userId}`, {
20 | method: 'PUT',
21 | headers: {
22 | Accept: 'application/json',
23 | 'Content-Type': 'application/json',
24 | Authorization: `Bearer ${token}`,
25 | },
26 | body: JSON.stringify(user),
27 | })
28 | .then((response) => {
29 | return response.json();
30 | })
31 | .catch((err) => console.log(err));
32 | };
33 |
34 | export const updateUser = (user, next) => {
35 | if (typeof window !== 'undefined') {
36 | if (localStorage.getItem('jwt')) {
37 | let auth = JSON.parse(localStorage.getItem('jwt'));
38 | auth.user = user;
39 | localStorage.setItem('jwt', JSON.stringify(auth));
40 | next();
41 | }
42 | }
43 | };
44 |
45 | export const getPurchaseHistory = (userId, token) => {
46 | return fetch(`${API}/orders/by/user/${userId}`, {
47 | method: 'GET',
48 | headers: {
49 | Accept: 'application/json',
50 | 'Content-Type': 'application/json',
51 | Authorization: `Bearer ${token}`,
52 | },
53 | })
54 | .then((response) => {
55 | return response.json();
56 | })
57 | .catch((err) => console.log(err));
58 | };
59 |
60 |
--------------------------------------------------------------------------------
/controllers/auth.js:
--------------------------------------------------------------------------------
1 | const User = require('../models/user');
2 | const jwt = require('jsonwebtoken'); // to generate signed token
3 | const expressJwt = require('express-jwt'); // for auth check
4 | const { errorHandler } = require('../helpers/dbErrorHandler');
5 |
6 | require('dotenv').config();
7 |
8 | exports.signup = async (req, res) => {
9 | const user = new User(req.body);
10 | try {
11 | const data = await user.save();
12 | if (!data) {
13 | return res.status(400).json({
14 | error: errorHandler(err),
15 | });
16 | }
17 |
18 | user.salt = undefined;
19 | user.hashed_password = undefined;
20 | res.json({
21 | user,
22 | });
23 | } catch (error) {
24 | return res.status(400).json({
25 | error: errorHandler(err),
26 | });
27 | }
28 | };
29 |
30 | exports.signin = async (req, res) => {
31 | const { email, password } = req.body;
32 |
33 | try {
34 | // Find the user based on email
35 | const user = await User.findOne({ email });
36 |
37 | if (!user) {
38 | return res.status(400).json({
39 | error: "User with that email doesn't exist. Please signup.",
40 | });
41 | }
42 |
43 | // If user found, check if the password matches
44 | if (!user.authenticate(password)) {
45 | return res.status(401).json({
46 | error: "Email and password didn't match",
47 | });
48 | }
49 |
50 | // Generate a signed token with user ID and secret
51 | const token = jwt.sign({ _id: user._id }, process.env.JWT_SECRET);
52 |
53 | // Persist the token as 't' in cookie with expiry date
54 | res.cookie('t', token, { expire: new Date() + 9999 });
55 |
56 | // Return the token and user details to the frontend client
57 | const { _id, name, email: userEmail, role } = user;
58 | return res.json({ token, user: { _id, email: userEmail, name, role } });
59 | } catch (err) {
60 | return res.status(400).json({
61 | error: 'Signin failed. Please try again later.',
62 | });
63 | }
64 | };
65 |
66 | exports.signout = (req, res) => {
67 | res.clearCookie('t');
68 | res.json({ message: 'Signout success' });
69 | };
70 |
71 | exports.requireSignin = expressJwt({
72 | secret: process.env.JWT_SECRET,
73 | // algorithms: ['RS256'],
74 | userProperty: 'auth',
75 | });
76 |
77 | exports.isAuth = (req, res, next) => {
78 | let user = req.profile && req.auth && req.profile._id == req.auth._id;
79 | if (!user) {
80 | return res.status(403).json({
81 | error: 'Access denied',
82 | });
83 | }
84 | next();
85 | };
86 |
87 | exports.isAdmin = (req, res, next) => {
88 | if (req.profile.role === 0) {
89 | return res.status(403).json({
90 | error: 'Admin resource! Access denied',
91 | });
92 | }
93 | next();
94 | };
95 |
--------------------------------------------------------------------------------
/controllers/braintree.js:
--------------------------------------------------------------------------------
1 | const braintree = require('braintree');
2 | require('dotenv').config();
3 |
4 | const gateway = braintree.connect({
5 | environment: braintree.Environment.Sandbox, // Production
6 | merchantId: process.env.BRAINTREE_MERCHANT_ID,
7 | publicKey: process.env.BRAINTREE_PUBLIC_KEY,
8 | privateKey: process.env.BRAINTREE_PRIVATE_KEY,
9 | });
10 |
11 | exports.generateToken = (req, res) => {
12 | gateway.clientToken.generate({}, function (err, response) {
13 | if (err) {
14 | res.status(500).send(err);
15 | } else {
16 | res.send(response);
17 | }
18 | });
19 | };
20 |
21 | exports.processPayment = (req, res) => {
22 | let nonceFromTheClient = req.body.paymentMethodNonce;
23 | let amountFromTheClient = req.body.amount;
24 | // charge
25 | let newTransaction = gateway.transaction.sale(
26 | {
27 | amount: amountFromTheClient,
28 | paymentMethodNonce: nonceFromTheClient,
29 | options: {
30 | submitForSettlement: true,
31 | },
32 | },
33 | (error, result) => {
34 | if (error) {
35 | res.status(500).json(error);
36 | } else {
37 | res.json(result);
38 | }
39 | }
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/controllers/category.js:
--------------------------------------------------------------------------------
1 | const Category = require('../models/category');
2 | const { errorHandler } = require('../helpers/dbErrorHandler');
3 |
4 | exports.categoryById = async (req, res, next, id) => {
5 | try {
6 | const category = await Category.findById(id).exec();
7 | if (!category) {
8 | return res.status(400).json({
9 | error: "Category doesn't exist",
10 | });
11 | }
12 | req.category = category;
13 | next();
14 | } catch (err) {
15 | return res.status(400).json({
16 | error: errorHandler(err),
17 | });
18 | }
19 | };
20 |
21 | exports.create = async (req, res) => {
22 | const category = new Category(req.body);
23 | try {
24 | const data = await category.save();
25 | res.json({ data });
26 | } catch (err) {
27 | return res.status(400).json({
28 | error: errorHandler(err),
29 | });
30 | }
31 | };
32 |
33 | exports.read = (req, res) => {
34 | return res.json(req.category);
35 | };
36 |
37 | exports.update = async (req, res) => {
38 | const category = req.category;
39 | category.name = req.body.name;
40 | try {
41 | const data = await category.save();
42 | res.json(data);
43 | } catch (err) {
44 | return res.status(400).json({
45 | error: errorHandler(err),
46 | });
47 | }
48 | };
49 |
50 | exports.remove = async (req, res) => {
51 | const category = req.category;
52 | try {
53 | await category.remove();
54 | res.json({
55 | message: 'Category deleted',
56 | });
57 | } catch (err) {
58 | return res.status(400).json({
59 | error: errorHandler(err),
60 | });
61 | }
62 | };
63 |
64 | exports.list = async (req, res) => {
65 | try {
66 | const data = await Category.find().exec();
67 | res.json(data);
68 | } catch (err) {
69 | return res.status(400).json({
70 | error: errorHandler(err),
71 | });
72 | }
73 | };
74 |
--------------------------------------------------------------------------------
/controllers/order.js:
--------------------------------------------------------------------------------
1 | const { Order } = require('../models/order');
2 | const { errorHandler } = require('../helpers/dbErrorHandler');
3 |
4 | exports.orderById = async (req, res, next, id) => {
5 | try {
6 | const order = await Order.findById(id)
7 | .populate('products.product', 'name price')
8 | .exec();
9 | if (!order) {
10 | return res.status(400).json({
11 | error: 'Order not found',
12 | });
13 | }
14 | req.order = order;
15 | next();
16 | } catch (err) {
17 | return res.status(400).json({
18 | error: errorHandler(err),
19 | });
20 | }
21 | };
22 |
23 | exports.create = async (req, res) => {
24 | try {
25 | req.body.order.user = req.profile;
26 | const order = new Order(req.body.order);
27 | const data = await order.save();
28 | res.json(data);
29 | } catch (error) {
30 | return res.status(400).json({
31 | error: errorHandler(error),
32 | });
33 | }
34 | };
35 |
36 | exports.listOrders = async (req, res) => {
37 | try {
38 | const orders = await Order.find()
39 | .populate('user', '_id name address')
40 | .sort('-created')
41 | .exec();
42 | res.json(orders);
43 | } catch (error) {
44 | return res.status(400).json({
45 | error: errorHandler(error),
46 | });
47 | }
48 | };
49 |
50 | exports.getStatusValues = (req, res) => {
51 | res.json(Order.schema.path('status').enumValues);
52 | };
53 |
54 | exports.updateOrderStatus = async (req, res) => {
55 | try {
56 | const order = await Order.updateOne(
57 | { _id: req.body.orderId },
58 | { $set: { status: req.body.status } }
59 | );
60 | res.json(order);
61 | } catch (err) {
62 | return res.status(400).json({
63 | error: errorHandler(err),
64 | });
65 | }
66 | };
67 |
--------------------------------------------------------------------------------
/controllers/product.js:
--------------------------------------------------------------------------------
1 | const formidable = require('formidable');
2 | const _ = require('lodash');
3 | const fs = require('fs');
4 | const Product = require('../models/product');
5 | const { errorHandler } = require('../helpers/dbErrorHandler');
6 |
7 | // Get product by ID middleware
8 | exports.productById = async (req, res, next, id) => {
9 | try {
10 | const product = await Product.findById(id).populate('category').exec();
11 | if (!product) {
12 | return res.status(400).json({ error: 'Product not found' });
13 | }
14 | req.product = product;
15 | next();
16 | } catch (err) {
17 | return res.status(400).json({ error: 'Product not found' });
18 | }
19 | };
20 |
21 | // Read product details
22 | exports.read = (req, res) => {
23 | req.product.photo = undefined;
24 | return res.json(req.product);
25 | };
26 |
27 | // Create a new product
28 | exports.create = async (req, res) => {
29 | const form = new formidable.IncomingForm();
30 | form.keepExtensions = true;
31 |
32 | form.parse(req, async (err, fields, files) => {
33 | if (err) {
34 | return res.status(400).json({ error: 'Image could not be uploaded' });
35 | }
36 |
37 | const { name, description, price, category, quantity, shipping } = fields;
38 |
39 | if (
40 | !name ||
41 | !description ||
42 | !price ||
43 | !category ||
44 | !quantity ||
45 | !shipping
46 | ) {
47 | return res.status(400).json({ error: 'All fields are required' });
48 | }
49 |
50 | let product = new Product(fields);
51 |
52 | if (files.photo) {
53 | if (files.photo.size > 1000000) {
54 | return res
55 | .status(400)
56 | .json({ error: 'Image should be less than 1MB in size' });
57 | }
58 | product.photo.data = fs.readFileSync(files.photo.path);
59 | product.photo.contentType = files.photo.type;
60 | }
61 |
62 | try {
63 | const result = await product.save();
64 | res.json(result);
65 | } catch (error) {
66 | return res.status(400).json({ error: errorHandler(error) });
67 | }
68 | });
69 | };
70 |
71 | // Delete a product
72 | exports.remove = async (req, res) => {
73 | try {
74 | let product = req.product;
75 | await product.remove();
76 | res.json({ message: 'Product deleted successfully' });
77 | } catch (err) {
78 | return res.status(400).json({ error: errorHandler(err) });
79 | }
80 | };
81 |
82 | // Update a product
83 | exports.update = async (req, res) => {
84 | const form = new formidable.IncomingForm();
85 | form.keepExtensions = true;
86 |
87 | form.parse(req, async (err, fields, files) => {
88 | if (err) {
89 | return res.status(400).json({ error: 'Image could not be uploaded' });
90 | }
91 |
92 | let product = req.product;
93 | product = _.extend(product, fields);
94 |
95 | if (files.photo) {
96 | if (files.photo.size > 1000000) {
97 | return res
98 | .status(400)
99 | .json({ error: 'Image should be less than 1MB in size' });
100 | }
101 | product.photo.data = fs.readFileSync(files.photo.path);
102 | product.photo.contentType = files.photo.type;
103 | }
104 |
105 | try {
106 | const result = await product.save();
107 | res.json(result);
108 | } catch (err) {
109 | return res.status(400).json({ error: errorHandler(err) });
110 | }
111 | });
112 | };
113 |
114 | // List products with filters and pagination
115 | exports.list = async (req, res) => {
116 | const order = req.query.order || 'asc';
117 | const sortBy = req.query.sortBy || '_id';
118 | const limit = req.query.limit ? parseInt(req.query.limit) : 6;
119 |
120 | try {
121 | const products = await Product.find()
122 | .select('-photo')
123 | .populate('category')
124 | .sort([[sortBy, order]])
125 | .limit(limit)
126 | .exec();
127 | res.json(products);
128 | } catch (error) {
129 | return res.status(400).json({ error: 'Products not found' });
130 | }
131 | };
132 |
133 | // List related products based on category
134 | exports.listRelated = async (req, res) => {
135 | const limit = req.query.limit ? parseInt(req.query.limit) : 6;
136 |
137 | try {
138 | const products = await Product.find({
139 | _id: { $ne: req.product._id },
140 | category: req.product.category,
141 | })
142 | .limit(limit)
143 | .populate('category', '_id name')
144 | .exec();
145 | res.json(products);
146 | } catch (error) {
147 | return res.status(400).json({ error: 'Products not found' });
148 | }
149 | };
150 |
151 | // List categories used in products
152 | exports.listCategories = async (req, res) => {
153 | try {
154 | const categories = await Product.distinct('category', {}).exec();
155 | res.json(categories);
156 | } catch (error) {
157 | return res.status(400).json({ error: 'Categories not found' });
158 | }
159 | };
160 |
161 | // List products by search
162 | exports.listBySearch = async (req, res) => {
163 | const order = req.body.order || 'desc';
164 | const sortBy = req.body.sortBy || '_id';
165 | const limit = req.body.limit ? parseInt(req.body.limit) : 100;
166 | const skip = parseInt(req.body.skip);
167 | const findArgs = {};
168 |
169 | for (let key in req.body.filters) {
170 | if (req.body.filters[key].length > 0) {
171 | if (key === 'price') {
172 | findArgs[key] = {
173 | $gte: req.body.filters[key][0],
174 | $lte: req.body.filters[key][1],
175 | };
176 | } else {
177 | findArgs[key] = req.body.filters[key];
178 | }
179 | }
180 | }
181 |
182 | try {
183 | const products = await Product.find(findArgs)
184 | .select('-photo')
185 | .populate('category')
186 | .sort([[sortBy, order]])
187 | .skip(skip)
188 | .limit(limit)
189 | .exec();
190 | res.json({ size: products.length, data: products });
191 | } catch (error) {
192 | return res.status(400).json({ error: 'Products not found' });
193 | }
194 | };
195 |
196 | // Product photo handler
197 | exports.photo = (req, res, next) => {
198 | if (req.product.photo.data) {
199 | res.set('Content-Type', req.product.photo.contentType);
200 | return res.send(req.product.photo.data);
201 | }
202 | next();
203 | };
204 |
205 | // List products by search (query-based)
206 | exports.listSearch = async (req, res) => {
207 | const query = {};
208 |
209 | if (req.query.search) {
210 | query.name = { $regex: req.query.search, $options: 'i' };
211 |
212 | if (req.query.category && req.query.category !== 'All') {
213 | query.category = req.query.category;
214 | }
215 |
216 | try {
217 | const products = await Product.find(query).select('-photo').exec();
218 | res.json(products);
219 | } catch (error) {
220 | return res.status(400).json({ error: errorHandler(error) });
221 | }
222 | }
223 | };
224 |
225 | // Decrease product quantity after purchase
226 | exports.decreaseQuantity = async (req, res, next) => {
227 | const bulkOps = req.body.order.products.map((item) => ({
228 | updateOne: {
229 | filter: { _id: item._id },
230 | update: { $inc: { quantity: -item.count, sold: +item.count } },
231 | },
232 | }));
233 |
234 | try {
235 | await Product.bulkWrite(bulkOps, {});
236 | next();
237 | } catch (error) {
238 | return res.status(400).json({ error: 'Could not update product' });
239 | }
240 | };
241 |
--------------------------------------------------------------------------------
/controllers/user.js:
--------------------------------------------------------------------------------
1 | const User = require('../models/user');
2 | const { Order } = require('../models/order');
3 | const { errorHandler } = require('../helpers/dbErrorHandler');
4 |
5 | exports.userById = async (req, res, next, id) => {
6 | try {
7 | const user = await User.findById(id);
8 | if (!user) {
9 | return res.status(400).json({ error: 'User not found' });
10 | }
11 | req.profile = user;
12 | next();
13 | } catch (err) {
14 | return res.status(400).json({ error: 'User not found' });
15 | }
16 | };
17 |
18 | exports.read = (req, res) => {
19 | req.profile.hashed_password = undefined;
20 | req.profile.salt = undefined;
21 | return res.json(req.profile);
22 | };
23 |
24 | exports.update = async (req, res) => {
25 | try {
26 | const user = await User.findByIdAndUpdate(
27 | { _id: req.profile._id },
28 | { $set: req.body },
29 | { new: true }
30 | );
31 | if (!user) {
32 | return res.status(400).json({
33 | error: 'You are not authorized to perform this action',
34 | });
35 | }
36 | user.hashed_password = undefined;
37 | user.salt = undefined;
38 | res.json(user);
39 | } catch (err) {
40 | return res.status(400).json({
41 | error: 'You are not authorized to perform this action',
42 | });
43 | }
44 | };
45 |
46 | exports.addOrderToUserHistory = async (req, res, next) => {
47 | let history = [];
48 |
49 | req.body.order.products.forEach((item) => {
50 | history.push({
51 | _id: item._id,
52 | name: item.name,
53 | description: item.description,
54 | category: item.category,
55 | quantity: item.count,
56 | transaction_id: req.body.order.transaction_id,
57 | amount: req.body.order.amount,
58 | });
59 | });
60 |
61 | try {
62 | await User.findByIdAndUpdate(
63 | { _id: req.profile._id },
64 | { $push: { history: history } },
65 | { new: true }
66 | );
67 | next();
68 | } catch (error) {
69 | return res.status(400).json({
70 | error: 'Could not update user purchase history',
71 | });
72 | }
73 | };
74 |
75 | exports.purchaseHistory = async (req, res) => {
76 | try {
77 | const orders = await Order.find({ user: req.profile._id })
78 | .populate('user', '_id name')
79 | .sort('-created');
80 | res.json(orders);
81 | } catch (err) {
82 | return res.status(400).json({
83 | error: errorHandler(err),
84 | });
85 | }
86 | };
87 |
--------------------------------------------------------------------------------
/helpers/dbErrorHandler.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Get unique error field name
5 | */
6 | const uniqueMessage = (error) => {
7 | let output;
8 | try {
9 | let fieldName = error.message.substring(
10 | error.message.lastIndexOf('.$') + 2,
11 | error.message.lastIndexOf('_1')
12 | );
13 | output =
14 | fieldName.charAt(0).toUpperCase() +
15 | fieldName.slice(1) +
16 | ' already exists';
17 | } catch (ex) {
18 | output = 'Unique field already exists';
19 | }
20 |
21 | return output;
22 | };
23 |
24 | /**
25 | * Get the erroror message from error object
26 | */
27 | exports.errorHandler = (error) => {
28 | let message = '';
29 |
30 | if (error.code) {
31 | switch (error.code) {
32 | case 11000:
33 | case 11001:
34 | message = uniqueMessage(error);
35 | break;
36 | default:
37 | message = 'Something went wrong';
38 | }
39 | } else {
40 | for (let errorName in error.errorors) {
41 | if (error.errorors[errorName].message)
42 | message = error.errorors[errorName].message;
43 | }
44 | }
45 |
46 | return message;
47 | };
48 |
--------------------------------------------------------------------------------
/models/category.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const categorySchema = new mongoose.Schema(
4 | {
5 | name: {
6 | type: String,
7 | trim: true,
8 | required: true,
9 | maxlength: 32,
10 | unique: true,
11 | },
12 | },
13 | { timestamps: true }
14 | );
15 |
16 | module.exports = mongoose.model('Category', categorySchema);
17 |
--------------------------------------------------------------------------------
/models/order.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 | const { ObjectId } = mongoose.Schema;
4 |
5 | const CartItemSchema = new mongoose.Schema(
6 | {
7 | product: { type: ObjectId, ref: 'Product' },
8 | name: String,
9 | price: Number,
10 | count: Number,
11 | },
12 | { timestamps: true }
13 | );
14 |
15 | const CartItem = mongoose.model('CartItem', CartItemSchema);
16 |
17 | const OrderSchema = new mongoose.Schema(
18 | {
19 | products: [CartItemSchema],
20 | transaction_id: {},
21 | amount: { type: Number },
22 | address: String,
23 | status: {
24 | type: String,
25 | default: 'Not processed',
26 | enum: [
27 | 'Not processed',
28 | 'Processing',
29 | 'Shipped',
30 | 'Delivered',
31 | 'Cancelled',
32 | ], // enum means string objects
33 | },
34 | updated: Date,
35 | user: { type: ObjectId, ref: 'User' },
36 | },
37 | { timestamps: true }
38 | );
39 |
40 | const Order = mongoose.model('Order', OrderSchema);
41 |
42 | module.exports = { Order, CartItem };
43 |
--------------------------------------------------------------------------------
/models/product.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const { ObjectId } = mongoose.Schema;
3 |
4 | const productSchema = new mongoose.Schema(
5 | {
6 | name: {
7 | type: String,
8 | trim: true,
9 | required: true,
10 | maxlength: 32,
11 | },
12 | description: {
13 | type: String,
14 | required: true,
15 | maxlength: 2000,
16 | },
17 | price: {
18 | type: Number,
19 | trim: true,
20 | required: true,
21 | maxlength: 32,
22 | },
23 | category: {
24 | type: ObjectId,
25 | ref: 'Category',
26 | required: true,
27 | },
28 | quantity: {
29 | type: Number,
30 | },
31 | sold: {
32 | type: Number,
33 | default: 0,
34 | },
35 | photo: {
36 | data: Buffer,
37 | contentType: String,
38 | },
39 | shipping: {
40 | required: false,
41 | type: Boolean,
42 | },
43 | },
44 | { timestamps: true }
45 | );
46 |
47 | module.exports = mongoose.model('Product', productSchema);
48 |
--------------------------------------------------------------------------------
/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const crypto = require('crypto');
3 | const { v1: uuidv1 } = require('uuid');
4 |
5 | const userSchema = new mongoose.Schema(
6 | {
7 | name: {
8 | type: String,
9 | trim: true,
10 | required: true,
11 | maxlength: 32,
12 | },
13 | email: {
14 | type: String,
15 | trim: true,
16 | required: true,
17 | unique: true,
18 | },
19 | hashed_password: {
20 | type: String,
21 | required: true,
22 | },
23 | about: {
24 | type: String,
25 | trim: true,
26 | },
27 | salt: String,
28 | role: {
29 | type: Number,
30 | default: 0,
31 | },
32 | history: {
33 | type: Array,
34 | default: [],
35 | },
36 | },
37 | { timestamps: true }
38 | );
39 |
40 | // virtual field
41 | userSchema
42 | .virtual('password')
43 | .set(function (password) {
44 | this._password = password;
45 | this.salt = uuidv1();
46 | this.hashed_password = this.encryptPassword(password);
47 | })
48 | .get(function () {
49 | return this._password;
50 | });
51 |
52 | userSchema.methods = {
53 | authenticate: function(plainText) {
54 | return this.encryptPassword(plainText) === this.hashed_password;
55 | },
56 |
57 | encryptPassword: function (password) {
58 | if (!password) return '';
59 | try {
60 | return crypto
61 | .createHmac('sha1', this.salt)
62 | .update(password)
63 | .digest('hex');
64 | } catch (err) {
65 | return '';
66 | }
67 | },
68 | };
69 |
70 | module.exports = mongoose.model('User', userSchema);
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mern-ecommerce",
3 | "version": "1.0.0",
4 | "description": "ecommerce site",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "server": "nodemon server.js",
9 | "client": "npm start --prefix client",
10 | "dev": "concurrently \"npm run server\" \"npm run client\"",
11 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/ashraf-kabir/mern-ecommerce.git"
16 | },
17 | "keywords": [],
18 | "author": "Ashraf Kabir",
19 | "license": "MIT",
20 | "dependencies": {
21 | "axios": "^1.7.7",
22 | "body-parser": "^1.19.0",
23 | "braintree": "^2.23.0",
24 | "concurrently": "^5.3.0",
25 | "config": "^3.3.1",
26 | "cookie-parser": "^1.4.5",
27 | "cors": "^2.8.5",
28 | "dotenv": "^8.2.0",
29 | "express": "^4.17.1",
30 | "express-jwt": "^5.3.1",
31 | "express-validator": "^5.3.1",
32 | "formidable": "^1.2.2",
33 | "jsonwebtoken": "^8.5.1",
34 | "lodash": "^4.17.19",
35 | "mongodb": "^6.9.0",
36 | "mongoose": "^8.7.0",
37 | "morgan": "^1.10.0",
38 | "nodemon": "^2.0.4",
39 | "uuid": "^8.2.0",
40 | "uuidv1": "^1.6.14"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/routes/auth.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const {
5 | signup,
6 | signin,
7 | signout,
8 | requireSignin,
9 | } = require('../controllers/auth');
10 | const { userSignupValidator } = require('../validator');
11 |
12 | router.post('/signup', userSignupValidator, signup);
13 | router.post('/signin', signin);
14 | router.get('/signout', signout);
15 |
16 | module.exports = router;
17 |
--------------------------------------------------------------------------------
/routes/braintree.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const { requireSignin, isAuth } = require('../controllers/auth');
5 | const { userById } = require('../controllers/user');
6 | const { generateToken, processPayment } = require('../controllers/braintree');
7 |
8 | router.get('/braintree/getToken/:userId', requireSignin, isAuth, generateToken);
9 | router.post(
10 | '/braintree/payment/:userId',
11 | requireSignin,
12 | isAuth,
13 | processPayment
14 | );
15 |
16 | router.param('userId', userById);
17 |
18 | module.exports = router;
19 |
--------------------------------------------------------------------------------
/routes/category.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const {
5 | create,
6 | categoryById,
7 | read,
8 | update,
9 | remove,
10 | list
11 | } = require('../controllers/category');
12 | const { requireSignin, isAuth, isAdmin } = require('../controllers/auth');
13 | const { userById } = require('../controllers/user');
14 |
15 | router.get('/category/:categoryId', read);
16 | router.post('/category/create/:userId', requireSignin, isAuth, isAdmin, create);
17 | router.put(
18 | '/category/:categoryId/:userId',
19 | requireSignin,
20 | isAuth,
21 | isAdmin,
22 | update
23 | );
24 | router.delete(
25 | '/category/:categoryId/:userId',
26 | requireSignin,
27 | isAuth,
28 | isAdmin,
29 | remove
30 | );
31 | router.get('/categories', list);
32 |
33 | router.param('categoryId', categoryById);
34 | router.param('userId', userById);
35 |
36 | module.exports = router;
37 |
--------------------------------------------------------------------------------
/routes/order.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const { requireSignin, isAuth, isAdmin } = require('../controllers/auth');
5 | const { userById, addOrderToUserHistory } = require('../controllers/user');
6 | const {
7 | create,
8 | listOrders,
9 | getStatusValues,
10 | orderById,
11 | updateOrderStatus,
12 | } = require('../controllers/order');
13 |
14 | const { decreaseQuantity } = require('../controllers/product');
15 |
16 | router.post(
17 | '/order/create/:userId',
18 | requireSignin,
19 | isAuth,
20 | addOrderToUserHistory,
21 | decreaseQuantity,
22 | create
23 | );
24 |
25 | router.get('/order/list/:userId', requireSignin, isAuth, isAdmin, listOrders);
26 |
27 | router.get(
28 | '/order/status-values/:userId',
29 | requireSignin,
30 | isAuth,
31 | isAdmin,
32 | getStatusValues
33 | );
34 |
35 | router.put(
36 | '/order/:orderId/status/:userId',
37 | requireSignin,
38 | isAuth,
39 | isAdmin,
40 | updateOrderStatus
41 | );
42 |
43 | router.param('userId', userById);
44 | router.param('orderId', orderById);
45 |
46 | module.exports = router;
47 |
--------------------------------------------------------------------------------
/routes/product.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const {
5 | create,
6 | productById,
7 | read,
8 | update,
9 | remove,
10 | list,
11 | listRelated,
12 | listCategories,
13 | listBySearch,
14 | photo,
15 | listSearch
16 | } = require('../controllers/product');
17 | const { requireSignin, isAuth, isAdmin } = require('../controllers/auth');
18 | const { userById } = require('../controllers/user');
19 |
20 | router.get('/product/:productId', read);
21 | router.post('/product/create/:userId', requireSignin, isAuth, isAdmin, create);
22 | router.delete(
23 | '/product/:productId/:userId',
24 | requireSignin,
25 | isAuth,
26 | isAdmin,
27 | remove
28 | );
29 |
30 | router.put(
31 | '/product/:productId/:userId',
32 | requireSignin,
33 | isAuth,
34 | isAdmin,
35 | update
36 | );
37 |
38 | router.get('/products', list);
39 | router.get('/products/search', listSearch);
40 | router.get('/products/related/:productId', listRelated);
41 | router.get('/products/categories', listCategories);
42 | router.post('/products/by/search', listBySearch);
43 | router.get('/product/photo/:productId', photo);
44 |
45 | router.param('userId', userById);
46 | router.param('productId', productById);
47 |
48 | module.exports = router;
49 |
--------------------------------------------------------------------------------
/routes/user.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const { requireSignin, isAuth, isAdmin } = require('../controllers/auth');
5 |
6 | const {
7 | userById,
8 | read,
9 | update,
10 | purchaseHistory,
11 | } = require('../controllers/user');
12 |
13 | router.get('/secret/:userId', requireSignin, isAuth, isAdmin, (req, res) => {
14 | res.json({
15 | user: req.profile,
16 | });
17 | });
18 |
19 | router.get('/user/:userId', requireSignin, isAuth, read);
20 | router.put('/user/:userId', requireSignin, isAuth, update);
21 | router.get('/orders/by/user/:userId', requireSignin, isAuth, purchaseHistory);
22 |
23 | router.param('userId', userById);
24 |
25 | module.exports = router;
26 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const mongoose = require('mongoose');
3 | const morgan = require('morgan');
4 | const bodyParser = require('body-parser');
5 | const cookieParser = require('cookie-parser');
6 | const cors = require('cors');
7 | const path = require('path');
8 | const expressValidator = require('express-validator');
9 | require('dotenv').config();
10 | // import routes
11 | const authRoutes = require('./routes/auth');
12 | const userRoutes = require('./routes/user');
13 | const categoryRoutes = require('./routes/category');
14 | const productRoutes = require('./routes/product');
15 | const braintreeRoutes = require('./routes/braintree');
16 | const orderRoutes = require('./routes/order');
17 |
18 | // app
19 | const app = express();
20 |
21 | // db connection
22 | const connectDB = async () => {
23 | try {
24 | await mongoose.connect(process.env.MONGODB_URI);
25 | console.log('MongoDB Connected');
26 | } catch (err) {
27 | console.error(err.message);
28 | }
29 | };
30 | connectDB();
31 |
32 | // middlewares
33 | app.use(morgan('dev'));
34 | app.use(bodyParser.json());
35 | app.use(cookieParser());
36 | app.use(expressValidator());
37 | app.use(cors());
38 |
39 | // routes middleware
40 | app.use('/api', authRoutes);
41 | app.use('/api', userRoutes);
42 | app.use('/api', categoryRoutes);
43 | app.use('/api', productRoutes);
44 | app.use('/api', braintreeRoutes);
45 | app.use('/api', orderRoutes);
46 |
47 | // Server static assets if in production
48 | if (process.env.NODE_ENV === 'production') {
49 | // Set static folder
50 | app.use(express.static('client/build'));
51 |
52 | app.get('*', (req, res) => {
53 | res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
54 | });
55 | }
56 |
57 | const PORT = process.env.PORT || 5000;
58 |
59 | app.listen(PORT, () => {
60 | console.log(`Server is running on port ${PORT}`);
61 | });
62 |
--------------------------------------------------------------------------------
/validator/index.js:
--------------------------------------------------------------------------------
1 | exports.userSignupValidator = (req, res, next) => {
2 | req.check('name', 'Name is required').notEmpty();
3 | req
4 | .check('email', 'Email must be between 3 to 32 characters')
5 | .matches(/.+\@.+\..+/)
6 | .withMessage('Email must contain @')
7 | .isLength({
8 | min: 4,
9 | max: 32,
10 | });
11 | req.check('password', 'Password is required').notEmpty();
12 | req
13 | .check('password')
14 | .isLength({ min: 6 })
15 | .withMessage('Password must contain at least 6 characters')
16 | .matches(/\d/)
17 | .withMessage('Password must contain a number');
18 | const errors = req.validationErrors();
19 | if (errors) {
20 | const firstError = errors.map((error) => error.msg)[0];
21 | return res.status(400).json({ error: firstError });
22 | }
23 | next();
24 | };
25 |
--------------------------------------------------------------------------------