├── .eslintrc
├── .gitignore
├── README.md
├── components
├── CheckoutWizard.js
├── Layout.js
└── ProductItem.js
├── models
├── Order.js
├── Product.js
└── User.js
├── next.config.js
├── package-lock.json
├── package.json
├── pages
├── _app.js
├── _document.js
├── admin
│ ├── dashboard.js
│ ├── orders.js
│ ├── product
│ │ └── [id].js
│ ├── products.js
│ ├── user
│ │ └── [id].js
│ └── users.js
├── api
│ ├── admin
│ │ ├── orders.js
│ │ ├── products
│ │ │ ├── [id]
│ │ │ │ └── index.js
│ │ │ └── index.js
│ │ ├── summary.js
│ │ ├── upload.js
│ │ └── users
│ │ │ ├── [id]
│ │ │ └── index.js
│ │ │ └── index.js
│ ├── hello.js
│ ├── keys
│ │ ├── google.js
│ │ └── paypal.js
│ ├── orders
│ │ ├── [id]
│ │ │ ├── deliver.js
│ │ │ ├── index.js
│ │ │ └── pay.js
│ │ ├── history.js
│ │ └── index.js
│ ├── products
│ │ ├── [id]
│ │ │ ├── index.js
│ │ │ └── reviews.js
│ │ ├── categories.js
│ │ └── index.js
│ ├── seed.js
│ └── users
│ │ ├── login.js
│ │ ├── profile.js
│ │ └── register.js
├── cart.js
├── index.js
├── login.js
├── map.js
├── order-history.js
├── order
│ └── [id].js
├── payment.js
├── placeorder.js
├── product
│ └── [slug].js
├── profile.js
├── register.js
├── search.js
└── shipping.js
├── public
├── favicon.ico
├── images
│ ├── banner1.jpg
│ ├── banner2.jpg
│ ├── pants1.jpg
│ ├── pants2.jpg
│ ├── pants3.jpg
│ ├── shirt1.jpg
│ ├── shirt2.jpg
│ └── shirt3.jpg
└── vercel.svg
├── styles
├── Home.module.css
└── globals.css
└── utils
├── Store.js
├── auth.js
├── data.js
├── db.js
├── error.js
└── styles.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {"browser": true,"node":true, "es6": true},
3 | "extends": ["eslint:recommended","next", "next/core-web-vitals"]
4 | }
5 |
--------------------------------------------------------------------------------
/.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 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 | .env
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Next Amazona
2 | Build ECommerce Website Like Amazon by Next.js
3 | - Source Code : https://github.com/basir/next-amazona
4 | - Demo Website : https://nextjs-amazona-final.vercel.app
5 |
6 | ## What you will learn
7 | - NextJS basics like setting up project, navigating between pages and data fetching
8 | - NextJS advanced topics like dynamic routing, image optimization, SSG and SSR
9 | - MaterialUI framework to build responsive website using custom theme, animation and carousel
10 | - ReactJS including decomposing components, context API and hooks
11 | - Next Connect package to build backend API
12 | - MongoDB and Mongoose to save and retrieve data like products, orders and users
13 | - PayPal developer api to make online payment
14 | - Deploy web applications on servers like Vercel and Netlify
15 |
16 | ## Full Course
17 | Learn building this ecommerce website on Udemy with 90% discount:
18 | https://www.udemy.com/course/nextjs-ecommerce
19 |
20 | ## Run it Locally
21 | ```
22 | $ git clone https://github.com/basir/next-amazona
23 | $ cd next-amazona
24 | $ npm install
25 | $ npm run dev
26 | $ Open http://localhost:3000/api/seed
27 | $ Open http://localhost:3000
28 | ```
29 |
30 | ## Lessons
31 | 1. Introduction
32 | 1. What you will learn
33 | 2. What you will build
34 | 3. What Packages you will use
35 | 2. Install Tools
36 | 1. VS Code
37 | 2. Chrome
38 | 3. Node.js
39 | 4. MongoDB
40 | 3. Create Next App
41 | 1. npx create-next-app
42 | 2. add layout component
43 | 3. add header, main and footer
44 | 4. Add Styles
45 | 1. add css to header, main and footer
46 | 5. Fix SSR Issue on MaterialUI
47 | 1. add _documents.js
48 | 2. add code to fix styling issue
49 | 6. List Products
50 | 1. add data.js
51 | 2. add images
52 | 3. render products
53 | 7. Add header links
54 | 1. Add cart and login link
55 | 2. use next/link and mui/link
56 | 3. add css classes for header links
57 | 8. Route Product Details Page
58 | 1. Make Product cards linkable
59 | 2. Create /product/[slug] route
60 | 3. find product based on slug
61 | 9. Create Product Details Page
62 | 1. Create 3 columns
63 | 2. show image in first column
64 | 3. show product info in second column
65 | 4. show add to cart action on third column
66 | 5. add styles
67 | 10. Add MaterialUI Theme
68 | 1. create theme
69 | 2. use theme provider
70 | 3. add h1 and h2 styles
71 | 4. set theme colors
72 | 11. Create Application Context
73 | 1. define context and reducer
74 | 2. set darkMode flag
75 | 3. create store provider
76 | 4. use it on layout
77 | 12. Connect To MongoDB
78 | 1. install mongodb
79 | 2. install mongoose
80 | 3. define connect and disconnect
81 | 4. use it in the api
82 | 13. Create Products API
83 | 1. create product model
84 | 2. seed sample data
85 | 3. create /api/products/index.js
86 | 4. create product api
87 | 14. Fetch Products From API
88 | 1. use getServerSideProps()
89 | 3. get product from db
90 | 4. return data as props
91 | 5. use it in product screen too
92 | 15. Implement Add to cart
93 | 1. define cart in context
94 | 2. dispatch add to cart action
95 | 3. set click event handler for button
96 | 16. Create Cart Screen
97 | 1. create cart.js
98 | 2. redirect to cart screen
99 | 4. use context to get cart items
100 | 5. list items in cart items
101 | 17. Use Dynamic Import In Cart Screen
102 | 1. Use next/dynamic
103 | 2. Wrap cart in dynamic with out ssr
104 | 18. Update Remove Items In Cart
105 | 1. Implement onChange for Select
106 | 2. Show notification by notistack
107 | 3. implement delete button handler
108 | 19. Create Login Page
109 | 1. create form
110 | 2. add email and password field
111 | 3. add login button
112 | 4. style form
113 | 20. Create Sample Users
114 | 1. create user model
115 | 2. add sample user in seed api
116 | 21. Build Login API
117 | 3. use jsonwebtoken to sign token
118 | 4. implement login api
119 | 22. Complete Login Page
120 | 1. handle form submission
121 | 2. add userInfo to context
122 | 3. save userInfo in cookies
123 | 4. show user name in nav bar using menu
124 | 23. Create Register Page
125 | 1. create form
126 | 2. implement backend api
127 | 3. redirect user to redirect page
128 | 24. Login and Register Form Validation
129 | 1. install react-hook-form
130 | 2. change input to controller
131 | 3. use notistack to show errors
132 | 25. Create Shipping Page
133 | 4. create form
134 | 5. add address fields
135 | 8. save address in context
136 | 26. Create Payment Page
137 | 1. create form
138 | 2. add radio button
139 | 3. save method in context
140 | 27. Create Place Order Page
141 | 1. display order info
142 | 2. show order summary
143 | 3. add place order button
144 | 28. Implement Place Order Action
145 | 1. create click handler
146 | 2. send ajax request
147 | 4. clear cart
148 | 5. redirect to order screen
149 | 3. create backend api
150 | 29. Create Order Details Page
151 | 1. create api to order info
152 | 2. create payment, shipping and items
153 | 3. create order summary
154 | 30. Pay Order By PayPal
155 | 1. install paypal button
156 | 2. use it in order screen
157 | 3. implement pay order api
158 | 31. Display Orders History
159 | 1. create orders api
160 | 2. show orders in profile screen
161 | 32. Update User Profile
162 | 1. create profile screen
163 | 2. create update profile api
164 | 33. Create Admin Dashboard
165 | 1. Create Admin Menu
166 | 2. Add Admin Auth Middleware
167 | 3. Implement admin summary api
168 | 34. List Orders For Admin
169 | 1. fix isAdmin middleware
170 | 2. create orders page
171 | 3. create orders api
172 | 4. use api in page
173 | 35. Deliver Order For Admin
174 | 1. create deliver api
175 | 2. add deliver button
176 | 3. implement click handler
177 | 36. List Products For Admin
178 | 2. create products page
179 | 3. create products api
180 | 4. use api in page
181 | 37. Create Product Edit Page
182 | 1. create edit page
183 | 2. create api for product
184 | 3. show product data in form
185 | 38. Update Product
186 | 1. create form submit handler
187 | 2. create backend api for update
188 | 39. Upload Product Image
189 | 1. create cloudinary account
190 | 2. get cloudinary keys
191 | 3. create upload api
192 | 4. upload files in edit page
193 | 40. Create And Delete Products
194 | 1. add create product button
195 | 2. build new product api
196 | 3. add handler for delete
197 | 4. implement delete api
198 | 41. List Users For Admin
199 | 1. create users page
200 | 2. create users api
201 | 3. use api in page
202 | 42. Create User Edit Page
203 | 1. create edit page
204 | 2. create api for user
205 | 3. show user data in form
206 | 43. Deploy on Vercel
207 | 1. create vercel account
208 | 2. connect to github
209 | 3. create altas mongodb db
210 | 4. push code to github
211 | 44. Review Products
212 | 1. add reviews model
213 | 2. create api for reviews
214 | 3. create review form
215 | 4. show reviews on home screen
216 | 45. Create Sidebar
217 | 1. add drawer
218 | 2. list categories
219 | 3. redirect to search screen
220 | 46. Create Search Box
221 | 1. add form
222 | 2. handle form submit
223 | 3. redirect to search screen
224 | 47. Create Search Page
225 | 1. create filters
226 | 2. list products
227 | 3. show filters
228 | 48. Add Carousel
229 | 1. create featured products
230 | 2. feed carousel data
231 | 3. show popular products
232 | 49. Choose Location on Map
233 | 1. add google map
234 | 2. create map screen
235 | 3. choose location
236 | 4. show in order screen
237 |
238 |
--------------------------------------------------------------------------------
/components/CheckoutWizard.js:
--------------------------------------------------------------------------------
1 | import { Step, StepLabel, Stepper } from '@material-ui/core';
2 | import React from 'react';
3 | import useStyles from '../utils/styles';
4 |
5 | export default function CheckoutWizard({ activeStep = 0 }) {
6 | const classes = useStyles();
7 | return (
8 |
13 | {['Login', 'Shipping Address', 'Payment Method', 'Place Order'].map(
14 | (step) => (
15 |
16 | {step}
17 |
18 | )
19 | )}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/components/Layout.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import Head from 'next/head';
3 | import NextLink from 'next/link';
4 | import {
5 | AppBar,
6 | Toolbar,
7 | Typography,
8 | Container,
9 | Link,
10 | createMuiTheme,
11 | ThemeProvider,
12 | CssBaseline,
13 | Switch,
14 | Badge,
15 | Button,
16 | Menu,
17 | MenuItem,
18 | Box,
19 | IconButton,
20 | Drawer,
21 | List,
22 | ListItem,
23 | Divider,
24 | ListItemText,
25 | InputBase,
26 | } from '@material-ui/core';
27 | import MenuIcon from '@material-ui/icons/Menu';
28 | import CancelIcon from '@material-ui/icons/Cancel';
29 | import SearchIcon from '@material-ui/icons/Search';
30 | import useStyles from '../utils/styles';
31 | import { Store } from '../utils/Store';
32 | import { getError } from '../utils/error';
33 | import Cookies from 'js-cookie';
34 | import { useState } from 'react';
35 | import { useRouter } from 'next/router';
36 | import { useSnackbar } from 'notistack';
37 | import axios from 'axios';
38 | import { useEffect } from 'react';
39 |
40 | export default function Layout({ title, description, children }) {
41 | const router = useRouter();
42 | const { state, dispatch } = useContext(Store);
43 | const { darkMode, cart, userInfo } = state;
44 | const theme = createMuiTheme({
45 | typography: {
46 | h1: {
47 | fontSize: '1.6rem',
48 | fontWeight: 400,
49 | margin: '1rem 0',
50 | },
51 | h2: {
52 | fontSize: '1.4rem',
53 | fontWeight: 400,
54 | margin: '1rem 0',
55 | },
56 | },
57 | palette: {
58 | type: darkMode ? 'dark' : 'light',
59 | primary: {
60 | main: '#f0c000',
61 | },
62 | secondary: {
63 | main: '#208080',
64 | },
65 | },
66 | });
67 | const classes = useStyles();
68 |
69 | const [sidbarVisible, setSidebarVisible] = useState(false);
70 | const sidebarOpenHandler = () => {
71 | setSidebarVisible(true);
72 | };
73 | const sidebarCloseHandler = () => {
74 | setSidebarVisible(false);
75 | };
76 |
77 | const [categories, setCategories] = useState([]);
78 | const { enqueueSnackbar } = useSnackbar();
79 |
80 | const fetchCategories = async () => {
81 | try {
82 | const { data } = await axios.get(`/api/products/categories`);
83 | setCategories(data);
84 | } catch (err) {
85 | enqueueSnackbar(getError(err), { variant: 'error' });
86 | }
87 | };
88 |
89 | const [query, setQuery] = useState('');
90 | const queryChangeHandler = (e) => {
91 | setQuery(e.target.value);
92 | };
93 | const submitHandler = (e) => {
94 | e.preventDefault();
95 | router.push(`/search?query=${query}`);
96 | };
97 |
98 | useEffect(() => {
99 | fetchCategories();
100 | }, []);
101 |
102 | const darkModeChangeHandler = () => {
103 | dispatch({ type: darkMode ? 'DARK_MODE_OFF' : 'DARK_MODE_ON' });
104 | const newDarkMode = !darkMode;
105 | Cookies.set('darkMode', newDarkMode ? 'ON' : 'OFF');
106 | };
107 | const [anchorEl, setAnchorEl] = useState(null);
108 | const loginClickHandler = (e) => {
109 | setAnchorEl(e.currentTarget);
110 | };
111 | const loginMenuCloseHandler = (e, redirect) => {
112 | setAnchorEl(null);
113 | if (redirect) {
114 | router.push(redirect);
115 | }
116 | };
117 | const logoutClickHandler = () => {
118 | setAnchorEl(null);
119 | dispatch({ type: 'USER_LOGOUT' });
120 | Cookies.remove('userInfo');
121 | Cookies.remove('cartItems');
122 | Cookies.remove('shippinhAddress');
123 | Cookies.remove('paymentMethod');
124 | router.push('/');
125 | };
126 | return (
127 |
128 |
129 |
{title ? `${title} - Next Amazona` : 'Next Amazona'}
130 | {description &&
}
131 |
132 |
133 |
134 |
135 |
136 |
137 |
143 |
144 |
145 |
146 |
147 | amazona
148 |
149 |
150 |
151 |
156 |
157 |
158 |
163 | Shopping by category
164 |
168 |
169 |
170 |
171 |
172 |
173 | {categories.map((category) => (
174 |
179 |
184 |
185 |
186 |
187 | ))}
188 |
189 |
190 |
191 |
192 |
207 |
208 |
209 |
213 |
214 |
215 |
216 | {cart.cartItems.length > 0 ? (
217 |
221 | Cart
222 |
223 | ) : (
224 | 'Cart'
225 | )}
226 |
227 |
228 |
229 | {userInfo ? (
230 | <>
231 |
239 |
269 | >
270 | ) : (
271 |
272 |
273 | Login
274 |
275 |
276 | )}
277 |
278 |
279 |
280 | {children}
281 |
284 |
285 |
286 | );
287 | }
288 |
--------------------------------------------------------------------------------
/components/ProductItem.js:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Card,
4 | CardActionArea,
5 | CardActions,
6 | CardContent,
7 | CardMedia,
8 | Typography,
9 | } from '@material-ui/core';
10 | import React from 'react';
11 | import NextLink from 'next/link';
12 | import Rating from '@material-ui/lab/Rating';
13 |
14 | export default function ProductItem({ product, addToCartHandler }) {
15 | return (
16 |
17 |
18 |
19 |
24 |
25 | {product.name}
26 |
27 |
28 |
29 |
30 |
31 | ${product.price}
32 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/models/Order.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const orderSchema = new mongoose.Schema(
4 | {
5 | user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
6 | orderItems: [
7 | {
8 | name: { type: String, required: true },
9 | quantity: { type: Number, required: true },
10 | image: { type: String, required: true },
11 | price: { type: Number, required: true },
12 | },
13 | ],
14 | shippingAddress: {
15 | fullName: { type: String, required: true },
16 | address: { type: String, required: true },
17 | city: { type: String, required: true },
18 | postalCode: { type: String, required: true },
19 | country: { type: String, required: true },
20 | location: {
21 | lat: String,
22 | lng: String,
23 | address: String,
24 | name: String,
25 | vicinity: String,
26 | googleAddressId: String,
27 | },
28 | },
29 | paymentMethod: { type: String, required: true },
30 | paymentResult: { id: String, status: String, email_address: String },
31 | itemsPrice: { type: Number, required: true },
32 | shippingPrice: { type: Number, required: true },
33 | taxPrice: { type: Number, required: true },
34 | totalPrice: { type: Number, required: true },
35 | isPaid: { type: Boolean, required: true, default: false },
36 | isDelivered: { type: Boolean, required: true, default: false },
37 | paidAt: { type: Date },
38 | deliveredAt: { type: Date },
39 | },
40 | {
41 | timestamps: true,
42 | }
43 | );
44 |
45 | const Order = mongoose.models.Order || mongoose.model('Order', orderSchema);
46 | export default Order;
47 |
--------------------------------------------------------------------------------
/models/Product.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const reviewSchema = new mongoose.Schema(
4 | {
5 | user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
6 | name: { type: String, required: true },
7 | rating: { type: Number, default: 0 },
8 | comment: { type: String, required: true },
9 | },
10 | {
11 | timestamps: true,
12 | }
13 | );
14 |
15 | const productSchema = new mongoose.Schema(
16 | {
17 | name: { type: String, required: true },
18 | slug: { type: String, required: true, unique: true },
19 | category: { type: String, required: true },
20 | image: { type: String, required: true },
21 | price: { type: Number, required: true },
22 | brand: { type: String, required: true },
23 | rating: { type: Number, required: true, default: 0 },
24 | numReviews: { type: Number, required: true, default: 0 },
25 | countInStock: { type: Number, required: true, default: 0 },
26 | description: { type: String, required: true },
27 | reviews: [reviewSchema],
28 | featuredImage: { type: String },
29 | isFeatured: { type: Boolean, required: true, default: false },
30 | },
31 | {
32 | timestamps: true,
33 | }
34 | );
35 |
36 | const Product =
37 | mongoose.models.Product || mongoose.model('Product', productSchema);
38 | export default Product;
39 |
--------------------------------------------------------------------------------
/models/User.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const userSchema = new mongoose.Schema(
4 | {
5 | name: { type: String, required: true },
6 | email: { type: String, required: true, unique: true },
7 | password: { type: String, required: true },
8 | isAdmin: { type: Boolean, required: true, default: false },
9 | },
10 | {
11 | timestamps: true,
12 | }
13 | );
14 |
15 | const User = mongoose.models.User || mongoose.model('User', userSchema);
16 | export default User;
17 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true,
3 | images: { domains: ['res.cloudinary.com'] },
4 | };
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-amazona",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@material-ui/core": "^4.11.4",
13 | "@material-ui/icons": "^4.11.2",
14 | "@material-ui/lab": "^4.0.0-alpha.60",
15 | "@paypal/react-paypal-js": "^7.2.0",
16 | "@react-google-maps/api": "^2.2.0",
17 | "axios": "^0.21.1",
18 | "bcryptjs": "^2.4.3",
19 | "chart.js": "^3.4.1",
20 | "cloudinary": "^1.26.2",
21 | "js-cookie": "^2.2.1",
22 | "jsonwebtoken": "^8.5.1",
23 | "mongoose": "^5.13.2",
24 | "multer": "^1.4.2",
25 | "next": "11.0.1",
26 | "next-connect": "^0.10.1",
27 | "notistack": "^1.0.9",
28 | "react": "17.0.2",
29 | "react-chartjs-2": "^3.0.3",
30 | "react-dom": "17.0.2",
31 | "react-hook-form": "^7.11.0",
32 | "react-material-ui-carousel": "^2.2.7",
33 | "streamifier": "^0.1.1"
34 | },
35 | "devDependencies": {
36 | "eslint": "7.29.0",
37 | "eslint-config-next": "11.0.1"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import { PayPalScriptProvider } from '@paypal/react-paypal-js';
2 | import { SnackbarProvider } from 'notistack';
3 | import { useEffect } from 'react';
4 | import '../styles/globals.css';
5 | import { StoreProvider } from '../utils/Store';
6 |
7 | function MyApp({ Component, pageProps }) {
8 | useEffect(() => {
9 | const jssStyles = document.querySelector('#jss-server-side');
10 | if (jssStyles) {
11 | jssStyles.parentElement.removeChild(jssStyles);
12 | }
13 | }, []);
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default MyApp;
26 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import { ServerStyleSheets } from '@material-ui/core/styles';
2 | import Document, { Head, Html, Main, NextScript } from 'next/document';
3 | import React from 'react';
4 |
5 | export default class MyDocument extends Document {
6 | render() {
7 | return (
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 | }
23 |
24 | MyDocument.getInitialProps = async (ctx) => {
25 | const sheets = new ServerStyleSheets();
26 | const originalRenderPage = ctx.renderPage;
27 | ctx.renderPage = () => {
28 | return originalRenderPage({
29 | enhanceApp: (App) => (props) => sheets.collect(),
30 | });
31 | };
32 | const initialProps = await Document.getInitialProps(ctx);
33 | return {
34 | ...initialProps,
35 | styles: [
36 | ...React.Children.toArray(initialProps.styles),
37 | sheets.getStyleElement(),
38 | ],
39 | };
40 | };
41 |
--------------------------------------------------------------------------------
/pages/admin/dashboard.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import dynamic from 'next/dynamic';
3 | import { useRouter } from 'next/router';
4 | import NextLink from 'next/link';
5 | import React, { useEffect, useContext, useReducer } from 'react';
6 | import {
7 | CircularProgress,
8 | Grid,
9 | List,
10 | ListItem,
11 | Typography,
12 | Card,
13 | Button,
14 | ListItemText,
15 | CardContent,
16 | CardActions,
17 | } from '@material-ui/core';
18 | import { Bar } from 'react-chartjs-2';
19 | import { getError } from '../../utils/error';
20 | import { Store } from '../../utils/Store';
21 | import Layout from '../../components/Layout';
22 | import useStyles from '../../utils/styles';
23 |
24 | function reducer(state, action) {
25 | switch (action.type) {
26 | case 'FETCH_REQUEST':
27 | return { ...state, loading: true, error: '' };
28 | case 'FETCH_SUCCESS':
29 | return { ...state, loading: false, summary: action.payload, error: '' };
30 | case 'FETCH_FAIL':
31 | return { ...state, loading: false, error: action.payload };
32 | default:
33 | state;
34 | }
35 | }
36 |
37 | function AdminDashboard() {
38 | const { state } = useContext(Store);
39 | const router = useRouter();
40 | const classes = useStyles();
41 | const { userInfo } = state;
42 |
43 | const [{ loading, error, summary }, dispatch] = useReducer(reducer, {
44 | loading: true,
45 | summary: { salesData: [] },
46 | error: '',
47 | });
48 |
49 | useEffect(() => {
50 | if (!userInfo) {
51 | router.push('/login');
52 | }
53 | const fetchData = async () => {
54 | try {
55 | dispatch({ type: 'FETCH_REQUEST' });
56 | const { data } = await axios.get(`/api/admin/summary`, {
57 | headers: { authorization: `Bearer ${userInfo.token}` },
58 | });
59 | dispatch({ type: 'FETCH_SUCCESS', payload: data });
60 | } catch (err) {
61 | dispatch({ type: 'FETCH_FAIL', payload: getError(err) });
62 | }
63 | };
64 | fetchData();
65 | }, []);
66 | return (
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | {loading ? (
100 |
101 | ) : error ? (
102 | {error}
103 | ) : (
104 |
105 |
106 |
107 |
108 |
109 | ${summary.ordersPrice}
110 |
111 | Sales
112 |
113 |
114 |
115 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | {summary.ordersCount}
127 |
128 | Orders
129 |
130 |
131 |
132 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | {summary.productsCount}
144 |
145 | Products
146 |
147 |
148 |
149 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 | {summary.usersCount}
161 |
162 | Users
163 |
164 |
165 |
166 |
169 |
170 |
171 |
172 |
173 |
174 | )}
175 |
176 |
177 |
178 | Sales Chart
179 |
180 |
181 |
182 | x._id),
185 | datasets: [
186 | {
187 | label: 'Sales',
188 | backgroundColor: 'rgba(162, 222, 208, 1)',
189 | data: summary.salesData.map((x) => x.totalSales),
190 | },
191 | ],
192 | }}
193 | options={{
194 | legend: { display: true, position: 'right' },
195 | }}
196 | >
197 |
198 |
199 |
200 |
201 |
202 |
203 | );
204 | }
205 |
206 | export default dynamic(() => Promise.resolve(AdminDashboard), { ssr: false });
207 |
--------------------------------------------------------------------------------
/pages/admin/orders.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import dynamic from 'next/dynamic';
3 | import { useRouter } from 'next/router';
4 | import NextLink from 'next/link';
5 | import React, { useEffect, useContext, useReducer } from 'react';
6 | import {
7 | CircularProgress,
8 | Grid,
9 | List,
10 | ListItem,
11 | Typography,
12 | Card,
13 | Button,
14 | ListItemText,
15 | TableContainer,
16 | Table,
17 | TableHead,
18 | TableRow,
19 | TableCell,
20 | TableBody,
21 | } from '@material-ui/core';
22 | import { getError } from '../../utils/error';
23 | import { Store } from '../../utils/Store';
24 | import Layout from '../../components/Layout';
25 | import useStyles from '../../utils/styles';
26 |
27 | function reducer(state, action) {
28 | switch (action.type) {
29 | case 'FETCH_REQUEST':
30 | return { ...state, loading: true, error: '' };
31 | case 'FETCH_SUCCESS':
32 | return { ...state, loading: false, orders: action.payload, error: '' };
33 | case 'FETCH_FAIL':
34 | return { ...state, loading: false, error: action.payload };
35 | default:
36 | state;
37 | }
38 | }
39 |
40 | function AdminOrders() {
41 | const { state } = useContext(Store);
42 | const router = useRouter();
43 | const classes = useStyles();
44 | const { userInfo } = state;
45 |
46 | const [{ loading, error, orders }, dispatch] = useReducer(reducer, {
47 | loading: true,
48 | orders: [],
49 | error: '',
50 | });
51 |
52 | useEffect(() => {
53 | if (!userInfo) {
54 | router.push('/login');
55 | }
56 | const fetchData = async () => {
57 | try {
58 | dispatch({ type: 'FETCH_REQUEST' });
59 | const { data } = await axios.get(`/api/admin/orders`, {
60 | headers: { authorization: `Bearer ${userInfo.token}` },
61 | });
62 | dispatch({ type: 'FETCH_SUCCESS', payload: data });
63 | } catch (err) {
64 | dispatch({ type: 'FETCH_FAIL', payload: getError(err) });
65 | }
66 | };
67 | fetchData();
68 | }, []);
69 | return (
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | Orders
104 |
105 |
106 |
107 |
108 | {loading ? (
109 |
110 | ) : error ? (
111 | {error}
112 | ) : (
113 |
114 |
115 |
116 |
117 | ID
118 | USER
119 | DATE
120 | TOTAL
121 | PAID
122 | DELIVERED
123 | ACTION
124 |
125 |
126 |
127 | {orders.map((order) => (
128 |
129 | {order._id.substring(20, 24)}
130 |
131 | {order.user ? order.user.name : 'DELETED USER'}
132 |
133 | {order.createdAt}
134 | ${order.totalPrice}
135 |
136 | {order.isPaid
137 | ? `paid at ${order.paidAt}`
138 | : 'not paid'}
139 |
140 |
141 | {order.isDelivered
142 | ? `delivered at ${order.deliveredAt}`
143 | : 'not delivered'}
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | ))}
152 |
153 |
154 |
155 | )}
156 |
157 |
158 |
159 |
160 |
161 |
162 | );
163 | }
164 |
165 | export default dynamic(() => Promise.resolve(AdminOrders), { ssr: false });
166 |
--------------------------------------------------------------------------------
/pages/admin/product/[id].js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import dynamic from 'next/dynamic';
3 | import { useRouter } from 'next/router';
4 | import NextLink from 'next/link';
5 | import React, { useEffect, useContext, useReducer, useState } from 'react';
6 | import {
7 | Grid,
8 | List,
9 | ListItem,
10 | Typography,
11 | Card,
12 | Button,
13 | ListItemText,
14 | TextField,
15 | CircularProgress,
16 | FormControlLabel,
17 | Checkbox,
18 | } from '@material-ui/core';
19 | import { getError } from '../../../utils/error';
20 | import { Store } from '../../../utils/Store';
21 | import Layout from '../../../components/Layout';
22 | import useStyles from '../../../utils/styles';
23 | import { Controller, useForm } from 'react-hook-form';
24 | import { useSnackbar } from 'notistack';
25 |
26 | function reducer(state, action) {
27 | switch (action.type) {
28 | case 'FETCH_REQUEST':
29 | return { ...state, loading: true, error: '' };
30 | case 'FETCH_SUCCESS':
31 | return { ...state, loading: false, error: '' };
32 | case 'FETCH_FAIL':
33 | return { ...state, loading: false, error: action.payload };
34 | case 'UPDATE_REQUEST':
35 | return { ...state, loadingUpdate: true, errorUpdate: '' };
36 | case 'UPDATE_SUCCESS':
37 | return { ...state, loadingUpdate: false, errorUpdate: '' };
38 | case 'UPDATE_FAIL':
39 | return { ...state, loadingUpdate: false, errorUpdate: action.payload };
40 | case 'UPLOAD_REQUEST':
41 | return { ...state, loadingUpload: true, errorUpload: '' };
42 | case 'UPLOAD_SUCCESS':
43 | return {
44 | ...state,
45 | loadingUpload: false,
46 | errorUpload: '',
47 | };
48 | case 'UPLOAD_FAIL':
49 | return { ...state, loadingUpload: false, errorUpload: action.payload };
50 |
51 | default:
52 | return state;
53 | }
54 | }
55 |
56 | function ProductEdit({ params }) {
57 | const productId = params.id;
58 | const { state } = useContext(Store);
59 | const [{ loading, error, loadingUpdate, loadingUpload }, dispatch] =
60 | useReducer(reducer, {
61 | loading: true,
62 | error: '',
63 | });
64 | const {
65 | handleSubmit,
66 | control,
67 | formState: { errors },
68 | setValue,
69 | } = useForm();
70 | const { enqueueSnackbar, closeSnackbar } = useSnackbar();
71 | const router = useRouter();
72 | const classes = useStyles();
73 | const { userInfo } = state;
74 |
75 | useEffect(() => {
76 | if (!userInfo) {
77 | return router.push('/login');
78 | } else {
79 | const fetchData = async () => {
80 | try {
81 | dispatch({ type: 'FETCH_REQUEST' });
82 | const { data } = await axios.get(`/api/admin/products/${productId}`, {
83 | headers: { authorization: `Bearer ${userInfo.token}` },
84 | });
85 | dispatch({ type: 'FETCH_SUCCESS' });
86 | setValue('name', data.name);
87 | setValue('slug', data.slug);
88 | setValue('price', data.price);
89 | setValue('image', data.image);
90 | setValue('featuredImage', data.featuredImage);
91 | setIsFeatured(data.isFeatured);
92 | setValue('category', data.category);
93 | setValue('brand', data.brand);
94 | setValue('countInStock', data.countInStock);
95 | setValue('description', data.description);
96 | } catch (err) {
97 | dispatch({ type: 'FETCH_FAIL', payload: getError(err) });
98 | }
99 | };
100 | fetchData();
101 | }
102 | }, []);
103 | const uploadHandler = async (e, imageField = 'image') => {
104 | const file = e.target.files[0];
105 | const bodyFormData = new FormData();
106 | bodyFormData.append('file', file);
107 | try {
108 | dispatch({ type: 'UPLOAD_REQUEST' });
109 | const { data } = await axios.post('/api/admin/upload', bodyFormData, {
110 | headers: {
111 | 'Content-Type': 'multipart/form-data',
112 | authorization: `Bearer ${userInfo.token}`,
113 | },
114 | });
115 | dispatch({ type: 'UPLOAD_SUCCESS' });
116 | setValue(imageField, data.secure_url);
117 | enqueueSnackbar('File uploaded successfully', { variant: 'success' });
118 | } catch (err) {
119 | dispatch({ type: 'UPLOAD_FAIL', payload: getError(err) });
120 | enqueueSnackbar(getError(err), { variant: 'error' });
121 | }
122 | };
123 |
124 | const submitHandler = async ({
125 | name,
126 | slug,
127 | price,
128 | category,
129 | image,
130 | featuredImage,
131 | brand,
132 | countInStock,
133 | description,
134 | }) => {
135 | closeSnackbar();
136 | try {
137 | dispatch({ type: 'UPDATE_REQUEST' });
138 | await axios.put(
139 | `/api/admin/products/${productId}`,
140 | {
141 | name,
142 | slug,
143 | price,
144 | category,
145 | image,
146 | isFeatured,
147 | featuredImage,
148 | brand,
149 | countInStock,
150 | description,
151 | },
152 | { headers: { authorization: `Bearer ${userInfo.token}` } }
153 | );
154 | dispatch({ type: 'UPDATE_SUCCESS' });
155 | enqueueSnackbar('Product updated successfully', { variant: 'success' });
156 | router.push('/admin/products');
157 | } catch (err) {
158 | dispatch({ type: 'UPDATE_FAIL', payload: getError(err) });
159 | enqueueSnackbar(getError(err), { variant: 'error' });
160 | }
161 | };
162 |
163 | const [isFeatured, setIsFeatured] = useState(false);
164 |
165 | return (
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 | Edit Product {productId}
200 |
201 |
202 |
203 | {loading && }
204 | {error && (
205 | {error}
206 | )}
207 |
208 |
209 |
460 |
461 |
462 |
463 |
464 |
465 |
466 | );
467 | }
468 |
469 | export async function getServerSideProps({ params }) {
470 | return {
471 | props: { params },
472 | };
473 | }
474 |
475 | export default dynamic(() => Promise.resolve(ProductEdit), { ssr: false });
476 |
--------------------------------------------------------------------------------
/pages/admin/products.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import dynamic from 'next/dynamic';
3 | import { useRouter } from 'next/router';
4 | import NextLink from 'next/link';
5 | import React, { useEffect, useContext, useReducer } from 'react';
6 | import {
7 | CircularProgress,
8 | Grid,
9 | List,
10 | ListItem,
11 | Typography,
12 | Card,
13 | Button,
14 | ListItemText,
15 | TableContainer,
16 | Table,
17 | TableHead,
18 | TableRow,
19 | TableCell,
20 | TableBody,
21 | } from '@material-ui/core';
22 | import { getError } from '../../utils/error';
23 | import { Store } from '../../utils/Store';
24 | import Layout from '../../components/Layout';
25 | import useStyles from '../../utils/styles';
26 | import { useSnackbar } from 'notistack';
27 |
28 | function reducer(state, action) {
29 | switch (action.type) {
30 | case 'FETCH_REQUEST':
31 | return { ...state, loading: true, error: '' };
32 | case 'FETCH_SUCCESS':
33 | return { ...state, loading: false, products: action.payload, error: '' };
34 | case 'FETCH_FAIL':
35 | return { ...state, loading: false, error: action.payload };
36 | case 'CREATE_REQUEST':
37 | return { ...state, loadingCreate: true };
38 | case 'CREATE_SUCCESS':
39 | return { ...state, loadingCreate: false };
40 | case 'CREATE_FAIL':
41 | return { ...state, loadingCreate: false };
42 | case 'DELETE_REQUEST':
43 | return { ...state, loadingDelete: true };
44 | case 'DELETE_SUCCESS':
45 | return { ...state, loadingDelete: false, successDelete: true };
46 | case 'DELETE_FAIL':
47 | return { ...state, loadingDelete: false };
48 | case 'DELETE_RESET':
49 | return { ...state, loadingDelete: false, successDelete: false };
50 | default:
51 | state;
52 | }
53 | }
54 |
55 | function AdminProdcuts() {
56 | const { state } = useContext(Store);
57 | const router = useRouter();
58 | const classes = useStyles();
59 | const { userInfo } = state;
60 |
61 | const [
62 | { loading, error, products, loadingCreate, successDelete, loadingDelete },
63 | dispatch,
64 | ] = useReducer(reducer, {
65 | loading: true,
66 | products: [],
67 | error: '',
68 | });
69 |
70 | useEffect(() => {
71 | if (!userInfo) {
72 | router.push('/login');
73 | }
74 | const fetchData = async () => {
75 | try {
76 | dispatch({ type: 'FETCH_REQUEST' });
77 | const { data } = await axios.get(`/api/admin/products`, {
78 | headers: { authorization: `Bearer ${userInfo.token}` },
79 | });
80 | dispatch({ type: 'FETCH_SUCCESS', payload: data });
81 | } catch (err) {
82 | dispatch({ type: 'FETCH_FAIL', payload: getError(err) });
83 | }
84 | };
85 | if (successDelete) {
86 | dispatch({ type: 'DELETE_RESET' });
87 | } else {
88 | fetchData();
89 | }
90 | }, [successDelete]);
91 |
92 | const { enqueueSnackbar } = useSnackbar();
93 | const createHandler = async () => {
94 | if (!window.confirm('Are you sure?')) {
95 | return;
96 | }
97 | try {
98 | dispatch({ type: 'CREATE_REQUEST' });
99 | const { data } = await axios.post(
100 | `/api/admin/products`,
101 | {},
102 | {
103 | headers: { authorization: `Bearer ${userInfo.token}` },
104 | }
105 | );
106 | dispatch({ type: 'CREATE_SUCCESS' });
107 | enqueueSnackbar('Product created successfully', { variant: 'success' });
108 | router.push(`/admin/product/${data.product._id}`);
109 | } catch (err) {
110 | dispatch({ type: 'CREATE_FAIL' });
111 | enqueueSnackbar(getError(err), { variant: 'error' });
112 | }
113 | };
114 | const deleteHandler = async (productId) => {
115 | if (!window.confirm('Are you sure?')) {
116 | return;
117 | }
118 | try {
119 | dispatch({ type: 'DELETE_REQUEST' });
120 | await axios.delete(`/api/admin/products/${productId}`, {
121 | headers: { authorization: `Bearer ${userInfo.token}` },
122 | });
123 | dispatch({ type: 'DELETE_SUCCESS' });
124 | enqueueSnackbar('Product deleted successfully', { variant: 'success' });
125 | } catch (err) {
126 | dispatch({ type: 'DELETE_FAIL' });
127 | enqueueSnackbar(getError(err), { variant: 'error' });
128 | }
129 | };
130 | return (
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | Products
167 |
168 | {loadingDelete && }
169 |
170 |
171 |
178 | {loadingCreate && }
179 |
180 |
181 |
182 |
183 |
184 | {loading ? (
185 |
186 | ) : error ? (
187 | {error}
188 | ) : (
189 |
190 |
191 |
192 |
193 | ID
194 | NAME
195 | PRICE
196 | CATEGORY
197 | COUNT
198 | RATING
199 | ACTIONS
200 |
201 |
202 |
203 | {products.map((product) => (
204 |
205 |
206 | {product._id.substring(20, 24)}
207 |
208 | {product.name}
209 | ${product.price}
210 | {product.category}
211 | {product.countInStock}
212 | {product.rating}
213 |
214 |
218 |
221 | {' '}
222 |
229 |
230 |
231 | ))}
232 |
233 |
234 |
235 | )}
236 |
237 |
238 |
239 |
240 |
241 |
242 | );
243 | }
244 |
245 | export default dynamic(() => Promise.resolve(AdminProdcuts), { ssr: false });
246 |
--------------------------------------------------------------------------------
/pages/admin/user/[id].js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import dynamic from 'next/dynamic';
3 | import { useRouter } from 'next/router';
4 | import NextLink from 'next/link';
5 | import React, { useEffect, useContext, useReducer, useState } from 'react';
6 | import {
7 | Grid,
8 | List,
9 | ListItem,
10 | Typography,
11 | Card,
12 | Button,
13 | ListItemText,
14 | TextField,
15 | CircularProgress,
16 | Checkbox,
17 | FormControlLabel,
18 | } from '@material-ui/core';
19 | import { getError } from '../../../utils/error';
20 | import { Store } from '../../../utils/Store';
21 | import Layout from '../../../components/Layout';
22 | import useStyles from '../../../utils/styles';
23 | import { Controller, useForm } from 'react-hook-form';
24 | import { useSnackbar } from 'notistack';
25 |
26 | function reducer(state, action) {
27 | switch (action.type) {
28 | case 'FETCH_REQUEST':
29 | return { ...state, loading: true, error: '' };
30 | case 'FETCH_SUCCESS':
31 | return { ...state, loading: false, error: '' };
32 | case 'FETCH_FAIL':
33 | return { ...state, loading: false, error: action.payload };
34 | case 'UPDATE_REQUEST':
35 | return { ...state, loadingUpdate: true, errorUpdate: '' };
36 | case 'UPDATE_SUCCESS':
37 | return { ...state, loadingUpdate: false, errorUpdate: '' };
38 | case 'UPDATE_FAIL':
39 | return { ...state, loadingUpdate: false, errorUpdate: action.payload };
40 | case 'UPLOAD_REQUEST':
41 | return { ...state, loadingUpload: true, errorUpload: '' };
42 | case 'UPLOAD_SUCCESS':
43 | return {
44 | ...state,
45 | loadingUpload: false,
46 | errorUpload: '',
47 | };
48 | case 'UPLOAD_FAIL':
49 | return { ...state, loadingUpload: false, errorUpload: action.payload };
50 |
51 | default:
52 | return state;
53 | }
54 | }
55 |
56 | function UserEdit({ params }) {
57 | const userId = params.id;
58 | const { state } = useContext(Store);
59 | const [{ loading, error, loadingUpdate }, dispatch] = useReducer(reducer, {
60 | loading: true,
61 | error: '',
62 | });
63 | const {
64 | handleSubmit,
65 | control,
66 | formState: { errors },
67 | setValue,
68 | } = useForm();
69 | const [isAdmin, setIsAdmin] = useState(false);
70 | const { enqueueSnackbar, closeSnackbar } = useSnackbar();
71 | const router = useRouter();
72 | const classes = useStyles();
73 | const { userInfo } = state;
74 |
75 | useEffect(() => {
76 | if (!userInfo) {
77 | return router.push('/login');
78 | } else {
79 | const fetchData = async () => {
80 | try {
81 | dispatch({ type: 'FETCH_REQUEST' });
82 | const { data } = await axios.get(`/api/admin/users/${userId}`, {
83 | headers: { authorization: `Bearer ${userInfo.token}` },
84 | });
85 | setIsAdmin(data.isAdmin);
86 | dispatch({ type: 'FETCH_SUCCESS' });
87 | setValue('name', data.name);
88 | } catch (err) {
89 | dispatch({ type: 'FETCH_FAIL', payload: getError(err) });
90 | }
91 | };
92 | fetchData();
93 | }
94 | }, []);
95 |
96 | const submitHandler = async ({ name }) => {
97 | closeSnackbar();
98 | try {
99 | dispatch({ type: 'UPDATE_REQUEST' });
100 | await axios.put(
101 | `/api/admin/users/${userId}`,
102 | {
103 | name,
104 | isAdmin,
105 | },
106 | { headers: { authorization: `Bearer ${userInfo.token}` } }
107 | );
108 | dispatch({ type: 'UPDATE_SUCCESS' });
109 | enqueueSnackbar('User updated successfully', { variant: 'success' });
110 | router.push('/admin/users');
111 | } catch (err) {
112 | dispatch({ type: 'UPDATE_FAIL', payload: getError(err) });
113 | enqueueSnackbar(getError(err), { variant: 'error' });
114 | }
115 | };
116 | return (
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | Edit User {userId}
151 |
152 |
153 |
154 | {loading && }
155 | {error && (
156 | {error}
157 | )}
158 |
159 |
160 |
211 |
212 |
213 |
214 |
215 |
216 |
217 | );
218 | }
219 |
220 | export async function getServerSideProps({ params }) {
221 | return {
222 | props: { params },
223 | };
224 | }
225 |
226 | export default dynamic(() => Promise.resolve(UserEdit), { ssr: false });
227 |
--------------------------------------------------------------------------------
/pages/admin/users.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import dynamic from 'next/dynamic';
3 | import { useRouter } from 'next/router';
4 | import NextLink from 'next/link';
5 | import React, { useEffect, useContext, useReducer } from 'react';
6 | import {
7 | CircularProgress,
8 | Grid,
9 | List,
10 | ListItem,
11 | Typography,
12 | Card,
13 | Button,
14 | ListItemText,
15 | TableContainer,
16 | Table,
17 | TableHead,
18 | TableRow,
19 | TableCell,
20 | TableBody,
21 | } from '@material-ui/core';
22 | import { getError } from '../../utils/error';
23 | import { Store } from '../../utils/Store';
24 | import Layout from '../../components/Layout';
25 | import useStyles from '../../utils/styles';
26 | import { useSnackbar } from 'notistack';
27 |
28 | function reducer(state, action) {
29 | switch (action.type) {
30 | case 'FETCH_REQUEST':
31 | return { ...state, loading: true, error: '' };
32 | case 'FETCH_SUCCESS':
33 | return { ...state, loading: false, users: action.payload, error: '' };
34 | case 'FETCH_FAIL':
35 | return { ...state, loading: false, error: action.payload };
36 |
37 | case 'DELETE_REQUEST':
38 | return { ...state, loadingDelete: true };
39 | case 'DELETE_SUCCESS':
40 | return { ...state, loadingDelete: false, successDelete: true };
41 | case 'DELETE_FAIL':
42 | return { ...state, loadingDelete: false };
43 | case 'DELETE_RESET':
44 | return { ...state, loadingDelete: false, successDelete: false };
45 | default:
46 | state;
47 | }
48 | }
49 |
50 | function AdminUsers() {
51 | const { state } = useContext(Store);
52 | const router = useRouter();
53 | const classes = useStyles();
54 | const { userInfo } = state;
55 |
56 | const [{ loading, error, users, successDelete, loadingDelete }, dispatch] =
57 | useReducer(reducer, {
58 | loading: true,
59 | users: [],
60 | error: '',
61 | });
62 |
63 | useEffect(() => {
64 | if (!userInfo) {
65 | router.push('/login');
66 | }
67 | const fetchData = async () => {
68 | try {
69 | dispatch({ type: 'FETCH_REQUEST' });
70 | const { data } = await axios.get(`/api/admin/users`, {
71 | headers: { authorization: `Bearer ${userInfo.token}` },
72 | });
73 | dispatch({ type: 'FETCH_SUCCESS', payload: data });
74 | } catch (err) {
75 | dispatch({ type: 'FETCH_FAIL', payload: getError(err) });
76 | }
77 | };
78 | if (successDelete) {
79 | dispatch({ type: 'DELETE_RESET' });
80 | } else {
81 | fetchData();
82 | }
83 | }, [successDelete]);
84 |
85 | const { enqueueSnackbar } = useSnackbar();
86 |
87 | const deleteHandler = async (userId) => {
88 | if (!window.confirm('Are you sure?')) {
89 | return;
90 | }
91 | try {
92 | dispatch({ type: 'DELETE_REQUEST' });
93 | await axios.delete(`/api/admin/users/${userId}`, {
94 | headers: { authorization: `Bearer ${userInfo.token}` },
95 | });
96 | dispatch({ type: 'DELETE_SUCCESS' });
97 | enqueueSnackbar('User deleted successfully', { variant: 'success' });
98 | } catch (err) {
99 | dispatch({ type: 'DELETE_FAIL' });
100 | enqueueSnackbar(getError(err), { variant: 'error' });
101 | }
102 | };
103 | return (
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | Users
138 |
139 | {loadingDelete && }
140 |
141 |
142 |
143 | {loading ? (
144 |
145 | ) : error ? (
146 | {error}
147 | ) : (
148 |
149 |
150 |
151 |
152 | ID
153 | NAME
154 | EMAIL
155 | ISADMIN
156 | ACTIONS
157 |
158 |
159 |
160 | {users.map((user) => (
161 |
162 | {user._id.substring(20, 24)}
163 | {user.name}
164 | {user.email}
165 | {user.isAdmin ? 'YES' : 'NO'}
166 |
167 |
171 |
174 | {' '}
175 |
182 |
183 |
184 | ))}
185 |
186 |
187 |
188 | )}
189 |
190 |
191 |
192 |
193 |
194 |
195 | );
196 | }
197 |
198 | export default dynamic(() => Promise.resolve(AdminUsers), { ssr: false });
199 |
--------------------------------------------------------------------------------
/pages/api/admin/orders.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import Order from '../../../models/Order';
3 | import { isAuth, isAdmin } from '../../../utils/auth';
4 | import db from '../../../utils/db';
5 | import { onError } from '../../../utils/error';
6 |
7 | const handler = nc({
8 | onError,
9 | });
10 | handler.use(isAuth, isAdmin);
11 |
12 | handler.get(async (req, res) => {
13 | await db.connect();
14 | const orders = await Order.find({}).populate('user', 'name');
15 | await db.disconnect();
16 | res.send(orders);
17 | });
18 |
19 | export default handler;
20 |
--------------------------------------------------------------------------------
/pages/api/admin/products/[id]/index.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import { isAdmin, isAuth } from '../../../../../utils/auth';
3 | import Product from '../../../../../models/Product';
4 | import db from '../../../../../utils/db';
5 |
6 | const handler = nc();
7 | handler.use(isAuth, isAdmin);
8 |
9 | handler.get(async (req, res) => {
10 | await db.connect();
11 | const product = await Product.findById(req.query.id);
12 | await db.disconnect();
13 | res.send(product);
14 | });
15 |
16 | handler.put(async (req, res) => {
17 | await db.connect();
18 | const product = await Product.findById(req.query.id);
19 | if (product) {
20 | product.name = req.body.name;
21 | product.slug = req.body.slug;
22 | product.price = req.body.price;
23 | product.category = req.body.category;
24 | product.image = req.body.image;
25 | product.featuredImage = req.body.featuredImage;
26 | product.isFeatured = req.body.isFeatured;
27 | product.brand = req.body.brand;
28 | product.countInStock = req.body.countInStock;
29 | product.description = req.body.description;
30 | await product.save();
31 | await db.disconnect();
32 | res.send({ message: 'Product Updated Successfully' });
33 | } else {
34 | await db.disconnect();
35 | res.status(404).send({ message: 'Product Not Found' });
36 | }
37 | });
38 |
39 | handler.delete(async (req, res) => {
40 | await db.connect();
41 | const product = await Product.findById(req.query.id);
42 | if (product) {
43 | await product.remove();
44 | await db.disconnect();
45 | res.send({ message: 'Product Deleted' });
46 | } else {
47 | await db.disconnect();
48 | res.status(404).send({ message: 'Product Not Found' });
49 | }
50 | });
51 |
52 | export default handler;
53 |
--------------------------------------------------------------------------------
/pages/api/admin/products/index.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import { isAdmin, isAuth } from '../../../../utils/auth';
3 | import Product from '../../../../models/Product';
4 | import db from '../../../../utils/db';
5 |
6 | const handler = nc();
7 | handler.use(isAuth, isAdmin);
8 |
9 | handler.get(async (req, res) => {
10 | await db.connect();
11 | const products = await Product.find({});
12 | await db.disconnect();
13 | res.send(products);
14 | });
15 |
16 | handler.post(async (req, res) => {
17 | await db.connect();
18 | const newProduct = new Product({
19 | name: 'sample name',
20 | slug: 'sample-slug-' + Math.random(),
21 | image: '/images/shirt1.jpg',
22 | price: 0,
23 | category: 'sample category',
24 | brand: 'sample brand',
25 | countInStock: 0,
26 | description: 'sample description',
27 | rating: 0,
28 | numReviews: 0,
29 | });
30 |
31 | const product = await newProduct.save();
32 | await db.disconnect();
33 | res.send({ message: 'Product Created', product });
34 | });
35 |
36 | export default handler;
37 |
--------------------------------------------------------------------------------
/pages/api/admin/summary.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import Order from '../../../models/Order';
3 | import Product from '../../../models/Product';
4 | import User from '../../../models/User';
5 | import { isAuth, isAdmin } from '../../../utils/auth';
6 | import db from '../../../utils/db';
7 | import { onError } from '../../../utils/error';
8 |
9 | const handler = nc({
10 | onError,
11 | });
12 | handler.use(isAuth, isAdmin);
13 |
14 | handler.get(async (req, res) => {
15 | await db.connect();
16 | const ordersCount = await Order.countDocuments();
17 | const productsCount = await Product.countDocuments();
18 | const usersCount = await User.countDocuments();
19 | const ordersPriceGroup = await Order.aggregate([
20 | {
21 | $group: {
22 | _id: null,
23 | sales: { $sum: '$totalPrice' },
24 | },
25 | },
26 | ]);
27 | const ordersPrice =
28 | ordersPriceGroup.length > 0 ? ordersPriceGroup[0].sales : 0;
29 | const salesData = await Order.aggregate([
30 | {
31 | $group: {
32 | _id: { $dateToString: { format: '%Y-%m', date: '$createdAt' } },
33 | totalSales: { $sum: '$totalPrice' },
34 | },
35 | },
36 | ]);
37 | await db.disconnect();
38 | res.send({ ordersCount, productsCount, usersCount, ordersPrice, salesData });
39 | });
40 |
41 | export default handler;
42 |
--------------------------------------------------------------------------------
/pages/api/admin/upload.js:
--------------------------------------------------------------------------------
1 | import nextConnect from 'next-connect';
2 | import { isAuth, isAdmin } from '../../../utils/auth';
3 | import { onError } from '../../../utils/error';
4 | import multer from 'multer';
5 | import { v2 as cloudinary } from 'cloudinary';
6 | import streamifier from 'streamifier';
7 |
8 | cloudinary.config({
9 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
10 | api_key: process.env.CLOUDINARY_API_KEY,
11 | api_secret: process.env.CLOUDINARY_API_SECRET,
12 | });
13 |
14 | export const config = {
15 | api: {
16 | bodyParser: false,
17 | },
18 | };
19 |
20 | const handler = nextConnect({ onError });
21 | const upload = multer();
22 |
23 | handler.use(isAuth, isAdmin, upload.single('file')).post(async (req, res) => {
24 | const streamUpload = (req) => {
25 | return new Promise((resolve, reject) => {
26 | const stream = cloudinary.uploader.upload_stream((error, result) => {
27 | if (result) {
28 | resolve(result);
29 | } else {
30 | reject(error);
31 | }
32 | });
33 | streamifier.createReadStream(req.file.buffer).pipe(stream);
34 | });
35 | };
36 | const result = await streamUpload(req);
37 | res.send(result);
38 | });
39 |
40 | export default handler;
41 |
--------------------------------------------------------------------------------
/pages/api/admin/users/[id]/index.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import { isAdmin, isAuth } from '../../../../../utils/auth';
3 | import User from '../../../../../models/User';
4 | import db from '../../../../../utils/db';
5 |
6 | const handler = nc();
7 | handler.use(isAuth, isAdmin);
8 |
9 | handler.get(async (req, res) => {
10 | await db.connect();
11 | const user = await User.findById(req.query.id);
12 | await db.disconnect();
13 | res.send(user);
14 | });
15 |
16 | handler.put(async (req, res) => {
17 | await db.connect();
18 | const user = await User.findById(req.query.id);
19 | if (user) {
20 | user.name = req.body.name;
21 | user.isAdmin = Boolean(req.body.isAdmin);
22 | await user.save();
23 | await db.disconnect();
24 | res.send({ message: 'User Updated Successfully' });
25 | } else {
26 | await db.disconnect();
27 | res.status(404).send({ message: 'User Not Found' });
28 | }
29 | });
30 |
31 | handler.delete(async (req, res) => {
32 | await db.connect();
33 | const user = await User.findById(req.query.id);
34 | if (user) {
35 | await user.remove();
36 | await db.disconnect();
37 | res.send({ message: 'User Deleted' });
38 | } else {
39 | await db.disconnect();
40 | res.status(404).send({ message: 'User Not Found' });
41 | }
42 | });
43 |
44 | export default handler;
45 |
--------------------------------------------------------------------------------
/pages/api/admin/users/index.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import { isAdmin, isAuth } from '../../../../utils/auth';
3 | import User from '../../../../models/User';
4 | import db from '../../../../utils/db';
5 |
6 | const handler = nc();
7 | handler.use(isAuth, isAdmin);
8 |
9 | handler.get(async (req, res) => {
10 | await db.connect();
11 | const users = await User.find({});
12 | await db.disconnect();
13 | res.send(users);
14 | });
15 |
16 | export default handler;
17 |
--------------------------------------------------------------------------------
/pages/api/hello.js:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 |
3 | //import db from '../../utils/db';
4 |
5 | export default async function handler(req, res) {
6 | //await db.connect();
7 | //await db.disconnect();
8 | res.status(200).json({ name: 'John Doe' });
9 | }
10 |
--------------------------------------------------------------------------------
/pages/api/keys/google.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import { isAuth } from '../../../utils/auth';
3 |
4 | const handler = nc();
5 | handler.use(isAuth);
6 | handler.get(async (req, res) => {
7 | res.send(process.env.GOOGLE_API_KEY || 'nokey');
8 | });
9 |
10 | export default handler;
11 |
--------------------------------------------------------------------------------
/pages/api/keys/paypal.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import { isAuth } from '../../../utils/auth';
3 |
4 | const handler = nc();
5 | handler.use(isAuth);
6 | handler.get(async (req, res) => {
7 | res.send(process.env.PAYPAL_CLIENT_ID || 'sb');
8 | });
9 |
10 | export default handler;
11 |
--------------------------------------------------------------------------------
/pages/api/orders/[id]/deliver.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import Order from '../../../../models/Order';
3 | import db from '../../../../utils/db';
4 | import onError from '../../../../utils/error';
5 | import { isAuth } from '../../../../utils/auth';
6 |
7 | const handler = nc({
8 | onError,
9 | });
10 | handler.use(isAuth);
11 | handler.put(async (req, res) => {
12 | await db.connect();
13 | const order = await Order.findById(req.query.id);
14 | if (order) {
15 | order.isDelivered = true;
16 | order.deliveredAt = Date.now();
17 | const deliveredOrder = await order.save();
18 | await db.disconnect();
19 | res.send({ message: 'order delivered', order: deliveredOrder });
20 | } else {
21 | await db.disconnect();
22 | res.status(404).send({ message: 'order not found' });
23 | }
24 | });
25 |
26 | export default handler;
27 |
--------------------------------------------------------------------------------
/pages/api/orders/[id]/index.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import Order from '../../../../models/Order';
3 | import db from '../../../../utils/db';
4 | import { isAuth } from '../../../../utils/auth';
5 |
6 | const handler = nc();
7 | handler.use(isAuth);
8 | handler.get(async (req, res) => {
9 | await db.connect();
10 | const order = await Order.findById(req.query.id);
11 | await db.disconnect();
12 | res.send(order);
13 | });
14 |
15 | export default handler;
16 |
--------------------------------------------------------------------------------
/pages/api/orders/[id]/pay.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import Order from '../../../../models/Order';
3 | import db from '../../../../utils/db';
4 | import onError from '../../../../utils/error';
5 | import { isAuth } from '../../../../utils/auth';
6 |
7 | const handler = nc({
8 | onError,
9 | });
10 | handler.use(isAuth);
11 | handler.put(async (req, res) => {
12 | await db.connect();
13 | const order = await Order.findById(req.query.id);
14 | if (order) {
15 | order.isPaid = true;
16 | order.paidAt = Date.now();
17 | order.paymentResult = {
18 | id: req.body.id,
19 | status: req.body.status,
20 | email_address: req.body.email_address,
21 | };
22 | const paidOrder = await order.save();
23 | await db.disconnect();
24 | res.send({ message: 'order paid', order: paidOrder });
25 | } else {
26 | await db.disconnect();
27 | res.status(404).send({ message: 'order not found' });
28 | }
29 | });
30 |
31 | export default handler;
32 |
--------------------------------------------------------------------------------
/pages/api/orders/history.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import Order from '../../../models/Order';
3 | import { isAuth } from '../../../utils/auth';
4 | import db from '../../../utils/db';
5 | import { onError } from '../../../utils/error';
6 |
7 | const handler = nc({
8 | onError,
9 | });
10 | handler.use(isAuth);
11 |
12 | handler.get(async (req, res) => {
13 | await db.connect();
14 | const orders = await Order.find({ user: req.user._id });
15 | res.send(orders);
16 | });
17 |
18 | export default handler;
19 |
--------------------------------------------------------------------------------
/pages/api/orders/index.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import Order from '../../../models/Order';
3 | import { isAuth } from '../../../utils/auth';
4 | import db from '../../../utils/db';
5 | import { onError } from '../../../utils/error';
6 |
7 | const handler = nc({
8 | onError,
9 | });
10 | handler.use(isAuth);
11 |
12 | handler.post(async (req, res) => {
13 | await db.connect();
14 | const newOrder = new Order({
15 | ...req.body,
16 | user: req.user._id,
17 | });
18 | const order = await newOrder.save();
19 | res.status(201).send(order);
20 | });
21 |
22 | export default handler;
23 |
--------------------------------------------------------------------------------
/pages/api/products/[id]/index.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import Product from '../../../../models/Product';
3 | import db from '../../../../utils/db';
4 |
5 | const handler = nc();
6 |
7 | handler.get(async (req, res) => {
8 | await db.connect();
9 | const product = await Product.findById(req.query.id);
10 | await db.disconnect();
11 | res.send(product);
12 | });
13 |
14 | export default handler;
15 |
--------------------------------------------------------------------------------
/pages/api/products/[id]/reviews.js:
--------------------------------------------------------------------------------
1 | // /api/products/:id/reviews
2 | import mongoose from 'mongoose';
3 | import nextConnect from 'next-connect';
4 | import { onError } from '../../../../utils/error';
5 | import db from '../../../../utils/db';
6 | import Product from '../../../../models/Product';
7 | import { isAuth } from '../../../../utils/auth';
8 |
9 | const handler = nextConnect({
10 | onError,
11 | });
12 |
13 | handler.get(async (req, res) => {
14 | db.connect();
15 | const product = await Product.findById(req.query.id);
16 | db.disconnect();
17 | if (product) {
18 | res.send(product.reviews);
19 | } else {
20 | res.status(404).send({ message: 'Product not found' });
21 | }
22 | });
23 |
24 | handler.use(isAuth).post(async (req, res) => {
25 | await db.connect();
26 | const product = await Product.findById(req.query.id);
27 | if (product) {
28 | const existReview = product.reviews.find((x) => x.user == req.user._id);
29 | if (existReview) {
30 | await Product.updateOne(
31 | { _id: req.query.id, 'reviews._id': existReview._id },
32 | {
33 | $set: {
34 | 'reviews.$.comment': req.body.comment,
35 | 'reviews.$.rating': Number(req.body.rating),
36 | },
37 | }
38 | );
39 |
40 | const updatedProduct = await Product.findById(req.query.id);
41 | updatedProduct.numReviews = updatedProduct.reviews.length;
42 | updatedProduct.rating =
43 | updatedProduct.reviews.reduce((a, c) => c.rating + a, 0) /
44 | updatedProduct.reviews.length;
45 | await updatedProduct.save();
46 |
47 | await db.disconnect();
48 | return res.send({ message: 'Review updated' });
49 | } else {
50 | const review = {
51 | user: mongoose.Types.ObjectId(req.user._id),
52 | name: req.user.name,
53 | rating: Number(req.body.rating),
54 | comment: req.body.comment,
55 | };
56 | product.reviews.push(review);
57 | product.numReviews = product.reviews.length;
58 | product.rating =
59 | product.reviews.reduce((a, c) => c.rating + a, 0) /
60 | product.reviews.length;
61 | await product.save();
62 | await db.disconnect();
63 | res.status(201).send({
64 | message: 'Review submitted',
65 | });
66 | }
67 | } else {
68 | await db.disconnect();
69 | res.status(404).send({ message: 'Product Not Found' });
70 | }
71 | });
72 |
73 | export default handler;
74 |
--------------------------------------------------------------------------------
/pages/api/products/categories.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import Product from '../../../models/Product';
3 | import db from '../../../utils/db';
4 |
5 | const handler = nc();
6 |
7 | handler.get(async (req, res) => {
8 | await db.connect();
9 | const categories = await Product.find().distinct('category');
10 | await db.disconnect();
11 | res.send(categories);
12 | });
13 |
14 | export default handler;
15 |
--------------------------------------------------------------------------------
/pages/api/products/index.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import Product from '../../../models/Product';
3 | import db from '../../../utils/db';
4 |
5 | const handler = nc();
6 |
7 | handler.get(async (req, res) => {
8 | await db.connect();
9 | const products = await Product.find({});
10 | await db.disconnect();
11 | res.send(products);
12 | });
13 |
14 | export default handler;
15 |
--------------------------------------------------------------------------------
/pages/api/seed.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | // import Product from '../../models/Product';
3 | // import db from '../../utils/db';
4 | // import data from '../../utils/data';
5 | // import User from '../../models/User';
6 |
7 | const handler = nc();
8 |
9 | handler.get(async (req, res) => {
10 | return res.send({ message: 'already seeded' });
11 | // await db.connect();
12 | // await User.deleteMany();
13 | // await User.insertMany(data.users);
14 | // await Product.deleteMany();
15 | // await Product.insertMany(data.products);
16 | // await db.disconnect();
17 | // res.send({ message: 'seeded successfully' });
18 | });
19 |
20 | export default handler;
21 |
--------------------------------------------------------------------------------
/pages/api/users/login.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import bcrypt from 'bcryptjs';
3 | import User from '../../../models/User';
4 | import db from '../../../utils/db';
5 | import { signToken } from '../../../utils/auth';
6 |
7 | const handler = nc();
8 |
9 | handler.post(async (req, res) => {
10 | await db.connect();
11 | const user = await User.findOne({ email: req.body.email });
12 | await db.disconnect();
13 | if (user && bcrypt.compareSync(req.body.password, user.password)) {
14 | const token = signToken(user);
15 | res.send({
16 | token,
17 | _id: user._id,
18 | name: user.name,
19 | email: user.email,
20 | isAdmin: user.isAdmin,
21 | });
22 | } else {
23 | res.status(401).send({ message: 'Invalid email or password' });
24 | }
25 | });
26 |
27 | export default handler;
28 |
--------------------------------------------------------------------------------
/pages/api/users/profile.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import bcrypt from 'bcryptjs';
3 | import User from '../../../models/User';
4 | import db from '../../../utils/db';
5 | import { signToken, isAuth } from '../../../utils/auth';
6 |
7 | const handler = nc();
8 | handler.use(isAuth);
9 |
10 | handler.put(async (req, res) => {
11 | await db.connect();
12 | const user = await User.findById(req.user._id);
13 | user.name = req.body.name;
14 | user.email = req.body.email;
15 | user.password = req.body.password
16 | ? bcrypt.hashSync(req.body.password)
17 | : user.password;
18 | await user.save();
19 | await db.disconnect();
20 |
21 | const token = signToken(user);
22 | res.send({
23 | token,
24 | _id: user._id,
25 | name: user.name,
26 | email: user.email,
27 | isAdmin: user.isAdmin,
28 | });
29 | });
30 |
31 | export default handler;
32 |
--------------------------------------------------------------------------------
/pages/api/users/register.js:
--------------------------------------------------------------------------------
1 | import nc from 'next-connect';
2 | import bcrypt from 'bcryptjs';
3 | import User from '../../../models/User';
4 | import db from '../../../utils/db';
5 | import { signToken } from '../../../utils/auth';
6 |
7 | const handler = nc();
8 |
9 | handler.post(async (req, res) => {
10 | await db.connect();
11 | const newUser = new User({
12 | name: req.body.name,
13 | email: req.body.email,
14 | password: bcrypt.hashSync(req.body.password),
15 | isAdmin: false,
16 | });
17 | const user = await newUser.save();
18 | await db.disconnect();
19 |
20 | const token = signToken(user);
21 | res.send({
22 | token,
23 | _id: user._id,
24 | name: user.name,
25 | email: user.email,
26 | isAdmin: user.isAdmin,
27 | });
28 | });
29 |
30 | export default handler;
31 |
--------------------------------------------------------------------------------
/pages/cart.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import dynamic from 'next/dynamic';
3 | import Layout from '../components/Layout';
4 | import { Store } from '../utils/Store';
5 | import NextLink from 'next/link';
6 | import Image from 'next/image';
7 | import {
8 | Grid,
9 | TableContainer,
10 | Table,
11 | Typography,
12 | TableHead,
13 | TableBody,
14 | TableRow,
15 | TableCell,
16 | Link,
17 | Select,
18 | MenuItem,
19 | Button,
20 | Card,
21 | List,
22 | ListItem,
23 | } from '@material-ui/core';
24 | import axios from 'axios';
25 | import { useRouter } from 'next/router';
26 |
27 | function CartScreen() {
28 | const router = useRouter();
29 | const { state, dispatch } = useContext(Store);
30 | const {
31 | cart: { cartItems },
32 | } = state;
33 | const updateCartHandler = async (item, quantity) => {
34 | const { data } = await axios.get(`/api/products/${item._id}`);
35 | if (data.countInStock < quantity) {
36 | window.alert('Sorry. Product is out of stock');
37 | return;
38 | }
39 | dispatch({ type: 'CART_ADD_ITEM', payload: { ...item, quantity } });
40 | };
41 | const removeItemHandler = (item) => {
42 | dispatch({ type: 'CART_REMOVE_ITEM', payload: item });
43 | };
44 | const checkoutHandler = () => {
45 | router.push('/shipping');
46 | };
47 | return (
48 |
49 |
50 | Shopping Cart
51 |
52 | {cartItems.length === 0 ? (
53 |
54 | Cart is empty.{' '}
55 |
56 | Go shopping
57 |
58 |
59 | ) : (
60 |
61 |
62 |
63 |
64 |
65 |
66 | Image
67 | Name
68 | Quantity
69 | Price
70 | Action
71 |
72 |
73 |
74 | {cartItems.map((item) => (
75 |
76 |
77 |
78 |
79 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | {item.name}
93 |
94 |
95 |
96 |
97 |
109 |
110 | ${item.price}
111 |
112 |
119 |
120 |
121 | ))}
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | Subtotal ({cartItems.reduce((a, c) => a + c.quantity, 0)}{' '}
132 | items) : $
133 | {cartItems.reduce((a, c) => a + c.quantity * c.price, 0)}
134 |
135 |
136 |
137 |
145 |
146 |
147 |
148 |
149 |
150 | )}
151 |
152 | );
153 | }
154 |
155 | export default dynamic(() => Promise.resolve(CartScreen), { ssr: false });
156 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 | import NextLink from 'next/link';
3 | import { Grid, Link, Typography } from '@material-ui/core';
4 | import Layout from '../components/Layout';
5 | import db from '../utils/db';
6 | import Product from '../models/Product';
7 | import axios from 'axios';
8 | import { useRouter } from 'next/router';
9 | import { useContext } from 'react';
10 | import { Store } from '../utils/Store';
11 | import ProductItem from '../components/ProductItem';
12 | import Carousel from 'react-material-ui-carousel';
13 | import useStyles from '../utils/styles';
14 |
15 | export default function Home(props) {
16 | const classes = useStyles();
17 | const router = useRouter();
18 | const { state, dispatch } = useContext(Store);
19 | const { topRatedProducts, featuredProducts } = props;
20 | const addToCartHandler = async (product) => {
21 | const existItem = state.cart.cartItems.find((x) => x._id === product._id);
22 | const quantity = existItem ? existItem.quantity + 1 : 1;
23 | const { data } = await axios.get(`/api/products/${product._id}`);
24 | if (data.countInStock < quantity) {
25 | window.alert('Sorry. Product is out of stock');
26 | return;
27 | }
28 | dispatch({ type: 'CART_ADD_ITEM', payload: { ...product, quantity } });
29 | router.push('/cart');
30 | };
31 | return (
32 |
33 |
34 | {featuredProducts.map((product) => (
35 |
40 |
41 |
46 |
47 |
48 | ))}
49 |
50 | Popular Products
51 |
52 | {topRatedProducts.map((product) => (
53 |
54 |
58 |
59 | ))}
60 |
61 |
62 | );
63 | }
64 |
65 | export async function getServerSideProps() {
66 | await db.connect();
67 | const featuredProductsDocs = await Product.find(
68 | { isFeatured: true },
69 | '-reviews'
70 | )
71 | .lean()
72 | .limit(3);
73 | const topRatedProductsDocs = await Product.find({}, '-reviews')
74 | .lean()
75 | .sort({
76 | rating: -1,
77 | })
78 | .limit(6);
79 | await db.disconnect();
80 | return {
81 | props: {
82 | featuredProducts: featuredProductsDocs.map(db.convertDocToObj),
83 | topRatedProducts: topRatedProductsDocs.map(db.convertDocToObj),
84 | },
85 | };
86 | }
87 |
--------------------------------------------------------------------------------
/pages/login.js:
--------------------------------------------------------------------------------
1 | import {
2 | List,
3 | ListItem,
4 | Typography,
5 | TextField,
6 | Button,
7 | Link,
8 | } from '@material-ui/core';
9 | import axios from 'axios';
10 | import { useRouter } from 'next/router';
11 | import NextLink from 'next/link';
12 | import React, { useContext, useEffect } from 'react';
13 | import Layout from '../components/Layout';
14 | import { Store } from '../utils/Store';
15 | import useStyles from '../utils/styles';
16 | import Cookies from 'js-cookie';
17 | import { Controller, useForm } from 'react-hook-form';
18 | import { useSnackbar } from 'notistack';
19 | import { getError } from '../utils/error';
20 |
21 | export default function Login() {
22 | const {
23 | handleSubmit,
24 | control,
25 | formState: { errors },
26 | } = useForm();
27 | const { enqueueSnackbar, closeSnackbar } = useSnackbar();
28 | const router = useRouter();
29 | const { redirect } = router.query; // login?redirect=/shipping
30 | const { state, dispatch } = useContext(Store);
31 | const { userInfo } = state;
32 | useEffect(() => {
33 | if (userInfo) {
34 | router.push('/');
35 | }
36 | }, []);
37 |
38 | const classes = useStyles();
39 | const submitHandler = async ({ email, password }) => {
40 | closeSnackbar();
41 | try {
42 | const { data } = await axios.post('/api/users/login', {
43 | email,
44 | password,
45 | });
46 | dispatch({ type: 'USER_LOGIN', payload: data });
47 | Cookies.set('userInfo', data);
48 | router.push(redirect || '/');
49 | } catch (err) {
50 | enqueueSnackbar(getError(err), { variant: 'error' });
51 | }
52 | };
53 | return (
54 |
55 |
131 |
132 | );
133 | }
134 |
--------------------------------------------------------------------------------
/pages/map.js:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import dynamic from 'next/dynamic';
3 | import useStyles from '../utils/styles';
4 | import { Store } from '../utils/Store';
5 | import React, { useContext, useEffect, useRef, useState } from 'react';
6 | import axios from 'axios';
7 | import { useSnackbar } from 'notistack';
8 | import { CircularProgress } from '@material-ui/core';
9 | import {
10 | GoogleMap,
11 | LoadScript,
12 | Marker,
13 | StandaloneSearchBox,
14 | } from '@react-google-maps/api';
15 | import { getError } from '../utils/error';
16 |
17 | const defaultLocation = { lat: 45.516, lng: -73.56 };
18 | const libs = ['places'];
19 |
20 | function Map() {
21 | const router = useRouter();
22 | const classes = useStyles();
23 | const { enqueueSnackbar } = useSnackbar();
24 |
25 | const { state, dispatch } = useContext(Store);
26 | const { userInfo } = state;
27 |
28 | const [googleApiKey, setGoogleApiKey] = useState('');
29 | useEffect(() => {
30 | const fetchGoogleApiKey = async () => {
31 | try {
32 | const { data } = await axios('/api/keys/google', {
33 | headers: { authorization: `Bearer ${userInfo.token}` },
34 | });
35 | setGoogleApiKey(data);
36 | getUserCurrentLocation();
37 | } catch (err) {
38 | enqueueSnackbar(getError(err), { variant: 'error' });
39 | }
40 | };
41 | fetchGoogleApiKey();
42 | }, []);
43 |
44 | const [center, setCenter] = useState(defaultLocation);
45 | const [location, setLocation] = useState(center);
46 |
47 | const getUserCurrentLocation = () => {
48 | if (!navigator.geolocation) {
49 | enqueueSnackbar('Geolocation is not supported by this browser', {
50 | variant: 'error',
51 | });
52 | } else {
53 | navigator.geolocation.getCurrentPosition((position) => {
54 | setCenter({
55 | lat: position.coords.latitude,
56 | lng: position.coords.longitude,
57 | });
58 | setLocation({
59 | lat: position.coords.latitude,
60 | lng: position.coords.longitude,
61 | });
62 | });
63 | }
64 | };
65 |
66 | const mapRef = useRef(null);
67 | const placeRef = useRef(null);
68 | const markerRef = useRef(null);
69 |
70 | const onLoad = (map) => {
71 | mapRef.current = map;
72 | };
73 | const onIdle = () => {
74 | setLocation({
75 | lat: mapRef.current.center.lat(),
76 | lng: mapRef.current.center.lng(),
77 | });
78 | };
79 |
80 | const onLoadPlaces = (place) => {
81 | placeRef.current = place;
82 | };
83 | const onPlacesChanged = () => {
84 | const place = placeRef.current.getPlaces()[0].geometry.location;
85 | setCenter({ lat: place.lat(), lng: place.lng() });
86 | setLocation({ lat: place.lat(), lng: place.lng() });
87 | };
88 | const onConfirm = () => {
89 | const places = placeRef.current.getPlaces();
90 | if (places && places.length === 1) {
91 | dispatch({
92 | type: 'SAVE_SHIPPING_ADDRESS_MAP_LOCATION',
93 | payload: {
94 | lat: location.lat,
95 | lng: location.lng,
96 | address: places[0].formatted_address,
97 | name: places[0].name,
98 | vicinity: places[0].vicinity,
99 | googleAddressId: places[0].id,
100 | },
101 | });
102 | enqueueSnackbar('location selected successfully', {
103 | variant: 'success',
104 | });
105 | router.push('/shipping');
106 | }
107 | };
108 | const onMarkerLoad = (marker) => {
109 | markerRef.current = marker;
110 | };
111 | return googleApiKey ? (
112 |
113 |
114 |
122 |
126 |
127 |
128 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | ) : (
138 |
139 | );
140 | }
141 |
142 | export default dynamic(() => Promise.resolve(Map), { ssr: false });
143 |
--------------------------------------------------------------------------------
/pages/order-history.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import dynamic from 'next/dynamic';
3 | import { useRouter } from 'next/router';
4 | import NextLink from 'next/link';
5 | import React, { useEffect, useContext, useReducer } from 'react';
6 | import {
7 | CircularProgress,
8 | Grid,
9 | List,
10 | ListItem,
11 | TableContainer,
12 | Typography,
13 | Card,
14 | Table,
15 | TableHead,
16 | TableRow,
17 | TableCell,
18 | TableBody,
19 | Button,
20 | ListItemText,
21 | } from '@material-ui/core';
22 | import { getError } from '../utils/error';
23 | import { Store } from '../utils/Store';
24 | import Layout from '../components/Layout';
25 | import useStyles from '../utils/styles';
26 |
27 | function reducer(state, action) {
28 | switch (action.type) {
29 | case 'FETCH_REQUEST':
30 | return { ...state, loading: true, error: '' };
31 | case 'FETCH_SUCCESS':
32 | return { ...state, loading: false, orders: action.payload, error: '' };
33 | case 'FETCH_FAIL':
34 | return { ...state, loading: false, error: action.payload };
35 | default:
36 | state;
37 | }
38 | }
39 |
40 | function OrderHistory() {
41 | const { state } = useContext(Store);
42 | const router = useRouter();
43 | const classes = useStyles();
44 | const { userInfo } = state;
45 |
46 | const [{ loading, error, orders }, dispatch] = useReducer(reducer, {
47 | loading: true,
48 | orders: [],
49 | error: '',
50 | });
51 |
52 | useEffect(() => {
53 | if (!userInfo) {
54 | router.push('/login');
55 | }
56 | const fetchOrders = async () => {
57 | try {
58 | dispatch({ type: 'FETCH_REQUEST' });
59 | const { data } = await axios.get(`/api/orders/history`, {
60 | headers: { authorization: `Bearer ${userInfo.token}` },
61 | });
62 | dispatch({ type: 'FETCH_SUCCESS', payload: data });
63 | } catch (err) {
64 | dispatch({ type: 'FETCH_FAIL', payload: getError(err) });
65 | }
66 | };
67 | fetchOrders();
68 | }, []);
69 | return (
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | Order History
94 |
95 |
96 |
97 | {loading ? (
98 |
99 | ) : error ? (
100 | {error}
101 | ) : (
102 |
103 |
104 |
105 |
106 | ID
107 | DATE
108 | TOTAL
109 | PAID
110 | DELIVERED
111 | ACTION
112 |
113 |
114 |
115 | {orders.map((order) => (
116 |
117 | {order._id.substring(20, 24)}
118 | {order.createdAt}
119 | ${order.totalPrice}
120 |
121 | {order.isPaid
122 | ? `paid at ${order.paidAt}`
123 | : 'not paid'}
124 |
125 |
126 | {order.isDelivered
127 | ? `delivered at ${order.deliveredAt}`
128 | : 'not delivered'}
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | ))}
137 |
138 |
139 |
140 | )}
141 |
142 |
143 |
144 |
145 |
146 |
147 | );
148 | }
149 |
150 | export default dynamic(() => Promise.resolve(OrderHistory), { ssr: false });
151 |
--------------------------------------------------------------------------------
/pages/order/[id].js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useReducer } from 'react';
2 | import dynamic from 'next/dynamic';
3 | import Layout from '../../components/Layout';
4 | import { Store } from '../../utils/Store';
5 | import NextLink from 'next/link';
6 | import Image from 'next/image';
7 | import {
8 | Grid,
9 | TableContainer,
10 | Table,
11 | Typography,
12 | TableHead,
13 | TableBody,
14 | TableRow,
15 | TableCell,
16 | Link,
17 | CircularProgress,
18 | Button,
19 | Card,
20 | List,
21 | ListItem,
22 | } from '@material-ui/core';
23 | import axios from 'axios';
24 | import { useRouter } from 'next/router';
25 | import useStyles from '../../utils/styles';
26 | import { useSnackbar } from 'notistack';
27 | import { getError } from '../../utils/error';
28 | import { PayPalButtons, usePayPalScriptReducer } from '@paypal/react-paypal-js';
29 |
30 | function reducer(state, action) {
31 | switch (action.type) {
32 | case 'FETCH_REQUEST':
33 | return { ...state, loading: true, error: '' };
34 | case 'FETCH_SUCCESS':
35 | return { ...state, loading: false, order: action.payload, error: '' };
36 | case 'FETCH_FAIL':
37 | return { ...state, loading: false, error: action.payload };
38 | case 'PAY_REQUEST':
39 | return { ...state, loadingPay: true };
40 | case 'PAY_SUCCESS':
41 | return { ...state, loadingPay: false, successPay: true };
42 | case 'PAY_FAIL':
43 | return { ...state, loadingPay: false, errorPay: action.payload };
44 | case 'PAY_RESET':
45 | return { ...state, loadingPay: false, successPay: false, errorPay: '' };
46 | case 'DELIVER_REQUEST':
47 | return { ...state, loadingDeliver: true };
48 | case 'DELIVER_SUCCESS':
49 | return { ...state, loadingDeliver: false, successDeliver: true };
50 | case 'DELIVER_FAIL':
51 | return { ...state, loadingDeliver: false, errorDeliver: action.payload };
52 | case 'DELIVER_RESET':
53 | return {
54 | ...state,
55 | loadingDeliver: false,
56 | successDeliver: false,
57 | errorDeliver: '',
58 | };
59 | default:
60 | state;
61 | }
62 | }
63 |
64 | function Order({ params }) {
65 | const orderId = params.id;
66 | const [{ isPending }, paypalDispatch] = usePayPalScriptReducer();
67 | const classes = useStyles();
68 | const router = useRouter();
69 | const { state } = useContext(Store);
70 | const { userInfo } = state;
71 |
72 | const [
73 | { loading, error, order, successPay, loadingDeliver, successDeliver },
74 | dispatch,
75 | ] = useReducer(reducer, {
76 | loading: true,
77 | order: {},
78 | error: '',
79 | });
80 | const {
81 | shippingAddress,
82 | paymentMethod,
83 | orderItems,
84 | itemsPrice,
85 | taxPrice,
86 | shippingPrice,
87 | totalPrice,
88 | isPaid,
89 | paidAt,
90 | isDelivered,
91 | deliveredAt,
92 | } = order;
93 |
94 | useEffect(() => {
95 | if (!userInfo) {
96 | return router.push('/login');
97 | }
98 | const fetchOrder = async () => {
99 | try {
100 | dispatch({ type: 'FETCH_REQUEST' });
101 | const { data } = await axios.get(`/api/orders/${orderId}`, {
102 | headers: { authorization: `Bearer ${userInfo.token}` },
103 | });
104 | dispatch({ type: 'FETCH_SUCCESS', payload: data });
105 | } catch (err) {
106 | dispatch({ type: 'FETCH_FAIL', payload: getError(err) });
107 | }
108 | };
109 | if (
110 | !order._id ||
111 | successPay ||
112 | successDeliver ||
113 | (order._id && order._id !== orderId)
114 | ) {
115 | fetchOrder();
116 | if (successPay) {
117 | dispatch({ type: 'PAY_RESET' });
118 | }
119 | if (successDeliver) {
120 | dispatch({ type: 'DELIVER_RESET' });
121 | }
122 | } else {
123 | const loadPaypalScript = async () => {
124 | const { data: clientId } = await axios.get('/api/keys/paypal', {
125 | headers: { authorization: `Bearer ${userInfo.token}` },
126 | });
127 | paypalDispatch({
128 | type: 'resetOptions',
129 | value: {
130 | 'client-id': clientId,
131 | currency: 'USD',
132 | },
133 | });
134 | paypalDispatch({ type: 'setLoadingStatus', value: 'pending' });
135 | };
136 | loadPaypalScript();
137 | }
138 | }, [order, successPay, successDeliver]);
139 | const { enqueueSnackbar } = useSnackbar();
140 |
141 | function createOrder(data, actions) {
142 | return actions.order
143 | .create({
144 | purchase_units: [
145 | {
146 | amount: { value: totalPrice },
147 | },
148 | ],
149 | })
150 | .then((orderID) => {
151 | return orderID;
152 | });
153 | }
154 | function onApprove(data, actions) {
155 | return actions.order.capture().then(async function (details) {
156 | try {
157 | dispatch({ type: 'PAY_REQUEST' });
158 | const { data } = await axios.put(
159 | `/api/orders/${order._id}/pay`,
160 | details,
161 | {
162 | headers: { authorization: `Bearer ${userInfo.token}` },
163 | }
164 | );
165 | dispatch({ type: 'PAY_SUCCESS', payload: data });
166 | enqueueSnackbar('Order is paid', { variant: 'success' });
167 | } catch (err) {
168 | dispatch({ type: 'PAY_FAIL', payload: getError(err) });
169 | enqueueSnackbar(getError(err), { variant: 'error' });
170 | }
171 | });
172 | }
173 |
174 | function onError(err) {
175 | enqueueSnackbar(getError(err), { variant: 'error' });
176 | }
177 |
178 | async function deliverOrderHandler() {
179 | try {
180 | dispatch({ type: 'DELIVER_REQUEST' });
181 | const { data } = await axios.put(
182 | `/api/orders/${order._id}/deliver`,
183 | {},
184 | {
185 | headers: { authorization: `Bearer ${userInfo.token}` },
186 | }
187 | );
188 | dispatch({ type: 'DELIVER_SUCCESS', payload: data });
189 | enqueueSnackbar('Order is delivered', { variant: 'success' });
190 | } catch (err) {
191 | dispatch({ type: 'DELIVER_FAIL', payload: getError(err) });
192 | enqueueSnackbar(getError(err), { variant: 'error' });
193 | }
194 | }
195 |
196 | return (
197 |
198 |
199 | Order {orderId}
200 |
201 | {loading ? (
202 |
203 | ) : error ? (
204 | {error}
205 | ) : (
206 |
207 |
208 |
209 |
210 |
211 |
212 | Shipping Address
213 |
214 |
215 |
216 | {shippingAddress.fullName}, {shippingAddress.address},{' '}
217 | {shippingAddress.city}, {shippingAddress.postalCode},{' '}
218 | {shippingAddress.country}
219 |
220 | {shippingAddress.location && (
221 |
226 | Show On Map
227 |
228 | )}
229 |
230 |
231 | Status:{' '}
232 | {isDelivered
233 | ? `delivered at ${deliveredAt}`
234 | : 'not delivered'}
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 | Payment Method
243 |
244 |
245 | {paymentMethod}
246 |
247 | Status: {isPaid ? `paid at ${paidAt}` : 'not paid'}
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 | Order Items
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 | Image
264 | Name
265 | Quantity
266 | Price
267 |
268 |
269 |
270 | {orderItems.map((item) => (
271 |
272 |
273 |
274 |
275 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 | {item.name}
289 |
290 |
291 |
292 |
293 | {item.quantity}
294 |
295 |
296 | ${item.price}
297 |
298 |
299 | ))}
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 | Order Summary
312 |
313 |
314 |
315 |
316 | Items:
317 |
318 |
319 | ${itemsPrice}
320 |
321 |
322 |
323 |
324 |
325 |
326 | Tax:
327 |
328 |
329 | ${taxPrice}
330 |
331 |
332 |
333 |
334 |
335 |
336 | Shipping:
337 |
338 |
339 | ${shippingPrice}
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 | Total:
348 |
349 |
350 |
351 |
352 | ${totalPrice}
353 |
354 |
355 |
356 |
357 | {!isPaid && (
358 |
359 | {isPending ? (
360 |
361 | ) : (
362 |
369 | )}
370 |
371 | )}
372 | {userInfo.isAdmin && order.isPaid && !order.isDelivered && (
373 |
374 | {loadingDeliver && }
375 |
383 |
384 | )}
385 |
386 |
387 |
388 |
389 | )}
390 |
391 | );
392 | }
393 |
394 | export async function getServerSideProps({ params }) {
395 | return { props: { params } };
396 | }
397 |
398 | export default dynamic(() => Promise.resolve(Order), { ssr: false });
399 |
--------------------------------------------------------------------------------
/pages/payment.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie';
2 | import { useRouter } from 'next/router';
3 | import React, { useContext, useEffect, useState } from 'react';
4 | import { Store } from '../utils/Store';
5 | import Layout from '../components/Layout';
6 | import CheckoutWizard from '../components/CheckoutWizard';
7 | import useStyles from '../utils/styles';
8 | import {
9 | Button,
10 | FormControl,
11 | FormControlLabel,
12 | List,
13 | ListItem,
14 | Radio,
15 | RadioGroup,
16 | Typography,
17 | } from '@material-ui/core';
18 | import { useSnackbar } from 'notistack';
19 |
20 | export default function Payment() {
21 | const { enqueueSnackbar, closeSnackbar } = useSnackbar();
22 | const classes = useStyles();
23 | const router = useRouter();
24 | const [paymentMethod, setPaymentMethod] = useState('');
25 | const { state, dispatch } = useContext(Store);
26 | const {
27 | cart: { shippingAddress },
28 | } = state;
29 | useEffect(() => {
30 | if (!shippingAddress.address) {
31 | router.push('/shipping');
32 | } else {
33 | setPaymentMethod(Cookies.get('paymentMethod') || '');
34 | }
35 | }, []);
36 | const submitHandler = (e) => {
37 | closeSnackbar();
38 | e.preventDefault();
39 | if (!paymentMethod) {
40 | enqueueSnackbar('Payment method is required', { variant: 'error' });
41 | } else {
42 | dispatch({ type: 'SAVE_PAYMENT_METHOD', payload: paymentMethod });
43 | Cookies.set('paymentMethod', paymentMethod);
44 | router.push('/placeorder');
45 | }
46 | };
47 | return (
48 |
49 |
50 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/pages/placeorder.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from 'react';
2 | import dynamic from 'next/dynamic';
3 | import Layout from '../components/Layout';
4 | import { Store } from '../utils/Store';
5 | import NextLink from 'next/link';
6 | import Image from 'next/image';
7 | import {
8 | Grid,
9 | TableContainer,
10 | Table,
11 | Typography,
12 | TableHead,
13 | TableBody,
14 | TableRow,
15 | TableCell,
16 | Link,
17 | CircularProgress,
18 | Button,
19 | Card,
20 | List,
21 | ListItem,
22 | } from '@material-ui/core';
23 | import axios from 'axios';
24 | import { useRouter } from 'next/router';
25 | import useStyles from '../utils/styles';
26 | import CheckoutWizard from '../components/CheckoutWizard';
27 | import { useSnackbar } from 'notistack';
28 | import { getError } from '../utils/error';
29 | import Cookies from 'js-cookie';
30 |
31 | function PlaceOrder() {
32 | const classes = useStyles();
33 | const router = useRouter();
34 | const { state, dispatch } = useContext(Store);
35 | const {
36 | userInfo,
37 | cart: { cartItems, shippingAddress, paymentMethod },
38 | } = state;
39 | const round2 = (num) => Math.round(num * 100 + Number.EPSILON) / 100; // 123.456 => 123.46
40 | const itemsPrice = round2(
41 | cartItems.reduce((a, c) => a + c.price * c.quantity, 0)
42 | );
43 | const shippingPrice = itemsPrice > 200 ? 0 : 15;
44 | const taxPrice = round2(itemsPrice * 0.15);
45 | const totalPrice = round2(itemsPrice + shippingPrice + taxPrice);
46 |
47 | useEffect(() => {
48 | if (!paymentMethod) {
49 | router.push('/payment');
50 | }
51 | if (cartItems.length === 0) {
52 | router.push('/cart');
53 | }
54 | }, []);
55 | const { closeSnackbar, enqueueSnackbar } = useSnackbar();
56 | const [loading, setLoading] = useState(false);
57 | const placeOrderHandler = async () => {
58 | closeSnackbar();
59 | try {
60 | setLoading(true);
61 | const { data } = await axios.post(
62 | '/api/orders',
63 | {
64 | orderItems: cartItems,
65 | shippingAddress,
66 | paymentMethod,
67 | itemsPrice,
68 | shippingPrice,
69 | taxPrice,
70 | totalPrice,
71 | },
72 | {
73 | headers: {
74 | authorization: `Bearer ${userInfo.token}`,
75 | },
76 | }
77 | );
78 | dispatch({ type: 'CART_CLEAR' });
79 | Cookies.remove('cartItems');
80 | setLoading(false);
81 | router.push(`/order/${data._id}`);
82 | } catch (err) {
83 | setLoading(false);
84 | enqueueSnackbar(getError(err), { variant: 'error' });
85 | }
86 | };
87 | return (
88 |
89 |
90 |
91 | Place Order
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | Shipping Address
101 |
102 |
103 |
104 | {shippingAddress.fullName}, {shippingAddress.address},{' '}
105 | {shippingAddress.city}, {shippingAddress.postalCode},{' '}
106 | {shippingAddress.country}
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | Payment Method
115 |
116 |
117 | {paymentMethod}
118 |
119 |
120 |
121 |
122 |
123 |
124 | Order Items
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | Image
133 | Name
134 | Quantity
135 | Price
136 |
137 |
138 |
139 | {cartItems.map((item) => (
140 |
141 |
142 |
143 |
144 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 | {item.name}
158 |
159 |
160 |
161 |
162 | {item.quantity}
163 |
164 |
165 | ${item.price}
166 |
167 |
168 | ))}
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 | Order Summary
181 |
182 |
183 |
184 |
185 | Items:
186 |
187 |
188 | ${itemsPrice}
189 |
190 |
191 |
192 |
193 |
194 |
195 | Tax:
196 |
197 |
198 | ${taxPrice}
199 |
200 |
201 |
202 |
203 |
204 |
205 | Shipping:
206 |
207 |
208 | ${shippingPrice}
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 | Total:
217 |
218 |
219 |
220 |
221 | ${totalPrice}
222 |
223 |
224 |
225 |
226 |
227 |
235 |
236 | {loading && (
237 |
238 |
239 |
240 | )}
241 |
242 |
243 |
244 |
245 |
246 | );
247 | }
248 |
249 | export default dynamic(() => Promise.resolve(PlaceOrder), { ssr: false });
250 |
--------------------------------------------------------------------------------
/pages/product/[slug].js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from 'react';
2 | import NextLink from 'next/link';
3 | import Image from 'next/image';
4 | import {
5 | Grid,
6 | Link,
7 | List,
8 | ListItem,
9 | Typography,
10 | Card,
11 | Button,
12 | TextField,
13 | CircularProgress,
14 | } from '@material-ui/core';
15 | import Rating from '@material-ui/lab/Rating';
16 | import Layout from '../../components/Layout';
17 | import useStyles from '../../utils/styles';
18 | import Product from '../../models/Product';
19 | import db from '../../utils/db';
20 | import axios from 'axios';
21 | import { Store } from '../../utils/Store';
22 | import { getError } from '../../utils/error';
23 | import { useRouter } from 'next/router';
24 | import { useSnackbar } from 'notistack';
25 |
26 | export default function ProductScreen(props) {
27 | const router = useRouter();
28 | const { state, dispatch } = useContext(Store);
29 | const { userInfo } = state;
30 | const { product } = props;
31 | const classes = useStyles();
32 | const { enqueueSnackbar } = useSnackbar();
33 |
34 | const [reviews, setReviews] = useState([]);
35 | const [rating, setRating] = useState(0);
36 | const [comment, setComment] = useState('');
37 | const [loading, setLoading] = useState(false);
38 |
39 | const submitHandler = async (e) => {
40 | e.preventDefault();
41 | setLoading(true);
42 | try {
43 | await axios.post(
44 | `/api/products/${product._id}/reviews`,
45 | {
46 | rating,
47 | comment,
48 | },
49 | {
50 | headers: { authorization: `Bearer ${userInfo.token}` },
51 | }
52 | );
53 | setLoading(false);
54 | enqueueSnackbar('Review submitted successfully', { variant: 'success' });
55 | fetchReviews();
56 | } catch (err) {
57 | setLoading(false);
58 | enqueueSnackbar(getError(err), { variant: 'error' });
59 | }
60 | };
61 |
62 | const fetchReviews = async () => {
63 | try {
64 | const { data } = await axios.get(`/api/products/${product._id}/reviews`);
65 | setReviews(data);
66 | } catch (err) {
67 | enqueueSnackbar(getError(err), { variant: 'error' });
68 | }
69 | };
70 | useEffect(() => {
71 | fetchReviews();
72 | }, []);
73 |
74 | if (!product) {
75 | return Product Not Found
;
76 | }
77 | const addToCartHandler = async () => {
78 | const existItem = state.cart.cartItems.find((x) => x._id === product._id);
79 | const quantity = existItem ? existItem.quantity + 1 : 1;
80 | const { data } = await axios.get(`/api/products/${product._id}`);
81 | if (data.countInStock < quantity) {
82 | window.alert('Sorry. Product is out of stock');
83 | return;
84 | }
85 | dispatch({ type: 'CART_ADD_ITEM', payload: { ...product, quantity } });
86 | router.push('/cart');
87 | };
88 |
89 | return (
90 |
91 |
92 |
93 |
94 | back to products
95 |
96 |
97 |
98 |
99 |
100 |
107 |
108 |
109 |
110 |
111 |
112 | {product.name}
113 |
114 |
115 |
116 | Category: {product.category}
117 |
118 |
119 | Brand: {product.brand}
120 |
121 |
122 |
123 |
124 | ({product.numReviews} reviews)
125 |
126 |
127 |
128 | Description: {product.description}
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | Price
139 |
140 |
141 | ${product.price}
142 |
143 |
144 |
145 |
146 |
147 |
148 | Status
149 |
150 |
151 |
152 | {product.countInStock > 0 ? 'In stock' : 'Unavailable'}
153 |
154 |
155 |
156 |
157 |
158 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | Customer Reviews
175 |
176 |
177 | {reviews.length === 0 && No review}
178 | {reviews.map((review) => (
179 |
180 |
181 |
182 |
183 | {review.name}
184 |
185 | {review.createdAt.substring(0, 10)}
186 |
187 |
188 |
189 | {review.comment}
190 |
191 |
192 |
193 | ))}
194 |
195 | {userInfo ? (
196 |
233 | ) : (
234 |
235 | Please{' '}
236 |
237 | login
238 | {' '}
239 | to write a review
240 |
241 | )}
242 |
243 |
244 |
245 | );
246 | }
247 |
248 | export async function getServerSideProps(context) {
249 | const { params } = context;
250 | const { slug } = params;
251 |
252 | await db.connect();
253 | const product = await Product.findOne({ slug }, '-reviews').lean();
254 | await db.disconnect();
255 | return {
256 | props: {
257 | product: db.convertDocToObj(product),
258 | },
259 | };
260 | }
261 |
--------------------------------------------------------------------------------
/pages/profile.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import dynamic from 'next/dynamic';
3 | import { useRouter } from 'next/router';
4 | import NextLink from 'next/link';
5 | import React, { useEffect, useContext } from 'react';
6 | import {
7 | Grid,
8 | List,
9 | ListItem,
10 | Typography,
11 | Card,
12 | Button,
13 | ListItemText,
14 | TextField,
15 | } from '@material-ui/core';
16 | import { getError } from '../utils/error';
17 | import { Store } from '../utils/Store';
18 | import Layout from '../components/Layout';
19 | import useStyles from '../utils/styles';
20 | import { Controller, useForm } from 'react-hook-form';
21 | import { useSnackbar } from 'notistack';
22 | import Cookies from 'js-cookie';
23 |
24 | function Profile() {
25 | const { state, dispatch } = useContext(Store);
26 | const {
27 | handleSubmit,
28 | control,
29 | formState: { errors },
30 | setValue,
31 | } = useForm();
32 | const { enqueueSnackbar, closeSnackbar } = useSnackbar();
33 | const router = useRouter();
34 | const classes = useStyles();
35 | const { userInfo } = state;
36 |
37 | useEffect(() => {
38 | if (!userInfo) {
39 | return router.push('/login');
40 | }
41 | setValue('name', userInfo.name);
42 | setValue('email', userInfo.email);
43 | }, []);
44 | const submitHandler = async ({ name, email, password, confirmPassword }) => {
45 | closeSnackbar();
46 | if (password !== confirmPassword) {
47 | enqueueSnackbar("Passwords don't match", { variant: 'error' });
48 | return;
49 | }
50 | try {
51 | const { data } = await axios.put(
52 | '/api/users/profile',
53 | {
54 | name,
55 | email,
56 | password,
57 | },
58 | { headers: { authorization: `Bearer ${userInfo.token}` } }
59 | );
60 | dispatch({ type: 'USER_LOGIN', payload: data });
61 | Cookies.set('userInfo', data);
62 |
63 | enqueueSnackbar('Profile updated successfully', { variant: 'success' });
64 | } catch (err) {
65 | enqueueSnackbar(getError(err), { variant: 'error' });
66 | }
67 | };
68 | return (
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | Profile
93 |
94 |
95 |
96 |
229 |
230 |
231 |
232 |
233 |
234 |
235 | );
236 | }
237 |
238 | export default dynamic(() => Promise.resolve(Profile), { ssr: false });
239 |
--------------------------------------------------------------------------------
/pages/register.js:
--------------------------------------------------------------------------------
1 | import {
2 | List,
3 | ListItem,
4 | Typography,
5 | TextField,
6 | Button,
7 | Link,
8 | } from '@material-ui/core';
9 | import axios from 'axios';
10 | import { useRouter } from 'next/router';
11 | import NextLink from 'next/link';
12 | import React, { useContext, useEffect } from 'react';
13 | import Layout from '../components/Layout';
14 | import { Store } from '../utils/Store';
15 | import useStyles from '../utils/styles';
16 | import Cookies from 'js-cookie';
17 | import { Controller, useForm } from 'react-hook-form';
18 | import { useSnackbar } from 'notistack';
19 | import { getError } from '../utils/error';
20 |
21 | export default function Register() {
22 | const {
23 | handleSubmit,
24 | control,
25 | formState: { errors },
26 | } = useForm();
27 | const { enqueueSnackbar, closeSnackbar } = useSnackbar();
28 | const router = useRouter();
29 | const { redirect } = router.query;
30 | const { state, dispatch } = useContext(Store);
31 | const { userInfo } = state;
32 | useEffect(() => {
33 | if (userInfo) {
34 | router.push('/');
35 | }
36 | }, []);
37 |
38 | const classes = useStyles();
39 | const submitHandler = async ({ name, email, password, confirmPassword }) => {
40 | closeSnackbar();
41 | if (password !== confirmPassword) {
42 | enqueueSnackbar("Passwords don't match", { variant: 'error' });
43 | return;
44 | }
45 | try {
46 | const { data } = await axios.post('/api/users/register', {
47 | name,
48 | email,
49 | password,
50 | });
51 | dispatch({ type: 'USER_LOGIN', payload: data });
52 | Cookies.set('userInfo', data);
53 | router.push(redirect || '/');
54 | } catch (err) {
55 | enqueueSnackbar(getError(err), { variant: 'error' });
56 | }
57 | };
58 | return (
59 |
60 |
194 |
195 | );
196 | }
197 |
--------------------------------------------------------------------------------
/pages/search.js:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Grid,
5 | List,
6 | ListItem,
7 | MenuItem,
8 | Select,
9 | Typography,
10 | } from '@material-ui/core';
11 | import CancelIcon from '@material-ui/icons/Cancel';
12 | import { useRouter } from 'next/router';
13 | import React, { useContext } from 'react';
14 | import Layout from '../components/Layout';
15 | import db from '../utils/db';
16 | import Product from '../models/Product';
17 | import useStyles from '../utils/styles';
18 | import ProductItem from '../components/ProductItem';
19 | import { Store } from '../utils/Store';
20 | import axios from 'axios';
21 | import Rating from '@material-ui/lab/Rating';
22 | import { Pagination } from '@material-ui/lab';
23 |
24 | const PAGE_SIZE = 3;
25 |
26 | const prices = [
27 | {
28 | name: '$1 to $50',
29 | value: '1-50',
30 | },
31 | {
32 | name: '$51 to $200',
33 | value: '51-200',
34 | },
35 | {
36 | name: '$201 to $1000',
37 | value: '201-1000',
38 | },
39 | ];
40 |
41 | const ratings = [1, 2, 3, 4, 5];
42 |
43 | export default function Search(props) {
44 | const classes = useStyles();
45 | const router = useRouter();
46 | const {
47 | query = 'all',
48 | category = 'all',
49 | brand = 'all',
50 | price = 'all',
51 | rating = 'all',
52 | sort = 'featured',
53 | } = router.query;
54 | const { products, countProducts, categories, brands, pages } = props;
55 |
56 | const filterSearch = ({
57 | page,
58 | category,
59 | brand,
60 | sort,
61 | min,
62 | max,
63 | searchQuery,
64 | price,
65 | rating,
66 | }) => {
67 | const path = router.pathname;
68 | const { query } = router;
69 | if (page) query.page = page;
70 | if (searchQuery) query.searchQuery = searchQuery;
71 | if (sort) query.sort = sort;
72 | if (category) query.category = category;
73 | if (brand) query.brand = brand;
74 | if (price) query.price = price;
75 | if (rating) query.rating = rating;
76 | if (min) query.min ? query.min : query.min === 0 ? 0 : min;
77 | if (max) query.max ? query.max : query.max === 0 ? 0 : max;
78 |
79 | router.push({
80 | pathname: path,
81 | query: query,
82 | });
83 | };
84 | const categoryHandler = (e) => {
85 | filterSearch({ category: e.target.value });
86 | };
87 | const pageHandler = (e, page) => {
88 | filterSearch({ page });
89 | };
90 | const brandHandler = (e) => {
91 | filterSearch({ brand: e.target.value });
92 | };
93 | const sortHandler = (e) => {
94 | filterSearch({ sort: e.target.value });
95 | };
96 | const priceHandler = (e) => {
97 | filterSearch({ price: e.target.value });
98 | };
99 | const ratingHandler = (e) => {
100 | filterSearch({ rating: e.target.value });
101 | };
102 |
103 | const { state, dispatch } = useContext(Store);
104 | const addToCartHandler = async (product) => {
105 | const existItem = state.cart.cartItems.find((x) => x._id === product._id);
106 | const quantity = existItem ? existItem.quantity + 1 : 1;
107 | const { data } = await axios.get(`/api/products/${product._id}`);
108 | if (data.countInStock < quantity) {
109 | window.alert('Sorry. Product is out of stock');
110 | return;
111 | }
112 | dispatch({ type: 'CART_ADD_ITEM', payload: { ...product, quantity } });
113 | router.push('/cart');
114 | };
115 | return (
116 |
117 |
118 |
119 |
120 |
121 |
122 | Categories
123 |
132 |
133 |
134 |
135 |
136 | Brands
137 |
146 |
147 |
148 |
149 |
150 | Prices
151 |
159 |
160 |
161 |
162 |
163 | Ratings
164 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 | {products.length === 0 ? 'No' : countProducts} Results
181 | {query !== 'all' && query !== '' && ' : ' + query}
182 | {category !== 'all' && ' : ' + category}
183 | {brand !== 'all' && ' : ' + brand}
184 | {price !== 'all' && ' : Price ' + price}
185 | {rating !== 'all' && ' : Rating ' + rating + ' & up'}
186 | {(query !== 'all' && query !== '') ||
187 | category !== 'all' ||
188 | brand !== 'all' ||
189 | rating !== 'all' ||
190 | price !== 'all' ? (
191 |
194 | ) : null}
195 |
196 |
197 |
198 | Sort by
199 |
200 |
207 |
208 |
209 |
210 | {products.map((product) => (
211 |
212 |
216 |
217 | ))}
218 |
219 |
225 |
226 |
227 |
228 | );
229 | }
230 |
231 | export async function getServerSideProps({ query }) {
232 | await db.connect();
233 | const pageSize = query.pageSize || PAGE_SIZE;
234 | const page = query.page || 1;
235 | const category = query.category || '';
236 | const brand = query.brand || '';
237 | const price = query.price || '';
238 | const rating = query.rating || '';
239 | const sort = query.sort || '';
240 | const searchQuery = query.query || '';
241 |
242 | const queryFilter =
243 | searchQuery && searchQuery !== 'all'
244 | ? {
245 | name: {
246 | $regex: searchQuery,
247 | $options: 'i',
248 | },
249 | }
250 | : {};
251 | const categoryFilter = category && category !== 'all' ? { category } : {};
252 | const brandFilter = brand && brand !== 'all' ? { brand } : {};
253 | const ratingFilter =
254 | rating && rating !== 'all'
255 | ? {
256 | rating: {
257 | $gte: Number(rating),
258 | },
259 | }
260 | : {};
261 | // 10-50
262 | const priceFilter =
263 | price && price !== 'all'
264 | ? {
265 | price: {
266 | $gte: Number(price.split('-')[0]),
267 | $lte: Number(price.split('-')[1]),
268 | },
269 | }
270 | : {};
271 |
272 | const order =
273 | sort === 'featured'
274 | ? { featured: -1 }
275 | : sort === 'lowest'
276 | ? { price: 1 }
277 | : sort === 'highest'
278 | ? { price: -1 }
279 | : sort === 'toprated'
280 | ? { rating: -1 }
281 | : sort === 'newest'
282 | ? { createdAt: -1 }
283 | : { _id: -1 };
284 |
285 | const categories = await Product.find().distinct('category');
286 | const brands = await Product.find().distinct('brand');
287 | const productDocs = await Product.find(
288 | {
289 | ...queryFilter,
290 | ...categoryFilter,
291 | ...priceFilter,
292 | ...brandFilter,
293 | ...ratingFilter,
294 | },
295 | '-reviews'
296 | )
297 | .sort(order)
298 | .skip(pageSize * (page - 1))
299 | .limit(pageSize)
300 | .lean();
301 |
302 | const countProducts = await Product.countDocuments({
303 | ...queryFilter,
304 | ...categoryFilter,
305 | ...priceFilter,
306 | ...brandFilter,
307 | ...ratingFilter,
308 | });
309 | await db.disconnect();
310 |
311 | const products = productDocs.map(db.convertDocToObj);
312 |
313 | return {
314 | props: {
315 | products,
316 | countProducts,
317 | page,
318 | pages: Math.ceil(countProducts / pageSize),
319 | categories,
320 | brands,
321 | },
322 | };
323 | }
324 |
--------------------------------------------------------------------------------
/pages/shipping.js:
--------------------------------------------------------------------------------
1 | import {
2 | List,
3 | ListItem,
4 | Typography,
5 | TextField,
6 | Button,
7 | } from '@material-ui/core';
8 | import { useRouter } from 'next/router';
9 | import React, { useContext, useEffect } from 'react';
10 | import Layout from '../components/Layout';
11 | import { Store } from '../utils/Store';
12 | import useStyles from '../utils/styles';
13 | import Cookies from 'js-cookie';
14 | import { Controller, useForm } from 'react-hook-form';
15 | import CheckoutWizard from '../components/CheckoutWizard';
16 |
17 | export default function Shipping() {
18 | const {
19 | handleSubmit,
20 | control,
21 | formState: { errors },
22 | setValue,
23 | getValues,
24 | } = useForm();
25 | const router = useRouter();
26 | const { state, dispatch } = useContext(Store);
27 | const {
28 | userInfo,
29 | cart: { shippingAddress },
30 | } = state;
31 | const { location } = shippingAddress;
32 | useEffect(() => {
33 | if (!userInfo) {
34 | router.push('/login?redirect=/shipping');
35 | }
36 | setValue('fullName', shippingAddress.fullName);
37 | setValue('address', shippingAddress.address);
38 | setValue('city', shippingAddress.city);
39 | setValue('postalCode', shippingAddress.postalCode);
40 | setValue('country', shippingAddress.country);
41 | }, []);
42 |
43 | const classes = useStyles();
44 | const submitHandler = ({ fullName, address, city, postalCode, country }) => {
45 | dispatch({
46 | type: 'SAVE_SHIPPING_ADDRESS',
47 | payload: { fullName, address, city, postalCode, country, location },
48 | });
49 | Cookies.set('shippingAddress', {
50 | fullName,
51 | address,
52 | city,
53 | postalCode,
54 | country,
55 | location,
56 | });
57 | router.push('/payment');
58 | };
59 |
60 | const chooseLocationHandler = () => {
61 | const fullName = getValues('fullName');
62 | const address = getValues('address');
63 | const city = getValues('city');
64 | const postalCode = getValues('postalCode');
65 | const country = getValues('country');
66 | dispatch({
67 | type: 'SAVE_SHIPPING_ADDRESS',
68 | payload: { fullName, address, city, postalCode, country },
69 | });
70 | Cookies.set('shippingAddress', {
71 | fullName,
72 | address,
73 | city,
74 | postalCode,
75 | country,
76 | location,
77 | });
78 | router.push('/map');
79 | };
80 | return (
81 |
82 |
83 |
247 |
248 | );
249 | }
250 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basir/next-amazona/7dfc8e835de5e1f8b138bcb7d1ad8a98a8bbe7ae/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/banner1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basir/next-amazona/7dfc8e835de5e1f8b138bcb7d1ad8a98a8bbe7ae/public/images/banner1.jpg
--------------------------------------------------------------------------------
/public/images/banner2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basir/next-amazona/7dfc8e835de5e1f8b138bcb7d1ad8a98a8bbe7ae/public/images/banner2.jpg
--------------------------------------------------------------------------------
/public/images/pants1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basir/next-amazona/7dfc8e835de5e1f8b138bcb7d1ad8a98a8bbe7ae/public/images/pants1.jpg
--------------------------------------------------------------------------------
/public/images/pants2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basir/next-amazona/7dfc8e835de5e1f8b138bcb7d1ad8a98a8bbe7ae/public/images/pants2.jpg
--------------------------------------------------------------------------------
/public/images/pants3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basir/next-amazona/7dfc8e835de5e1f8b138bcb7d1ad8a98a8bbe7ae/public/images/pants3.jpg
--------------------------------------------------------------------------------
/public/images/shirt1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basir/next-amazona/7dfc8e835de5e1f8b138bcb7d1ad8a98a8bbe7ae/public/images/shirt1.jpg
--------------------------------------------------------------------------------
/public/images/shirt2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basir/next-amazona/7dfc8e835de5e1f8b138bcb7d1ad8a98a8bbe7ae/public/images/shirt2.jpg
--------------------------------------------------------------------------------
/public/images/shirt3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basir/next-amazona/7dfc8e835de5e1f8b138bcb7d1ad8a98a8bbe7ae/public/images/shirt3.jpg
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 0 0.5rem;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | height: 100vh;
9 | }
10 |
11 | .main {
12 | padding: 5rem 0;
13 | flex: 1;
14 | display: flex;
15 | flex-direction: column;
16 | justify-content: center;
17 | align-items: center;
18 | }
19 |
20 | .footer {
21 | width: 100%;
22 | height: 100px;
23 | border-top: 1px solid #eaeaea;
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 | }
28 |
29 | .footer a {
30 | display: flex;
31 | justify-content: center;
32 | align-items: center;
33 | flex-grow: 1;
34 | }
35 |
36 | .title a {
37 | color: #0070f3;
38 | text-decoration: none;
39 | }
40 |
41 | .title a:hover,
42 | .title a:focus,
43 | .title a:active {
44 | text-decoration: underline;
45 | }
46 |
47 | .title {
48 | margin: 0;
49 | line-height: 1.15;
50 | font-size: 4rem;
51 | }
52 |
53 | .title,
54 | .description {
55 | text-align: center;
56 | }
57 |
58 | .description {
59 | line-height: 1.5;
60 | font-size: 1.5rem;
61 | }
62 |
63 | .code {
64 | background: #fafafa;
65 | border-radius: 5px;
66 | padding: 0.75rem;
67 | font-size: 1.1rem;
68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
69 | Bitstream Vera Sans Mono, Courier New, monospace;
70 | }
71 |
72 | .grid {
73 | display: flex;
74 | align-items: center;
75 | justify-content: center;
76 | flex-wrap: wrap;
77 | max-width: 800px;
78 | margin-top: 3rem;
79 | }
80 |
81 | .card {
82 | margin: 1rem;
83 | padding: 1.5rem;
84 | text-align: left;
85 | color: inherit;
86 | text-decoration: none;
87 | border: 1px solid #eaeaea;
88 | border-radius: 10px;
89 | transition: color 0.15s ease, border-color 0.15s ease;
90 | width: 45%;
91 | }
92 |
93 | .card:hover,
94 | .card:focus,
95 | .card:active {
96 | color: #0070f3;
97 | border-color: #0070f3;
98 | }
99 |
100 | .card h2 {
101 | margin: 0 0 1rem 0;
102 | font-size: 1.5rem;
103 | }
104 |
105 | .card p {
106 | margin: 0;
107 | font-size: 1.25rem;
108 | line-height: 1.5;
109 | }
110 |
111 | .logo {
112 | height: 1em;
113 | margin-left: 0.5rem;
114 | }
115 |
116 | @media (max-width: 600px) {
117 | .grid {
118 | width: 100%;
119 | flex-direction: column;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
--------------------------------------------------------------------------------
/utils/Store.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie';
2 | import { createContext, useReducer } from 'react';
3 |
4 | export const Store = createContext();
5 | const initialState = {
6 | darkMode: Cookies.get('darkMode') === 'ON' ? true : false,
7 | cart: {
8 | cartItems: Cookies.get('cartItems')
9 | ? JSON.parse(Cookies.get('cartItems'))
10 | : [],
11 | shippingAddress: Cookies.get('shippingAddress')
12 | ? JSON.parse(Cookies.get('shippingAddress'))
13 | : { location: {} },
14 | paymentMethod: Cookies.get('paymentMethod')
15 | ? Cookies.get('paymentMethod')
16 | : '',
17 | },
18 | userInfo: Cookies.get('userInfo')
19 | ? JSON.parse(Cookies.get('userInfo'))
20 | : null,
21 | };
22 |
23 | function reducer(state, action) {
24 | switch (action.type) {
25 | case 'DARK_MODE_ON':
26 | return { ...state, darkMode: true };
27 | case 'DARK_MODE_OFF':
28 | return { ...state, darkMode: false };
29 | case 'CART_ADD_ITEM': {
30 | const newItem = action.payload;
31 | const existItem = state.cart.cartItems.find(
32 | (item) => item._id === newItem._id
33 | );
34 | const cartItems = existItem
35 | ? state.cart.cartItems.map((item) =>
36 | item.name === existItem.name ? newItem : item
37 | )
38 | : [...state.cart.cartItems, newItem];
39 | Cookies.set('cartItems', JSON.stringify(cartItems));
40 | return { ...state, cart: { ...state.cart, cartItems } };
41 | }
42 | case 'CART_REMOVE_ITEM': {
43 | const cartItems = state.cart.cartItems.filter(
44 | (item) => item._id !== action.payload._id
45 | );
46 | Cookies.set('cartItems', JSON.stringify(cartItems));
47 | return { ...state, cart: { ...state.cart, cartItems } };
48 | }
49 | case 'SAVE_SHIPPING_ADDRESS':
50 | return {
51 | ...state,
52 | cart: {
53 | ...state.cart,
54 | shippingAddress: {
55 | ...state.cart.shippingAddress,
56 | ...action.payload,
57 | },
58 | },
59 | };
60 | case 'SAVE_SHIPPING_ADDRESS_MAP_LOCATION':
61 | return {
62 | ...state,
63 | cart: {
64 | ...state.cart,
65 | shippingAddress: {
66 | ...state.cart.shippingAddress,
67 | location: action.payload,
68 | },
69 | },
70 | };
71 | case 'SAVE_PAYMENT_METHOD':
72 | return {
73 | ...state,
74 | cart: { ...state.cart, paymentMethod: action.payload },
75 | };
76 | case 'CART_CLEAR':
77 | return { ...state, cart: { ...state.cart, cartItems: [] } };
78 | case 'USER_LOGIN':
79 | return { ...state, userInfo: action.payload };
80 | case 'USER_LOGOUT':
81 | return {
82 | ...state,
83 | userInfo: null,
84 | cart: {
85 | cartItems: [],
86 | shippingAddress: { location: {} },
87 | paymentMethod: '',
88 | },
89 | };
90 |
91 | default:
92 | return state;
93 | }
94 | }
95 |
96 | export function StoreProvider(props) {
97 | const [state, dispatch] = useReducer(reducer, initialState);
98 | const value = { state, dispatch };
99 | return {props.children};
100 | }
101 |
--------------------------------------------------------------------------------
/utils/auth.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 |
3 | const signToken = (user) => {
4 | return jwt.sign(
5 | {
6 | _id: user._id,
7 | name: user.name,
8 | email: user.email,
9 | isAdmin: user.isAdmin,
10 | },
11 |
12 | process.env.JWT_SECRET,
13 | {
14 | expiresIn: '30d',
15 | }
16 | );
17 | };
18 | const isAuth = async (req, res, next) => {
19 | const { authorization } = req.headers;
20 | if (authorization) {
21 | // Bearer xxx => xxx
22 | const token = authorization.slice(7, authorization.length);
23 | jwt.verify(token, process.env.JWT_SECRET, (err, decode) => {
24 | if (err) {
25 | res.status(401).send({ message: 'Token is not valid' });
26 | } else {
27 | req.user = decode;
28 | next();
29 | }
30 | });
31 | } else {
32 | res.status(401).send({ message: 'Token is not suppiled' });
33 | }
34 | };
35 | const isAdmin = async (req, res, next) => {
36 | if (req.user.isAdmin) {
37 | next();
38 | } else {
39 | res.status(401).send({ message: 'User is not admin' });
40 | }
41 | };
42 |
43 | export { signToken, isAuth, isAdmin };
44 |
--------------------------------------------------------------------------------
/utils/data.js:
--------------------------------------------------------------------------------
1 | import bcrypt from 'bcryptjs';
2 | const data = {
3 | users: [
4 | {
5 | name: 'John',
6 | email: 'admin@example.com',
7 | password: bcrypt.hashSync('123456'),
8 | isAdmin: true,
9 | },
10 | {
11 | name: 'Jane',
12 | email: 'user@example.com',
13 | password: bcrypt.hashSync('123456'),
14 | isAdmin: false,
15 | },
16 | ],
17 | products: [
18 | {
19 | name: 'Free Shirt',
20 | slug: 'free-shirt',
21 | category: 'Shirts',
22 | image: '/images/shirt1.jpg',
23 | isFeatured: true,
24 | featuredImage: '/images/banner1.jpg',
25 | price: 70,
26 | brand: 'Nike',
27 | rating: 4.5,
28 | numReviews: 10,
29 | countInStock: 20,
30 | description: 'A popular shirt',
31 | },
32 | {
33 | name: 'Fit Shirt',
34 | slug: 'fit-shirt',
35 | category: 'Shirts',
36 | image: '/images/shirt2.jpg',
37 | isFeatured: true,
38 | featuredImage: '/images/banner2.jpg',
39 | price: 80,
40 | brand: 'Adidas',
41 | rating: 4.2,
42 | numReviews: 10,
43 | countInStock: 20,
44 | description: 'A popular shirt',
45 | },
46 | {
47 | name: 'Slim Shirt',
48 | slug: 'slim-shirt',
49 | category: 'Shirts',
50 | image: '/images/shirt3.jpg',
51 | price: 90,
52 | brand: 'Raymond',
53 | rating: 4.5,
54 | numReviews: 10,
55 | countInStock: 20,
56 | description: 'A popular shirt',
57 | },
58 | {
59 | name: 'Golf Pants',
60 | slug: 'golf-pants',
61 | category: 'Pants',
62 | image: '/images/pants1.jpg',
63 | price: 90,
64 | brand: 'Oliver',
65 | rating: 4.5,
66 | numReviews: 10,
67 | countInStock: 20,
68 | description: 'Smart looking pants',
69 | },
70 | {
71 | name: 'Fit Pants',
72 | slug: 'fit-pants',
73 | category: 'Pants',
74 | image: '/images/pants2.jpg',
75 | price: 95,
76 | brand: 'Zara',
77 | rating: 4.5,
78 | numReviews: 10,
79 | countInStock: 20,
80 | description: 'A popular pants',
81 | },
82 | {
83 | name: 'Classic Pants',
84 | slug: 'classic-pants',
85 | category: 'Pants',
86 | image: '/images/pants3.jpg',
87 | price: 75,
88 | brand: 'Casely',
89 | rating: 4.5,
90 | numReviews: 10,
91 | countInStock: 20,
92 | description: 'A popular pants',
93 | },
94 | ],
95 | };
96 | export default data;
97 |
--------------------------------------------------------------------------------
/utils/db.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const connection = {};
4 |
5 | async function connect() {
6 | if (connection.isConnected) {
7 | console.log('already connected');
8 | return;
9 | }
10 | if (mongoose.connections.length > 0) {
11 | connection.isConnected = mongoose.connections[0].readyState;
12 | if (connection.isConnected === 1) {
13 | console.log('use previous connection');
14 | return;
15 | }
16 | await mongoose.disconnect();
17 | }
18 | const db = await mongoose.connect(process.env.MONGODB_URI, {
19 | useNewUrlParser: true,
20 | useUnifiedTopology: true,
21 | useCreateIndex: true,
22 | });
23 | console.log('new connection');
24 | connection.isConnected = db.connections[0].readyState;
25 | }
26 |
27 | async function disconnect() {
28 | if (connection.isConnected) {
29 | if (process.env.NODE_ENV === 'production') {
30 | await mongoose.disconnect();
31 | connection.isConnected = false;
32 | } else {
33 | console.log('not disconnected');
34 | }
35 | }
36 | }
37 |
38 | function convertDocToObj(doc) {
39 | doc._id = doc._id.toString();
40 | doc.createdAt = doc.createdAt.toString();
41 | doc.updatedAt = doc.updatedAt.toString();
42 | return doc;
43 | }
44 |
45 | const db = { connect, disconnect, convertDocToObj };
46 | export default db;
47 |
--------------------------------------------------------------------------------
/utils/error.js:
--------------------------------------------------------------------------------
1 | import db from './db';
2 |
3 | const getError = (err) =>
4 | err.response && err.response.data && err.response.data.message
5 | ? err.response.data.message
6 | : err.message;
7 |
8 | const onError = async (err, req, res, next) => {
9 | await db.disconnect();
10 | res.status(500).send({ message: err.toString() });
11 | };
12 | export { getError, onError };
13 |
--------------------------------------------------------------------------------
/utils/styles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core';
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | navbar: {
5 | backgroundColor: '#203040',
6 | '& a': {
7 | color: '#ffffff',
8 | marginLeft: 10,
9 | },
10 | },
11 | brand: {
12 | fontWeight: 'bold',
13 | fontSize: '1.5rem',
14 | },
15 | grow: {
16 | flexGrow: 1,
17 | },
18 | main: {
19 | minHeight: '80vh',
20 | },
21 | footer: {
22 | marginTop: 10,
23 | textAlign: 'center',
24 | },
25 | section: {
26 | marginTop: 10,
27 | marginBottom: 10,
28 | },
29 | form: {
30 | width: '100%',
31 | maxWidth: 800,
32 | margin: '0 auto',
33 | },
34 | navbarButton: {
35 | color: '#ffffff',
36 | textTransform: 'initial',
37 | },
38 | transparentBackgroud: {
39 | backgroundColor: 'transparent',
40 | },
41 | error: {
42 | color: '#f04040',
43 | },
44 | fullWidth: {
45 | width: '100%',
46 | },
47 | reviewForm: {
48 | maxWidth: 800,
49 | width: '100%',
50 | },
51 | reviewItem: {
52 | marginRight: '1rem',
53 | borderRight: '1px #808080 solid',
54 | paddingRight: '1rem',
55 | },
56 | toolbar: {
57 | justifyContent: 'space-between',
58 | },
59 | menuButton: { padding: 0 },
60 | mt1: { marginTop: '1rem' },
61 | // search
62 | searchSection: {
63 | display: 'none',
64 | [theme.breakpoints.up('md')]: {
65 | display: 'flex',
66 | },
67 | },
68 | searchForm: {
69 | border: '1px solid #ffffff',
70 | backgroundColor: '#ffffff',
71 | borderRadius: 5,
72 | },
73 | searchInput: {
74 | paddingLeft: 5,
75 | color: '#000000',
76 | '& ::placeholder': {
77 | color: '#606060',
78 | },
79 | },
80 | iconButton: {
81 | backgroundColor: '#f8c040',
82 | padding: 5,
83 | borderRadius: '0 5px 5px 0',
84 | '& span': {
85 | color: '#000000',
86 | },
87 | },
88 | sort: {
89 | marginRight: 5,
90 | },
91 |
92 | fullContainer: { height: '100vh' },
93 | mapInputBox: {
94 | position: 'absolute',
95 | display: 'flex',
96 | left: 0,
97 | right: 0,
98 | margin: '10px auto',
99 | width: 300,
100 | height: 40,
101 | '& input': {
102 | width: 250,
103 | },
104 | },
105 | }));
106 | export default useStyles;
107 |
--------------------------------------------------------------------------------