├── .env.example ├── next.config.js ├── now.json ├── middlewares ├── session.js ├── middleware.js ├── authentication.js └── database.js ├── lib └── redirectTo.js ├── pages ├── _app.jsx ├── api │ ├── user │ │ ├── index.js │ │ └── profilepicture.js │ ├── session.js │ ├── authenticate.js │ ├── users.js │ ├── customer.js │ └── customer │ │ └── [id].js ├── index.jsx ├── profile │ ├── index.jsx │ └── settings.jsx ├── login.jsx ├── customer │ ├── index.jsx │ ├── listData.jsx │ └── customerForm.jsx └── signup.jsx ├── LICENSE ├── .gitignore ├── components ├── CustomerContext.jsx ├── UserContext.jsx └── layout.jsx ├── package.json ├── utils └── auth.js ├── CONTRIBUTING.md └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | # MONGODB_URI= 2 | # DB_NAME= -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = { 4 | env: { 5 | MONGODB_URI: process.env.MONGODB_URI, 6 | CLOUDINARY_URL: process.env.CLOUDINARY_URL, 7 | DB_NAME: process.env.DB_NAME, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-mongo-crud", 3 | "version": 1, 4 | "build": { 5 | "env": { 6 | "MONGODB_URI": "@mongodb_uri", 7 | "CLOUDINARY_URL": "@cloudinary_url", 8 | "DB_NAME": "@db_name" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /middlewares/session.js: -------------------------------------------------------------------------------- 1 | import session from 'next-session'; 2 | import connectMongo from 'connect-mongo'; 3 | 4 | const MongoStore = connectMongo(session); 5 | 6 | export default function (req, res, next) { 7 | return session({ store: new MongoStore({ client: req.dbClient }) })(req, res, next); 8 | } 9 | -------------------------------------------------------------------------------- /middlewares/middleware.js: -------------------------------------------------------------------------------- 1 | import nextConnect from 'next-connect'; 2 | import database from './database'; 3 | import session from './session'; 4 | import authentication from './authentication'; 5 | 6 | const middleware = nextConnect(); 7 | 8 | middleware.use(database); 9 | middleware.use(session); 10 | middleware.use(authentication); 11 | 12 | export default middleware; 13 | -------------------------------------------------------------------------------- /middlewares/authentication.js: -------------------------------------------------------------------------------- 1 | import { ObjectId } from 'mongodb'; 2 | 3 | export default function authentication(req, res, next) { 4 | if (req.session.userId) { 5 | return req.db.collection('users').findOne(ObjectId(req.session.userId)) 6 | .then((user) => { 7 | if (user) req.user = user; 8 | return next(); 9 | }); 10 | } 11 | return next(); 12 | } 13 | -------------------------------------------------------------------------------- /lib/redirectTo.js: -------------------------------------------------------------------------------- 1 | import Router from 'next/router'; 2 | 3 | export default function redirectTo(destination, { res, status } = {}) { 4 | if (res) { 5 | res.writeHead(status || 302, { Location: destination }); 6 | res.end(); 7 | } else if (destination[0] === '/' && destination[1] !== '/') { 8 | Router.push(destination); 9 | } else { 10 | window.location = destination; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pages/_app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from 'next/app'; 3 | // import { UserContextProvider } from '../components/UserContext'; 4 | 5 | class MyApp extends App { 6 | render() { 7 | const { Component, pageProps } = this.props; 8 | return ( 9 | // 10 | 11 | // 12 | 13 | ); 14 | } 15 | } 16 | 17 | export default MyApp; 18 | -------------------------------------------------------------------------------- /middlewares/database.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | 3 | const client = new MongoClient(process.env.MONGODB_URI, { 4 | useNewUrlParser: true, 5 | useUnifiedTopology: true, 6 | }); 7 | 8 | export default function database(req, res, next) { 9 | if (!client.isConnected()) { 10 | return client.connect().then(() => { 11 | req.dbClient = client; 12 | req.db = client.db(process.env.DB_NAME); 13 | return next(); 14 | }); 15 | } 16 | req.dbClient = client; 17 | req.db = client.db(process.env.DB_NAME); 18 | return next(); 19 | } 20 | -------------------------------------------------------------------------------- /pages/api/user/index.js: -------------------------------------------------------------------------------- 1 | import nextConnect from 'next-connect'; 2 | import middleware from '../../../middlewares/middleware'; 3 | 4 | const handler = nextConnect(); 5 | 6 | handler.use(middleware); 7 | 8 | handler.patch((req, res) => { 9 | if (!req.user) return res.status(401).send('You need to be logged in.'); 10 | const { name, bio } = req.body; 11 | return req.db 12 | .collection('users') 13 | .updateOne({ _id: req.user._id }, { $set: { name, bio } }) 14 | .then(() => res.json({ 15 | message: 'Profile updated successfully', 16 | data: { name, bio }, 17 | })) 18 | .catch(error => res.send({ 19 | status: 'error', 20 | message: error.toString(), 21 | })); 22 | }); 23 | 24 | export default handler; 25 | -------------------------------------------------------------------------------- /pages/api/session.js: -------------------------------------------------------------------------------- 1 | import nextConnect from 'next-connect'; 2 | import middleware from '../../middlewares/middleware'; 3 | 4 | const handler = nextConnect(); 5 | 6 | handler.use(middleware); 7 | 8 | handler.get((req, res) => { 9 | if (req.user) { 10 | const { 11 | name, email, bio, profilePicture, gender 12 | } = req.user; 13 | return res.status(200).send({ 14 | status: 'ok', 15 | data: { 16 | isLoggedIn: true, 17 | user: { 18 | name, email, bio, profilePicture, gender 19 | }, 20 | }, 21 | }); 22 | } 23 | return res.status(200).send({ 24 | status: 'ok', 25 | data: { 26 | isLoggedIn: false, 27 | user: {}, 28 | }, 29 | }); 30 | }); 31 | 32 | handler.delete((req, res) => { 33 | delete req.session.userId; 34 | return res.status(200).send({ 35 | status: 'ok', 36 | message: 'You have been logged out.', 37 | }); 38 | }); 39 | 40 | export default handler; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hoang Vo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pages/api/user/profilepicture.js: -------------------------------------------------------------------------------- 1 | import nextConnect from 'next-connect'; 2 | import formidable from 'formidable'; 3 | import { v2 as cloudinary } from 'cloudinary'; 4 | import middleware from '../../../middlewares/middleware'; 5 | 6 | const handler = nextConnect(); 7 | 8 | handler.use(middleware); 9 | 10 | handler.put((req, res) => { 11 | if (!req.user) return res.status(401).send('You need to be logged in.'); 12 | const form = new formidable.IncomingForm(); 13 | return form.parse(req, (err, fields, files) => cloudinary.uploader 14 | .upload(files.profilePicture.path, { 15 | width: 512, 16 | height: 512, 17 | crop: 'fill', 18 | }) 19 | .then(image => req.db 20 | .collection('users') 21 | .updateOne( 22 | { _id: req.user._id }, 23 | { $set: { profilePicture: image.secure_url } }, 24 | )) 25 | .then(() => res.send({ 26 | status: 'success', 27 | message: 'Profile picture updated successfully', 28 | })) 29 | .catch(error => res.send({ 30 | status: 'error', 31 | message: error.toString(), 32 | }))); 33 | }); 34 | 35 | export const config = { 36 | api: { 37 | bodyParser: false, 38 | }, 39 | }; 40 | 41 | export default handler; 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # package-lock 64 | package-lock.json 65 | yarn.lock 66 | -------------------------------------------------------------------------------- /pages/api/authenticate.js: -------------------------------------------------------------------------------- 1 | import nextConnect from 'next-connect'; 2 | import bcrypt from 'bcryptjs'; 3 | import middleware from '../../middlewares/middleware'; 4 | import jwt from 'jsonwebtoken'; 5 | const handler = nextConnect(); 6 | 7 | handler.use(middleware); 8 | 9 | handler.post((req, res) => { 10 | const { email, password } = req.body; 11 | 12 | return req.db 13 | .collection('users') 14 | .findOne({ email }) 15 | .then((user) => { 16 | if (user) { 17 | return bcrypt.compare(password, user.password).then((result) => { 18 | if (result) return Promise.resolve(user); 19 | return Promise.reject(Error('The password you entered is incorrect')); 20 | }); 21 | } 22 | return Promise.reject(Error('The email does not exist')); 23 | }) 24 | .then((user) => { 25 | req.session.userId = user._id; 26 | delete user.password; 27 | const token = jwt.sign({ "email": user.email, "_id": user._id }, process.env.jwtSecret, { expiresIn: 86400 }) // 1 day token 28 | return res.send({ 29 | status: 'ok', 30 | userData: user, 31 | token: token 32 | }); 33 | }) 34 | .catch(error => res.send({ 35 | status: 'error', 36 | message: error.toString(), 37 | })); 38 | }); 39 | 40 | export default handler; 41 | -------------------------------------------------------------------------------- /pages/api/users.js: -------------------------------------------------------------------------------- 1 | import nextConnect from 'next-connect'; 2 | import isEmail from 'validator/lib/isEmail'; 3 | import bcrypt from 'bcryptjs'; 4 | import middleware from '../../middlewares/middleware'; 5 | 6 | const handler = nextConnect(); 7 | 8 | handler.use(middleware); 9 | 10 | handler.post((req, res) => { 11 | const { email, name, password, gender } = req.body; 12 | if (!isEmail(email)) { 13 | return res.send({ 14 | status: 'error', 15 | message: 'The email you entered is invalid.', 16 | }); 17 | } 18 | return req.db 19 | .collection('users') 20 | .countDocuments({ email }) 21 | .then((count) => { 22 | if (count) { 23 | return Promise.reject(Error('The email has already been used.')); 24 | } 25 | return bcrypt.hashSync(password); 26 | }) 27 | .then(hashedPassword => req.db.collection('users').insertOne({ 28 | email, 29 | password: hashedPassword, 30 | name, 31 | gender 32 | })) 33 | .then((user) => { 34 | req.session.userId = user.insertedId; 35 | res.status(201).send({ 36 | status: 'ok', 37 | message: 'User signed up successfully', 38 | }); 39 | }) 40 | .catch(error => res.send({ 41 | status: 'error', 42 | message: error.toString(), 43 | })); 44 | }); 45 | 46 | export default handler; 47 | -------------------------------------------------------------------------------- /components/CustomerContext.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useReducer, useEffect, useContext } from 'react'; 2 | import axios from 'axios'; 3 | import ListPage from '../pages/customer/list'; 4 | 5 | const CustomerContext = createContext(); 6 | 7 | const reducer = (state, action) => { 8 | switch (action.type) { 9 | case 'set': 10 | return action.data; 11 | case 'clear': 12 | return { 13 | data: [], 14 | }; 15 | default: 16 | throw new Error(); 17 | } 18 | }; 19 | 20 | const CustomerContextProvider = ({ children }) => { 21 | const [state, dispatch] = useReducer(reducer, { data: [] }); 22 | const dispatchProxy = (action) => { 23 | switch (action.type) { 24 | case 'fetch': 25 | return axios.get('/api/customer') 26 | .then(res => ({ 27 | data: res.data, 28 | })) 29 | .then((data) => { 30 | dispatch({ 31 | type: 'set', 32 | data: data, 33 | }); 34 | }); 35 | default: 36 | return dispatch(action); 37 | } 38 | }; 39 | useEffect(() => { 40 | dispatchProxy({ type: 'fetch' }); 41 | }, []); 42 | return ( 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | 50 | const CustomerContextConsumer = CustomerContext.Consumer; 51 | 52 | export { CustomerContext, CustomerContextProvider, CustomerContextConsumer }; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-mongo-crud", 3 | "version": "1.0.0", 4 | "description": "Full fledged app made with Next.JS and MongoDB", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/tejasrana95/nextjs-mongo-crud.git" 14 | }, 15 | "author": "Tejas Rana (https://www.tejasrana.com)", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/tejasrana95/nextjs-mongo-crud/issues" 19 | }, 20 | "homepage": "https://github.com/tejasrana95/nextjs-mongo-crud#readme", 21 | "dependencies": { 22 | "axios": "^0.21.1", 23 | "axioswal": "^1.0.1", 24 | "bcryptjs": "^2.4.3", 25 | "cloudinary": "^1.25.1", 26 | "connect-mongo": "^4.4.1", 27 | "dotenv": "^8.2.0", 28 | "express-session": "^1.17.1", 29 | "formidable": "^1.2.2", 30 | "js-cookie": "^2.2.1", 31 | "jsonwebtoken": "^8.5.1", 32 | "mongodb": "^3.6.5", 33 | "next": "^10.0.9", 34 | "next-connect": "^0.10.1", 35 | "next-cookies": "^2.0.3", 36 | "next-session": "^3.4.0", 37 | "react": "^17.0.2", 38 | "react-dom": "^17.0.2", 39 | "validator": "^13.5.2" 40 | }, 41 | "devDependencies": { 42 | "eslint": "^7.23.0", 43 | "eslint-config-airbnb": "^18.2.1", 44 | "eslint-plugin-import": "^2.22.1", 45 | "eslint-plugin-jsx-a11y": "^6.4.1", 46 | "eslint-plugin-react": "^7.23.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /components/UserContext.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useReducer, useEffect } from 'react'; 2 | import axios from 'axios'; 3 | 4 | const UserContext = createContext(); 5 | 6 | const reducer = (state, action) => { 7 | switch (action.type) { 8 | case 'set': 9 | return action.data; 10 | case 'clear': 11 | return { 12 | isLoggedIn: false, 13 | user: {}, 14 | }; 15 | default: 16 | throw new Error(); 17 | } 18 | }; 19 | 20 | const UserContextProvider = ({ children }) => { 21 | const [state, dispatch] = useReducer(reducer, { isLoggedIn: false, user: {} }); 22 | const dispatchProxy = (action) => { 23 | switch (action.type) { 24 | case 'fetch': 25 | return axios.get('/api/session') 26 | .then(res => ({ 27 | isLoggedIn: res.data.data.isLoggedIn, 28 | user: res.data.data.user, 29 | })) 30 | .then(({ isLoggedIn, user }) => { 31 | dispatch({ 32 | type: 'set', 33 | data: { isLoggedIn, user }, 34 | }); 35 | }); 36 | default: 37 | return dispatch(action); 38 | } 39 | }; 40 | useEffect(() => { 41 | dispatchProxy({ type: 'fetch' }); 42 | }, []); 43 | return ( 44 | 45 | { children } 46 | 47 | ); 48 | }; 49 | 50 | const UserContextConsumer = UserContext.Consumer; 51 | 52 | export { UserContext, UserContextProvider, UserContextConsumer }; 53 | -------------------------------------------------------------------------------- /pages/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | // import { UserContext } from '../components/UserContext'; 3 | // import Layout from '../components/layout'; 4 | 5 | const IndexPage = () => { 6 | // const { state: { isLoggedIn, user: { name } } } = useContext(UserContext); 7 | return ( 8 | // 9 | 10 | 11 | // 12 |
13 | 31 |

32 | Hello, 33 | {/* {' '} 34 | {(isLoggedIn ? name : 'stranger')} 35 | ! */} 36 |

37 |

Have a wonderful day.

38 |
39 |

40 | To start using this starter kit, please uncomment every commented line in _app.jsx and index.jsx. 41 |
42 | Then the screen show you an error code: 500 in /api/session. 43 |
44 | Don't panic, please just configure the connection with your database in .env file and you can connect with your choice of database. 45 |

46 |
47 |
48 | ); 49 | }; 50 | 51 | export default IndexPage; 52 | -------------------------------------------------------------------------------- /pages/profile/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import Link from 'next/link'; 3 | import Layout from '../../components/layout'; 4 | import { withAuthSync, getUserData } from '../../utils/auth'; 5 | 6 | 7 | class ProfilePage extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | getUser: {} 13 | }; 14 | } 15 | 16 | componentDidMount() { 17 | this.setState({ getUser: getUserData() }); 18 | } 19 | 20 | render() { 21 | return ( 22 | 23 | 32 |

Profile

33 |
34 | {this.state.getUser.name} 35 |

36 | Name: 37 | {' '} 38 | {this.state.getUser.name} 39 |

40 |

41 | Bio: 42 | {' '} 43 | {this.state.getUser.bio} 44 |

45 |

46 | Email: 47 | {' '} 48 | {this.state.getUser.email} 49 |

50 |

51 | Gender: 52 | {' '} 53 | {this.state.getUser.gender} 54 |

55 |
56 | 57 |
58 | ); 59 | } 60 | } 61 | 62 | export default withAuthSync(ProfilePage); -------------------------------------------------------------------------------- /pages/login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react'; 2 | import axioswal from 'axioswal'; 3 | import { UserContext } from '../components/UserContext'; 4 | import Layout from '../components/layout'; 5 | import redirectTo from '../lib/redirectTo'; 6 | import { login } from '../utils/auth' 7 | const LoginPage = () => { 8 | const { dispatch } = useContext(UserContext); 9 | const [email, setEmail] = useState(''); 10 | const [password, setPassword] = useState(''); 11 | 12 | const handleSubmit = (event) => { 13 | event.preventDefault(); 14 | axioswal 15 | .post('/api/authenticate', { 16 | email, 17 | password, 18 | }) 19 | .then((data) => { 20 | if (data.status === 'ok') { 21 | const token = data.token; 22 | login(token, data.userData); 23 | } 24 | }); 25 | }; 26 | 27 | return ( 28 | 29 |
30 |

Log in

31 |
32 |
33 | setEmail(e.target.value)} 38 | /> 39 |
40 |
41 | setPassword(e.target.value)} 46 | /> 47 |
48 | 51 |
52 |
53 |
54 | ); 55 | }; 56 | 57 | export default LoginPage; 58 | -------------------------------------------------------------------------------- /pages/api/customer.js: -------------------------------------------------------------------------------- 1 | import nextConnect from 'next-connect'; 2 | import isEmail from 'validator/lib/isEmail'; 3 | import middleware from '../../middlewares/middleware'; 4 | 5 | const handler = nextConnect(); 6 | 7 | handler.use(middleware); 8 | 9 | handler.post((req, res) => { 10 | if (!req.user) return res.status(401).send('You need to be logged in.'); 11 | const { email, name, phone } = req.body; 12 | if (!isEmail(email)) { 13 | return res.status(400).send({ 14 | status: 'error', 15 | message: 'The email you entered is invalid.', 16 | }); 17 | } 18 | return req.db 19 | .collection('customers') 20 | .countDocuments({ email }) 21 | .then((count) => { 22 | if (count) { 23 | return Promise.reject( 24 | res.status(200).send({ 25 | status: 'error', 26 | message: 'The customer with email has already been used', 27 | }) 28 | ); 29 | } 30 | }) 31 | .then(() => req.db.collection('customers').insertOne({ 32 | email, 33 | phone, 34 | name 35 | })) 36 | .then((user) => { 37 | req.session.userId = user.insertedId; 38 | res.status(201).send({ 39 | status: 'ok', 40 | message: 'Customer added successfully', 41 | }); 42 | }) 43 | .catch(error => res.send({ 44 | status: 'error', 45 | message: error.toString(), 46 | })); 47 | }); 48 | 49 | 50 | handler.get((req, res) => { 51 | if (!req.user) return res.status(401).send('You need to be logged in.'); 52 | return req.db 53 | .collection('customers') 54 | .find({}) 55 | .toArray() 56 | .then((data) => res.json(data)) 57 | .catch(error => { 58 | return Promise.reject( 59 | res.status(200).send({ 60 | status: 'error', 61 | message: error.error, 62 | }) 63 | ) 64 | }); 65 | }); 66 | 67 | export default handler; 68 | -------------------------------------------------------------------------------- /pages/customer/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react'; 2 | import axios from 'axios'; 3 | import Layout from '../../components/layout'; 4 | import { UserContext } from '../../components/UserContext'; 5 | import Swal from 'sweetalert2'; 6 | import TableExp from './listData'; 7 | import CustomerForm from './customerForm'; 8 | import redirectTo from '../../lib/redirectTo'; 9 | import { withAuthSync } from '../../utils/auth'; 10 | 11 | class CustomerPage extends React.Component { 12 | updateTimeStamp = {}; 13 | static contextType = UserContext; 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | updateTimeStamp: 0, 18 | id: null 19 | }; 20 | this.updateList = this.updateList.bind(this); 21 | this.edit = this.edit.bind(this); 22 | } 23 | 24 | 25 | 26 | updateList(updateTime) { 27 | this.setState({ updateTimeStamp: updateTime }); 28 | } 29 | 30 | componentDidMount() { 31 | // setTimeout(() => { 32 | // if (!this.context.state.isLoggedIn) { 33 | // redirectTo('/login'); 34 | // } 35 | // }, 2000); 36 | 37 | } 38 | 39 | edit(id) { 40 | this.setState({ id: id }); 41 | } 42 | 43 | render() { 44 | 45 | return ( 46 | 47 | 56 |

Customer

57 |
58 | 59 |
60 | 61 |
62 | 63 | 64 |
65 |
66 | ); 67 | } 68 | } 69 | 70 | export default withAuthSync(CustomerPage); 71 | 72 | -------------------------------------------------------------------------------- /utils/auth.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import Router from 'next/router' 3 | import nextCookie from 'next-cookies' 4 | import cookie from 'js-cookie' 5 | 6 | export const login = (token, userData) => { 7 | cookie.set('token', token, { expires: 1 }) 8 | setUserData(userData); 9 | Router.push('/profile'); 10 | } 11 | 12 | export const auth = ctx => { 13 | const { token } = nextCookie(ctx) 14 | 15 | // If there's no token, it means the user is not logged in. 16 | if (!token) { 17 | if (typeof window === 'undefined') { 18 | ctx.res.writeHead(302, { Location: '/login' }) 19 | ctx.res.end() 20 | } else { 21 | Router.push('/login') 22 | } 23 | } 24 | 25 | return token 26 | } 27 | 28 | export const logout = () => { 29 | cookie.remove('token') 30 | // to support logging out from all windows 31 | window.localStorage.setItem('logout', Date.now()) 32 | Router.push('/login') 33 | } 34 | 35 | export const setUserData = (userData) => { 36 | window.localStorage.setItem('userData', JSON.stringify(userData)); 37 | } 38 | 39 | export const getUserData = () => { 40 | const userData = window.localStorage.getItem('userData'); 41 | return JSON.parse(userData); 42 | } 43 | 44 | export const withAuthSync = WrappedComponent => { 45 | const Wrapper = props => { 46 | const syncLogout = event => { 47 | if (event.key === 'logout') { 48 | console.log('logged out from storage!') 49 | Router.push('/login') 50 | } 51 | } 52 | 53 | useEffect(() => { 54 | window.addEventListener('storage', syncLogout) 55 | 56 | return () => { 57 | window.removeEventListener('storage', syncLogout) 58 | window.localStorage.removeItem('logout') 59 | } 60 | }, []) 61 | 62 | return 63 | } 64 | 65 | Wrapper.getInitialProps = async ctx => { 66 | const token = auth(ctx) 67 | 68 | const componentProps = 69 | WrappedComponent.getInitialProps && 70 | (await WrappedComponent.getInitialProps(ctx)) 71 | 72 | return { ...componentProps, token } 73 | } 74 | 75 | return Wrapper 76 | } 77 | -------------------------------------------------------------------------------- /pages/signup.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react'; 2 | import axioswal from 'axioswal'; 3 | import { UserContext } from '../components/UserContext'; 4 | import Layout from '../components/layout'; 5 | import redirectTo from '../lib/redirectTo'; 6 | 7 | const SignupPage = () => { 8 | const { dispatch } = useContext(UserContext); 9 | const [name, setName] = useState(''); 10 | const [email, setEmail] = useState(''); 11 | const [password, setPassword] = useState(''); 12 | const [gender, setGender] = useState(''); 13 | 14 | const handleSubmit = (event) => { 15 | event.preventDefault(); 16 | axioswal 17 | .post('/api/users', { 18 | name, 19 | email, 20 | password, 21 | gender 22 | }) 23 | .then((data) => { 24 | if (data.status === 'ok') { 25 | dispatch({ type: 'fetch' }); 26 | redirectTo('/'); 27 | } 28 | }); 29 | }; 30 | 31 | return ( 32 | 33 |
34 |

Sign up

35 |
36 |
37 | setName(e.target.value)} 42 | /> 43 |
44 |
45 | setEmail(e.target.value)} 50 | /> 51 |
52 |
53 | setPassword(e.target.value)} 58 | /> 59 |
60 |
61 | 70 | 79 |
80 | 83 |
84 |
85 |
86 | ); 87 | }; 88 | 89 | export default SignupPage; 90 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to nextjs-mongodb-app 2 | 3 | :+1::tada: Thank you for being here. It is people like you that make `nextjs-mongodb-app` great and help shape a better open-source community. 4 | 5 | Following this guideline improves communication and organization, which helps save your and other developers' times and effort in future development. 6 | 7 | ## What `nextjs-mongodb-app` is looking for 8 | 9 | I welcome all contributions from the community. There are many ways to contribute: 10 | 11 | - :art: Submit PR to fix bugs, enhance/add existed/new features. 12 | - :children_crossing: Submit bug reports and feature requests. 13 | - :pencil: Improve documentation and writing examples. 14 | 15 | However, please avoid using the issue tracker for support questions. You can receive help on my [Spectrum community](https://spectrum.chat/luvbitstudio). 16 | 17 | ## How to contribute 18 | 19 | ### Bug reports 20 | 21 | If you are submitting a :bug: bug report, please: 22 | - Use a clear and descriptive title. Describe the behavior you observed and the expected behavior. 23 | - Describe the exact steps which reproduce the problem. A minimal reproduction repo is greatly appreciated. 24 | - Include Node version, OS, or other information that may be helpful in the troubleshooting. 25 | 26 | ### Process on submitting a PR 27 | 28 | *Generally, all pull requests should have references to an issue.* 29 | 30 | If you are :sparkles: **adding a new feature** or :zap: **improving an algorithm**, please first [create an issue](../../issues/new) for discussion. 31 | 32 | The steps to submit a PR are: 33 | 34 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. 35 | 36 | 2. Install all dependencies and dev dependencies by `npm install`. 37 | 38 | 3. Make changes and commit (following [commit message styleguides](#commit-message)). 39 | 40 | 4. Make sure your code is linted by running `npm run lint`. 41 | 42 | 5. [Create a pull request](https://help.github.com/en/articles/creating-a-pull-request-from-a-fork) 43 | 44 | ## Styleguides 45 | 46 | ### Javascript style 47 | 48 | `nextjs-mongodb-app` follows [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript). Please run `npm run lint` and fix any linting warnings. 49 | 50 | ### Commit message 51 | 52 | - Use the present tense and imperative mood ("Add feature" instead of "Adds feature" or "Added feature") 53 | - Consider starting the commit message with an applicable emoji (ex. [gitmoji](https://gitmoji.carloscuesta.me)) for a more beautiful world :rainbow:. 54 | 55 | :heart: Thank you, 56 | Hoang Vo 57 | -------------------------------------------------------------------------------- /pages/profile/settings.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import axioswal from 'axioswal'; 3 | import { UserContext } from '../../components/UserContext'; 4 | import Layout from '../../components/layout'; 5 | 6 | const ProfileSection = ({ user: { name: initialName, bio: initialBio }, dispatch }) => { 7 | const [name, setName] = useState(initialName); 8 | const [bio, setBio] = useState(initialBio); 9 | 10 | const handleSubmit = (event) => { 11 | event.preventDefault(); 12 | axioswal 13 | .patch( 14 | '/api/user', 15 | { name, bio }, 16 | ) 17 | .then(() => { 18 | dispatch({ type: 'fetch' }); 19 | }); 20 | }; 21 | 22 | const profilePictureRef = React.createRef(); 23 | const [isUploading, setIsUploading] = useState(false); 24 | 25 | const handleSubmitProfilePicture = (event) => { 26 | if (isUploading) return; 27 | event.preventDefault(); 28 | setIsUploading(true); 29 | const formData = new FormData(); 30 | formData.append('profilePicture', profilePictureRef.current.files[0]); 31 | axioswal 32 | .put('/api/user/profilepicture', formData) 33 | .then(() => { 34 | setIsUploading(false); 35 | dispatch({ type: 'fetch' }); 36 | }); 37 | }; 38 | 39 | return ( 40 | <> 41 | 48 |
49 |

Edit Profile

50 |
51 | 62 |