├── .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 |
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 | Edit
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 |
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 |
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 |
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 |
93 | >
94 | );
95 | };
96 |
97 | const SettingPage = () => {
98 | const { state: { isLoggedIn, user }, dispatch } = useContext(UserContext);
99 |
100 | if (!isLoggedIn) return (Please log in
);
101 | return (
102 |
103 | Settings
104 |
105 |
106 | );
107 | };
108 |
109 | export default SettingPage;
110 |
--------------------------------------------------------------------------------
/pages/customer/listData.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import axios from 'axios';
3 | import Swal from 'sweetalert2';
4 | class TableExp extends React.Component {
5 |
6 | constructor(props) {
7 |
8 | super();
9 | this.state = {
10 | tableData: [{
11 | srNo: '',
12 | _id: '',
13 | name: '',
14 | email: '',
15 | phone: '',
16 |
17 | }],
18 | lastUpdate: 0
19 | };
20 | }
21 |
22 | UNSAFE_componentWillReceiveProps(nextProps) {
23 | if (this.state.lastUpdate !== nextProps.update) {
24 | this.componentDidMount();
25 | this.setState({ lastUpdate: nextProps.update });
26 | }
27 | }
28 |
29 | componentDidMount() {
30 | axios.get('/api/customer', {
31 | responseType: 'json'
32 | }).then(response => {
33 | this.setState({ tableData: response.data });
34 | });
35 | }
36 |
37 | edit(id) {
38 | this.props.edit(id);
39 | }
40 |
41 | delete(id) {
42 | Swal.fire({
43 | type: 'question',
44 | title: 'Are you sure',
45 | text: "Are you sure to delete this?",
46 | timer: 3000,
47 | showCancelButton: true
48 | }).then(data => {
49 | if (data.value === true) {
50 | this.deleteApi(id)
51 | }
52 | })
53 | }
54 |
55 | deleteApi(id) {
56 | axios.delete('/api/customer/' + id, {
57 | responseType: 'json'
58 | }).then(response => {
59 | this.componentDidMount();
60 | });
61 |
62 | }
63 |
64 | render() {
65 |
66 | const { tableData } = this.state;
67 | return (
68 | <>
69 |
70 |
85 | List
86 |
87 |
88 |
89 |
90 | Sr.No
91 | ID
92 | Name
93 | Email
94 | Phone
95 | Action
96 |
97 |
98 |
99 | {tableData.map((subData, index) => (
100 |
101 | {index + 1}
102 | {subData._id}
103 | {subData.name}
104 | {subData.email}
105 | {subData.phone}
106 |
107 | this.edit(subData._id)} >
108 |
109 |
110 |
111 | this.delete(subData._id)} >
112 |
113 |
114 |
115 |
116 |
117 |
118 | ))}
119 |
120 |
121 |
122 |
123 | >
124 | );
125 | }
126 | }
127 |
128 | export default TableExp;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Next.js + MongoDB + Basic Crud Operation
2 |
3 | A full-fledged app made with Next.JS and MongoDB.
4 |
5 | ## About this project
6 |
7 | `nextjs-mongo-crud` is a continously developed app built with Next.JS and MongoDB. Most tutorials on the Internet are either _half-baked_ or _not production-ready_. This project aims to fix that.
8 |
9 | This project goes even further and attempts to integrate top features as seen in real-life apps.
10 |
11 | Give this project a big ol' 🌟 star motivates me to work on new features.
12 |
13 | Check out my profile (https://tejasrana.com/).
14 |
15 | ## Using this project
16 |
17 | The goal is not to use this project as it, but to implement your own version.
18 |
19 | ### Requirement
20 |
21 | This project relies on the following components. Some of them are **optional** and some may be replaced by others with similar functionalities.
22 |
23 | #### Dependencies
24 |
25 | This project uses the following dependencies:
26 |
27 | - `next.js` - v9 or above required for **API Routes**.
28 | - `react` - v16.8 or above required for **react hooks**.
29 | - `react-dom` - v16.8 or above.
30 | - `mongodb` - may be replaced by `mongoose`.
31 | - `next-connect` - recommended if you want to use Express/Connect middleware.
32 | - `axios`, `axioswal` - optional, may be replaced with any request library.
33 | - `next-session`, `connect-mongo` - may be replaced with any session management solution.
34 | - `bcryptjs` - optional, may be replaced with any password-hashing library. `argon2` recommended.
35 | - `validator` - optional but recommended.
36 | - `formidable` - may be replaced by other file parser.
37 | - `cloudinary` - optional, only if you are using [Cloudinary](https://cloudinary.com) for image upload.
38 |
39 | #### Environmental variables
40 |
41 | The environment variables [will be inlined during build time](https://nextjs.org/docs#build-time-configuration) and thus should not be used in front-end codebase.
42 |
43 | Required environmental variables in this project include:
44 |
45 | - `process.env.MONGODB_URI` The MongoDB Connection String (with credentials)
46 | - `process.env.CLOUDINARY_URL` Cloudinary environment variable for configuration. See [this](https://cloudinary.com/documentation/node_integration#configuration "Cloudinary Configuration").
47 | - `process.env.DB_NAME` The name of the MongoDB database to be used.
48 |
49 | I include my own MongoDB and Cloudinary environment variables in [.env.example](.env.example) for experimentation purposes. Please replace them with your owns and refrain from sabotaging them. You can use them in development by renaming it into `.env`.
50 |
51 | In production, it is recommended to set the environment variables using the options provided by your cloud/hosting providers.
52 |
53 | ## Development
54 |
55 | `nextjs-mongo-crud` is a long-term developing project. There is no constraint on numbers of features. I continuously accepts feature proposals and am actively developing and expanding functionalities.
56 |
57 | Start the development server by running `yarn dev` or `npm run dev`.
58 |
59 | ### Features
60 |
61 | There are three states in feature development:
62 |
63 | - `developed`: The feature has been fully developed and is functional.
64 | - `developing`: The feature is being developed or being improved.
65 | - `proposed`: The feature is proposed and may or may not be developed in the future.
66 |
67 | #### Authentication
68 |
69 | - Session management
70 | - Allow users to sign up and log in/log out.
71 |
72 | #### User profile
73 |
74 | - Avatar, name, email, location, etc.
75 | - User profile page
76 | - Edit user profile
77 |
78 | #### Social `delayed`
79 |
80 | - Find other users with search functionality
81 | - View other users' profile page
82 | - Add/Remove friends
83 |
84 | #### Account management `developing`
85 |
86 | - Email verification
87 | - Password change
88 | - Password reset
89 |
90 | Have any features in mind, [make an issue](https://github.com/tejasrana95/nextjs-mongo-crud/issues). Would like to work on a feature, [make a PR](https://github.com/tejasrana95/nextjs-mongo-crud/pulls).
91 |
92 | ### Styles
93 |
94 | Despite the look, this project does not contain any stylesheets, and no component has classes. To remove the style, simply remove all `
63 |
106 |
107 |
108 | Next.js First POC
109 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | Next.js First POC
124 |
125 |
126 | {(!isLoggedIn ? (
127 | <>
128 |
Login
129 |
Sign up
130 | >
131 | ) : (
132 | <>
133 |
Customer
134 |
Profile
135 | {/* eslint-disable-next-line */}
136 |
Logout
137 | >
138 | ))}
139 |
140 |
141 |
142 |
143 |
144 | {children}
145 |
146 |
147 | >
148 | );
149 | };
150 |
151 | export default layout;
152 |
--------------------------------------------------------------------------------
/pages/customer/customerForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react';
2 | import axios from 'axios';
3 | import Layout from '../../components/layout';
4 | import Swal from 'sweetalert2';
5 | class CustomerForm extends React.Component {
6 |
7 | constructor(props) {
8 |
9 | super(props);
10 |
11 | this.initialState = {
12 | _id: '',
13 | name: '',
14 | email: '',
15 | phone: ''
16 | }
17 |
18 | this.state = {
19 | _id: '',
20 | name: '',
21 | email: '',
22 | phone: ''
23 | }
24 |
25 | this.handleSubmit = this.handleSubmit.bind(this);
26 | }
27 |
28 | UNSAFE_componentWillReceiveProps(nextProps) {
29 | if (nextProps.edit !== null) {
30 | this.getById(nextProps.edit);
31 | }
32 | }
33 |
34 | getById(id) {
35 | axios.get('/api/customer/' + id).then((data) => {
36 | this.setState({
37 | _id: data.data._id,
38 | name: data.data.name,
39 | email: data.data.email,
40 | phone: data.data.phone
41 | });
42 |
43 | }).catch(error => {
44 | console.error(error);
45 | });
46 | return;
47 | }
48 |
49 | handleSubmit(event) {
50 | event.preventDefault();
51 | if (this.state._id === '') {
52 | this.createSubmit();
53 | } else {
54 | this.updateSubmit();
55 | }
56 | }
57 |
58 |
59 | createSubmit() {
60 | axios.post('/api/customer', this.state).then((data) => {
61 |
62 | if (data.statusText === 'Created') {
63 | Swal.fire({
64 | type: 'success',
65 | title: 'Success',
66 | text: 'Customer Added successfully',
67 | timer: 1000,
68 | toast: true
69 | });
70 | this.cancelCustomer();
71 | } else {
72 | Swal.fire({
73 | type: 'error',
74 | title: 'Error',
75 | text: data.data.message,
76 | timer: 3000
77 | })
78 | }
79 | this.props.updateList(new Date().getTime());
80 | });
81 | return;
82 | }
83 |
84 |
85 | updateSubmit() {
86 | axios.put('/api/customer/' + this.state._id, this.state).then((data) => {
87 | Swal.fire({
88 | type: 'success',
89 | title: 'Success',
90 | text: 'Customer updated successfully',
91 | timer: 1000,
92 | toast: true
93 | });
94 | this.cancelCustomer();
95 | this.props.updateList(new Date().getTime());
96 | }).catch(error => {
97 | Swal.fire({
98 | type: 'error',
99 | title: 'Error',
100 | text: error.response.data.message,
101 | timer: 3000,
102 | toast: false
103 | });
104 | });
105 | return;
106 | }
107 |
108 | cancelCustomer = () => {
109 | setTimeout(() => {
110 | this.setState(this.initialState);
111 | }, 1000);
112 |
113 | }
114 |
115 | componentDidMount() {
116 | this.setState(this.initialState);
117 | }
118 |
119 | render() {
120 | return (
121 | <>
122 |
129 |
130 |
131 |
149 | Save
150 |
151 |
152 |
153 | >
154 | )
155 | }
156 | }
157 |
158 | export default CustomerForm;
159 |
--------------------------------------------------------------------------------