├── .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 |
193 | 199 | 204 | 205 | 206 | 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 | 246 | loginMenuCloseHandler(e, '/profile')} 248 | > 249 | Profile 250 | 251 | 253 | loginMenuCloseHandler(e, '/order-history') 254 | } 255 | > 256 | Order Hisotry 257 | 258 | {userInfo.isAdmin && ( 259 | 261 | loginMenuCloseHandler(e, '/admin/dashboard') 262 | } 263 | > 264 | Admin Dashboard 265 | 266 | )} 267 | Logout 268 | 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 |
213 | 214 | 215 | ( 223 | 232 | )} 233 | > 234 | 235 | 236 | ( 244 | 253 | )} 254 | > 255 | 256 | 257 | ( 265 | 274 | )} 275 | > 276 | 277 | 278 | ( 286 | 295 | )} 296 | > 297 | 298 | 299 | 303 | {loadingUpload && } 304 | 305 | 306 | setIsFeatured(e.target.checked)} 311 | checked={isFeatured} 312 | name="isFeatured" 313 | /> 314 | } 315 | > 316 | 317 | 318 | ( 326 | 337 | )} 338 | > 339 | 340 | 341 | 349 | {loadingUpload && } 350 | 351 | 352 | ( 360 | 371 | )} 372 | > 373 | 374 | 375 | ( 383 | 392 | )} 393 | > 394 | 395 | 396 | ( 404 | 417 | )} 418 | > 419 | 420 | 421 | ( 429 | 443 | )} 444 | > 445 | 446 | 447 | 448 | 456 | {loadingUpdate && } 457 | 458 | 459 |
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 |
164 | 165 | 166 | ( 174 | 183 | )} 184 | > 185 | 186 | 187 | setIsAdmin(e.target.checked)} 192 | checked={isAdmin} 193 | name="isAdmin" 194 | /> 195 | } 196 | > 197 | 198 | 199 | 207 | {loadingUpdate && } 208 | 209 | 210 |
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 | {product.name} 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 |
56 | 57 | Login 58 | 59 | 60 | 61 | ( 70 | 86 | )} 87 | > 88 | 89 | 90 | ( 99 | 115 | )} 116 | > 117 | 118 | 119 | 122 | 123 | 124 | Don't have an account?   125 | 126 | Register 127 | 128 | 129 | 130 |
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 |
363 | 368 |
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 |
51 | 52 | Payment Method 53 | 54 | 55 | 56 | 57 | setPaymentMethod(e.target.value)} 62 | > 63 | } 67 | > 68 | } 72 | > 73 | } 77 | > 78 | 79 | 80 | 81 | 82 | 85 | 86 | 87 | 95 | 96 | 97 |
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 | {product.name} 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 |
197 | 198 | 199 | Leave your review 200 | 201 | 202 | setComment(e.target.value)} 210 | /> 211 | 212 | 213 | setRating(e.target.value)} 217 | /> 218 | 219 | 220 | 228 | 229 | {loading && } 230 | 231 | 232 |
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 |
100 | 101 | 102 | ( 111 | 127 | )} 128 | > 129 | 130 | 131 | ( 140 | 156 | )} 157 | > 158 | 159 | 160 | 166 | value === '' || 167 | value.length > 5 || 168 | 'Password length is more than 5', 169 | }} 170 | render={({ field }) => ( 171 | 185 | )} 186 | > 187 | 188 | 189 | 195 | value === '' || 196 | value.length > 5 || 197 | 'Confirm Password length is more than 5', 198 | }} 199 | render={({ field }) => ( 200 | 214 | )} 215 | > 216 | 217 | 218 | 226 | 227 | 228 |
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 |
61 | 62 | Register 63 | 64 | 65 | 66 | ( 75 | 91 | )} 92 | > 93 | 94 | 95 | ( 104 | 120 | )} 121 | > 122 | 123 | 124 | ( 133 | 149 | )} 150 | > 151 | 152 | 153 | ( 162 | 178 | )} 179 | > 180 | 181 | 182 | 185 | 186 | 187 | Already have an account?   188 | 189 | Login 190 | 191 | 192 | 193 |
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 |
84 | 85 | Shipping Address 86 | 87 | 88 | 89 | ( 98 | 113 | )} 114 | > 115 | 116 | 117 | ( 126 | 141 | )} 142 | > 143 | 144 | 145 | ( 154 | 169 | )} 170 | > 171 | 172 | 173 | ( 182 | 197 | )} 198 | > 199 | 200 | 201 | ( 210 | 225 | )} 226 | > 227 | 228 | 229 | 236 | 237 | {location.lat && `${location.lat}, ${location.lat}`} 238 | 239 | 240 | 241 | 244 | 245 | 246 |
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 | 3 | 4 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------