├── .gitignore ├── README.md ├── backend ├── .env.example ├── app.js ├── controllers │ ├── authControllers.js │ ├── profileControllers.js │ └── taskControllers.js ├── middlewares.js │ └── index.js ├── models │ ├── Task.js │ └── User.js ├── package-lock.json ├── package.json ├── routes │ ├── authRoutes.js │ ├── profileRoutes.js │ └── taskRoutes.js └── utils │ ├── token.js │ └── validation.js ├── frontend ├── .gitignore ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ └── index.html ├── src │ ├── App.jsx │ ├── api │ │ └── index.jsx │ ├── components │ │ ├── LoginForm.jsx │ │ ├── Navbar.jsx │ │ ├── SignupForm.jsx │ │ ├── Tasks.jsx │ │ └── utils │ │ │ ├── Input.jsx │ │ │ ├── Loader.jsx │ │ │ └── Tooltip.jsx │ ├── hooks │ │ └── useFetch.jsx │ ├── index.css │ ├── index.js │ ├── layouts │ │ └── MainLayout.jsx │ ├── pages │ │ ├── Home.jsx │ │ ├── Login.jsx │ │ ├── NotFound.jsx │ │ ├── Signup.jsx │ │ └── Task.jsx │ ├── redux │ │ ├── actions │ │ │ ├── actionTypes.js │ │ │ └── authActions.js │ │ ├── reducers │ │ │ ├── authReducer.js │ │ │ └── index.js │ │ └── store.js │ └── validations │ │ └── index.js └── tailwind.config.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MERN Task Manager 2 | 3 | A MERN application for basic tasks management. 4 | ![image](https://user-images.githubusercontent.com/86913048/227101123-f8a35258-9c21-4479-86e8-055659ab75e2.png) 5 | 6 | ## Table of Contents 7 | 8 | - [Features](#features) 9 | - [Tools and Technologies](#tools-and-technologies) 10 | - [Dependencies](#dependencies) 11 | - [Dev-dependencies](#dev-dependencies) 12 | - [Prerequisites](#prerequisites) 13 | - [Installation and setup](#installation-and-setup) 14 | - [Backend API](#backend-api) 15 | - [frontend pages](#frontend-pages) 16 | - [npm scripts](#npm-scripts) 17 | - [Useful Links](#useful-links) 18 | - [Contact](#contact) 19 | 20 | ## Features 21 | 22 | ### User-side features 23 | 24 | - Signup 25 | - Login 26 | - Logout 27 | - Add tasks 28 | - View tasks 29 | - Update tasks 30 | - Delete tasks 31 | 32 | ### Developer-side features 33 | 34 | - Toasts for success and error messages 35 | - Form validations in frontend and backend 36 | - Fully Responsive Navbar 37 | - Token based Authentication 38 | - Use of 404 page for wrong urls 39 | - Relevant redirects 40 | - Global user state using Redux 41 | - Custom Loaders 42 | - Use of layout component for pages 43 | - Use of theme colors 44 | - No external CSS files needed (made using Tailwind CSS) 45 | - Usage of Tooltips 46 | - Dynamic document titles 47 | - Redirect to previous page after login 48 | - Use of various React hooks 49 | - Custom hook also used (useFetch) 50 | - Routes protection 51 | - Middleware for verifying the user in backend 52 | - Use of different HTTP status codes for sending responses 53 | - Standard pratices followed 54 | 55 | ## Tools and Technologies 56 | 57 | - HTML 58 | - CSS 59 | - Javascript 60 | - Tailwind CSS 61 | - Node.js 62 | - Express.js 63 | - React 64 | - Redux 65 | - Mongodb 66 | 67 | ## Dependencies 68 | 69 | Following are the major dependencies of the project: 70 | 71 | - axios 72 | - react 73 | - react-dom 74 | - react-redux 75 | - react-router-dom 76 | - react-toastify 77 | - redux 78 | - redux-thunk 79 | - bcrypt 80 | - cors 81 | - dotenv 82 | - express 83 | - jsonwebtoken 84 | - mongoose 85 | 86 | ## Dev-dependencies 87 | 88 | Following are the major dev-dependencies of the project: 89 | 90 | - nodemon 91 | - concurrently 92 | 93 | ## Prerequisites 94 | 95 | - Node.js must be installed on the system. 96 | - You should have a MongoDB database. 97 | - You should have a code editor (preferred: VS Code) 98 | 99 | ## Installation and Setup 100 | 101 | 1. Install all the dependencies 102 | 103 | ```sh 104 | npm run install-all 105 | ``` 106 | 107 | 2. Create a file named ".env" inside the backend folder. Add data from .env.example file and substitute your credentials there. 108 | 109 | 3. Start the application 110 | 111 | ```sh 112 | npm run dev 113 | ``` 114 | 115 | 4. Go to http://localhost:3000 116 | 117 | ## Backend API 118 | 119 |
120 | - POST     /api/auth/signup
121 | - POST     /api/auth/login
122 | - GET      /api/tasks
123 | - GET      /api/tasks/:taskId
124 | - POST     /api/tasks
125 | - PUT      /api/tasks/:taskId
126 | - DELETE   /api/tasks/:taskId
127 | - GET      /api/profile
128 | 
129 | 130 | ## Frontend pages 131 | 132 |
133 | - /                 Home Screen (Public home page for guests and private dashboard (tasks) for logged-in users)
134 | - /signup           Signup page
135 | - /login            Login page
136 | - /tasks/add        Add new task
137 | - /tasks/:taskId    Edit a task
138 | 
139 | 140 | ## npm scripts 141 | 142 | At root: 143 | 144 | - `npm run dev`: Starts both backend and frontend 145 | - `npm run dev-server`: Starts only backend 146 | - `npm run dev-client`: Starts only frontend 147 | - `npm run install-all`: Installs all dependencies and dev-dependencies required at root, at frontend and at backend. 148 | 149 | Inside frontend folder: 150 | 151 | - `npm start`: Starts frontend in development mode 152 | - `npm run build`: Builds the frontend for production to the build folder 153 | - `npm test`: Launches the test runner in the interactive watch mode 154 | - `npm run eject`: This will remove the single build dependency from the frontend. 155 | 156 | Inside backend folder: 157 | 158 | - `npm run dev`: Starts backend using nodemon. 159 | - `npm start`: Starts backend without nodemon. 160 | 161 | ## Useful Links 162 | 163 | - This project 164 | 165 | - Github Repo: https://github.com/aayush301/MERN-task-manager 166 | 167 | - Official Docs 168 | 169 | - Reactjs docs: https://reactjs.org/docs/getting-started.html 170 | - npmjs docs: https://docs.npmjs.com/ 171 | - Mongodb docs: https://docs.mongodb.com/manual/introduction/ 172 | - Github docs: https://docs.github.com/en/get-started/quickstart/hello-world 173 | 174 | - Youtube tutorials 175 | 176 | - Expressjs: https://youtu.be/L72fhGm1tfE 177 | - React: https://youtu.be/EHTWMpD6S_0 178 | - Redux: https://youtu.be/1oU_YGhT7ck 179 | 180 | - Download links 181 | 182 | - Nodejs download: https://nodejs.org/ 183 | - VS Code download: https://code.visualstudio.com/ 184 | 185 | - Cheatsheets 186 | - Git cheatsheet: https://education.github.com/git-cheat-sheet-education.pdf 187 | - VS Code keyboard shortcuts: https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf 188 | - CSS Selectors Cheatsheet: https://frontend30.com/css-selectors-cheatsheet/ 189 | 190 | ## Contact 191 | 192 | - Email: aayush5521186@gmail.com 193 | - Linkedin: https://www.linkedin.com/in/aayush12/ 194 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | MONGODB_URL = your-mongodb-url 2 | ACCESS_TOKEN_SECRET = Rj2S?RVe9[]8-dCS6A**&b5Tsg$gwbg~Bd{*QTK 3 | -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const mongoose = require("mongoose"); 4 | const path = require("path"); 5 | const cors = require("cors"); 6 | require("dotenv").config(); 7 | const authRoutes = require("./routes/authRoutes"); 8 | const taskRoutes = require("./routes/taskRoutes"); 9 | const profileRoutes = require("./routes/profileRoutes"); 10 | 11 | app.use(express.json()); 12 | app.use(cors()); 13 | 14 | const mongoUrl = process.env.MONGODB_URL; 15 | mongoose.connect(mongoUrl, err => { 16 | if (err) throw err; 17 | console.log("Mongodb connected..."); 18 | }); 19 | 20 | app.use("/api/auth", authRoutes); 21 | app.use("/api/tasks", taskRoutes); 22 | app.use("/api/profile", profileRoutes); 23 | 24 | if (process.env.NODE_ENV === "production") { 25 | app.use(express.static(path.resolve(__dirname, "../frontend/build"))); 26 | app.get("*", (req, res) => res.sendFile(path.resolve(__dirname, "../frontend/build/index.html"))); 27 | } 28 | 29 | const port = process.env.PORT || 5000; 30 | app.listen(port, () => { 31 | console.log(`Backend is running on port ${port}`); 32 | }); 33 | -------------------------------------------------------------------------------- /backend/controllers/authControllers.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/User"); 2 | const bcrypt = require("bcrypt"); 3 | const { createAccessToken } = require("../utils/token"); 4 | const { validateEmail } = require("../utils/validation"); 5 | 6 | 7 | exports.signup = async (req, res) => { 8 | try { 9 | const { name, email, password } = req.body; 10 | if (!name || !email || !password) { 11 | return res.status(400).json({ msg: "Please fill all the fields" }); 12 | } 13 | if (typeof name !== "string" || typeof email !== "string" || typeof password !== "string") { 14 | return res.status(400).json({ msg: "Please send string values only" }); 15 | } 16 | 17 | 18 | if (password.length < 4) { 19 | return res.status(400).json({ msg: "Password length must be atleast 4 characters" }); 20 | } 21 | 22 | if (!validateEmail(email)) { 23 | return res.status(400).json({ msg: "Invalid Email" }); 24 | } 25 | 26 | const user = await User.findOne({ email }); 27 | if (user) { 28 | return res.status(400).json({ msg: "This email is already registered" }); 29 | } 30 | 31 | const hashedPassword = await bcrypt.hash(password, 10); 32 | await User.create({ name, email, password: hashedPassword }); 33 | res.status(200).json({ msg: "Congratulations!! Account has been created for you.." }); 34 | } 35 | catch (err) { 36 | console.error(err); 37 | return res.status(500).json({ msg: "Internal Server Error" }); 38 | } 39 | } 40 | 41 | 42 | 43 | exports.login = async (req, res) => { 44 | try { 45 | const { email, password } = req.body; 46 | if (!email || !password) { 47 | return res.status(400).json({ status: false, msg: "Please enter all details!!" }); 48 | } 49 | 50 | const user = await User.findOne({ email }); 51 | if (!user) return res.status(400).json({ status: false, msg: "This email is not registered!!" }); 52 | 53 | const isMatch = await bcrypt.compare(password, user.password); 54 | if (!isMatch) return res.status(400).json({ status: false, msg: "Password incorrect!!" }); 55 | 56 | const token = createAccessToken({ id: user._id }); 57 | delete user.password; 58 | res.status(200).json({ token, user, status: true, msg: "Login successful.." }); 59 | } 60 | catch (err) { 61 | console.error(err); 62 | return res.status(500).json({ status: false, msg: "Internal Server Error" }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /backend/controllers/profileControllers.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/User"); 2 | 3 | exports.getProfile = async (req, res) => { 4 | try { 5 | const user = await User.findById(req.user.id).select("-password"); 6 | res.status(200).json({ user, status: true, msg: "Profile found successfully.." }); 7 | } 8 | catch (err) { 9 | console.error(err); 10 | return res.status(500).json({ status: false, msg: "Internal Server Error" }); 11 | } 12 | } -------------------------------------------------------------------------------- /backend/controllers/taskControllers.js: -------------------------------------------------------------------------------- 1 | const Task = require("../models/Task"); 2 | const { validateObjectId } = require("../utils/validation"); 3 | 4 | 5 | exports.getTasks = async (req, res) => { 6 | try { 7 | const tasks = await Task.find({ user: req.user.id }); 8 | res.status(200).json({ tasks, status: true, msg: "Tasks found successfully.." }); 9 | } 10 | catch (err) { 11 | console.error(err); 12 | return res.status(500).json({ status: false, msg: "Internal Server Error" }); 13 | } 14 | } 15 | 16 | exports.getTask = async (req, res) => { 17 | try { 18 | if (!validateObjectId(req.params.taskId)) { 19 | return res.status(400).json({ status: false, msg: "Task id not valid" }); 20 | } 21 | 22 | const task = await Task.findOne({ user: req.user.id, _id: req.params.taskId }); 23 | if (!task) { 24 | return res.status(400).json({ status: false, msg: "No task found.." }); 25 | } 26 | res.status(200).json({ task, status: true, msg: "Task found successfully.." }); 27 | } 28 | catch (err) { 29 | console.error(err); 30 | return res.status(500).json({ status: false, msg: "Internal Server Error" }); 31 | } 32 | } 33 | 34 | exports.postTask = async (req, res) => { 35 | try { 36 | const { description } = req.body; 37 | if (!description) { 38 | return res.status(400).json({ status: false, msg: "Description of task not found" }); 39 | } 40 | const task = await Task.create({ user: req.user.id, description }); 41 | res.status(200).json({ task, status: true, msg: "Task created successfully.." }); 42 | } 43 | catch (err) { 44 | console.error(err); 45 | return res.status(500).json({ status: false, msg: "Internal Server Error" }); 46 | } 47 | } 48 | 49 | exports.putTask = async (req, res) => { 50 | try { 51 | const { description } = req.body; 52 | if (!description) { 53 | return res.status(400).json({ status: false, msg: "Description of task not found" }); 54 | } 55 | 56 | if (!validateObjectId(req.params.taskId)) { 57 | return res.status(400).json({ status: false, msg: "Task id not valid" }); 58 | } 59 | 60 | let task = await Task.findById(req.params.taskId); 61 | if (!task) { 62 | return res.status(400).json({ status: false, msg: "Task with given id not found" }); 63 | } 64 | 65 | if (task.user != req.user.id) { 66 | return res.status(403).json({ status: false, msg: "You can't update task of another user" }); 67 | } 68 | 69 | task = await Task.findByIdAndUpdate(req.params.taskId, { description }, { new: true }); 70 | res.status(200).json({ task, status: true, msg: "Task updated successfully.." }); 71 | } 72 | catch (err) { 73 | console.error(err); 74 | return res.status(500).json({ status: false, msg: "Internal Server Error" }); 75 | } 76 | } 77 | 78 | 79 | exports.deleteTask = async (req, res) => { 80 | try { 81 | if (!validateObjectId(req.params.taskId)) { 82 | return res.status(400).json({ status: false, msg: "Task id not valid" }); 83 | } 84 | 85 | let task = await Task.findById(req.params.taskId); 86 | if (!task) { 87 | return res.status(400).json({ status: false, msg: "Task with given id not found" }); 88 | } 89 | 90 | if (task.user != req.user.id) { 91 | return res.status(403).json({ status: false, msg: "You can't delete task of another user" }); 92 | } 93 | 94 | await Task.findByIdAndDelete(req.params.taskId); 95 | res.status(200).json({ status: true, msg: "Task deleted successfully.." }); 96 | } 97 | catch (err) { 98 | console.error(err); 99 | return res.status(500).json({ status: false, msg: "Internal Server Error" }); 100 | } 101 | } -------------------------------------------------------------------------------- /backend/middlewares.js/index.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const User = require("../models/User"); 3 | const { ACCESS_TOKEN_SECRET } = process.env; 4 | 5 | 6 | exports.verifyAccessToken = async (req, res, next) => { 7 | 8 | const token = req.header("Authorization"); 9 | if (!token) return res.status(400).json({ status: false, msg: "Token not found" }); 10 | let user; 11 | try { 12 | user = jwt.verify(token, ACCESS_TOKEN_SECRET); 13 | } 14 | catch (err) { 15 | return res.status(401).json({ status: false, msg: "Invalid token" }); 16 | } 17 | 18 | try { 19 | user = await User.findById(user.id); 20 | if (!user) { 21 | return res.status(401).json({ status: false, msg: "User not found" }); 22 | } 23 | 24 | req.user = user; 25 | next(); 26 | } 27 | catch (err) { 28 | console.error(err); 29 | return res.status(500).json({ status: false, msg: "Internal Server Error" }); 30 | } 31 | } -------------------------------------------------------------------------------- /backend/models/Task.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const taskSchema = new mongoose.Schema({ 4 | user: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | ref: "User", 7 | required: true 8 | }, 9 | description: { 10 | type: String, 11 | required: true, 12 | }, 13 | }, { 14 | timestamps: true 15 | }); 16 | 17 | 18 | const Task = mongoose.model("Task", taskSchema); 19 | module.exports = Task; -------------------------------------------------------------------------------- /backend/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const userSchema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | required: [true, "Please enter your name"], 7 | trim: true 8 | }, 9 | email: { 10 | type: String, 11 | required: [true, "Please enter your email"], 12 | trim: true, 13 | unique: true 14 | }, 15 | password: { 16 | type: String, 17 | required: [true, "Please enter your password"], 18 | }, 19 | joiningTime: { 20 | type: Date, 21 | default: Date.now 22 | } 23 | }, { 24 | timestamps: true 25 | }); 26 | 27 | 28 | const User = mongoose.model("User", userSchema); 29 | module.exports = User; -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-task-manager-backend", 3 | "version": "1.0.0", 4 | "main": "app.js", 5 | "scripts": { 6 | "dev": "nodemon app.js", 7 | "start": "node app.js" 8 | }, 9 | "keywords": [ 10 | "Task manager", 11 | "Tasks" 12 | ], 13 | "author": "Aayush", 14 | "license": "ISC", 15 | "description": "The backend part of simple task manager with user authentication", 16 | "dependencies": { 17 | "bcrypt": "^5.0.1", 18 | "cors": "^2.8.5", 19 | "dotenv": "^16.0.0", 20 | "express": "^4.17.3", 21 | "jsonwebtoken": "^8.5.1", 22 | "mongoose": "^6.2.3" 23 | }, 24 | "devDependencies": { 25 | "nodemon": "^2.0.15" 26 | } 27 | } -------------------------------------------------------------------------------- /backend/routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { signup, login } = require("../controllers/authControllers"); 4 | 5 | // Routes beginning with /api/auth 6 | router.post("/signup", signup); 7 | router.post("/login", login); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /backend/routes/profileRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { getProfile } = require("../controllers/profileControllers"); 4 | const { verifyAccessToken } = require("../middlewares.js"); 5 | 6 | // Routes beginning with /api/profile 7 | router.get("/", verifyAccessToken, getProfile); 8 | 9 | module.exports = router; -------------------------------------------------------------------------------- /backend/routes/taskRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { getTasks, getTask, postTask, putTask, deleteTask } = require("../controllers/taskControllers"); 4 | const { verifyAccessToken } = require("../middlewares.js"); 5 | 6 | // Routes beginning with /api/tasks 7 | router.get("/", verifyAccessToken, getTasks); 8 | router.get("/:taskId", verifyAccessToken, getTask); 9 | router.post("/", verifyAccessToken, postTask); 10 | router.put("/:taskId", verifyAccessToken, putTask); 11 | router.delete("/:taskId", verifyAccessToken, deleteTask); 12 | 13 | module.exports = router; 14 | -------------------------------------------------------------------------------- /backend/utils/token.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const { ACCESS_TOKEN_SECRET } = process.env; 3 | 4 | const createAccessToken = (payload) => { 5 | return jwt.sign(payload, ACCESS_TOKEN_SECRET); 6 | } 7 | 8 | module.exports = { 9 | createAccessToken, 10 | } -------------------------------------------------------------------------------- /backend/utils/validation.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const validateEmail = (email) => { 4 | return String(email) 5 | .toLowerCase() 6 | .match( 7 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 8 | ); 9 | }; 10 | 11 | const validateObjectId = (string) => { 12 | return mongoose.Types.ObjectId.isValid(string); 13 | } 14 | 15 | module.exports = { 16 | validateEmail, 17 | validateObjectId, 18 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "proxy": "http://127.0.0.1:5000", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.16.4", 8 | "@testing-library/react": "^13.2.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^0.27.2", 11 | "react": "^18.1.0", 12 | "react-dom": "^18.1.0", 13 | "react-redux": "^8.0.1", 14 | "react-router-dom": "^6.3.0", 15 | "react-scripts": "5.0.1", 16 | "react-toastify": "^9.0.1", 17 | "redux": "^4.2.0", 18 | "redux-thunk": "^2.4.1", 19 | "web-vitals": "^2.1.4" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "autoprefixer": "^10.4.7", 47 | "postcss": "^8.4.13", 48 | "tailwindcss": "^3.0.24" 49 | } 50 | } -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Task manager 9 | 10 | 13 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; 4 | import Task from "./pages/Task"; 5 | import Home from "./pages/Home"; 6 | import Login from "./pages/Login"; 7 | import Signup from "./pages/Signup"; 8 | import { saveProfile } from "./redux/actions/authActions"; 9 | import NotFound from "./pages/NotFound"; 10 | 11 | function App() { 12 | 13 | const authState = useSelector(state => state.authReducer); 14 | const dispatch = useDispatch(); 15 | 16 | useEffect(() => { 17 | const token = localStorage.getItem("token"); 18 | if (!token) return; 19 | dispatch(saveProfile(token)); 20 | }, [authState.isLoggedIn, dispatch]); 21 | 22 | 23 | return ( 24 | <> 25 | 26 | 27 | } /> 28 | : } /> 29 | } /> 30 | : } /> 31 | : } /> 32 | } /> 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /frontend/src/api/index.jsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const api = axios.create({ 4 | baseURL: "/api", 5 | }); 6 | export default api; 7 | -------------------------------------------------------------------------------- /frontend/src/components/LoginForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import validateManyFields from '../validations'; 4 | import Input from './utils/Input'; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { postLoginData } from '../redux/actions/authActions'; 7 | import Loader from './utils/Loader'; 8 | import { useEffect } from 'react'; 9 | 10 | const LoginForm = ({ redirectUrl }) => { 11 | 12 | const [formErrors, setFormErrors] = useState({}); 13 | const [formData, setFormData] = useState({ 14 | email: "", 15 | password: "" 16 | }); 17 | const navigate = useNavigate(); 18 | 19 | const authState = useSelector(state => state.authReducer); 20 | const { loading, isLoggedIn } = authState; 21 | const dispatch = useDispatch(); 22 | 23 | 24 | useEffect(() => { 25 | if (isLoggedIn) { 26 | navigate(redirectUrl || "/"); 27 | } 28 | }, [authState, redirectUrl, isLoggedIn, navigate]); 29 | 30 | 31 | 32 | const handleChange = e => { 33 | setFormData({ 34 | ...formData, [e.target.name]: e.target.value 35 | }); 36 | } 37 | 38 | const handleSubmit = e => { 39 | e.preventDefault(); 40 | const errors = validateManyFields("login", formData); 41 | setFormErrors({}); 42 | if (errors.length > 0) { 43 | setFormErrors(errors.reduce((total, ob) => ({ ...total, [ob.field]: ob.err }), {})); 44 | return; 45 | } 46 | dispatch(postLoginData(formData.email, formData.password)); 47 | } 48 | 49 | 50 | 51 | const fieldError = (field) => ( 52 |

53 | 54 | {formErrors[field]} 55 |

56 | ) 57 | 58 | return ( 59 | <> 60 |
61 | {loading ? ( 62 | 63 | ) : ( 64 | <> 65 |

Welcome user, please login here

66 |
67 | 68 | 69 | {fieldError("email")} 70 |
71 | 72 |
73 | 74 | 75 | {fieldError("password")} 76 |
77 | 78 | 79 | 80 |
81 | Don't have an account? Signup here 82 |
83 | 84 | )} 85 | 86 | 87 | ) 88 | } 89 | 90 | export default LoginForm -------------------------------------------------------------------------------- /frontend/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import { logout } from '../redux/actions/authActions'; 5 | 6 | const Navbar = () => { 7 | 8 | const authState = useSelector(state => state.authReducer); 9 | const dispatch = useDispatch(); 10 | const [isNavbarOpen, setIsNavbarOpen] = useState(false); 11 | const toggleNavbar = () => { 12 | setIsNavbarOpen(!isNavbarOpen); 13 | } 14 | 15 | const handleLogoutClick = () => { 16 | dispatch(logout()); 17 | } 18 | 19 | return ( 20 | <> 21 |
22 |

23 | Task Manager 24 |

25 |
    26 | {authState.isLoggedIn ? ( 27 | <> 28 |
  • 29 | Add task 30 |
  • 31 |
  • Logout
  • 32 | 33 | ) : ( 34 |
  • Login
  • 35 | )} 36 |
37 | 38 | 39 | 40 | {/* Navbar displayed as sidebar on smaller screens */} 41 |
42 |
43 | 44 |
45 |
    46 | {authState.isLoggedIn ? ( 47 | <> 48 |
  • 49 | Add task 50 |
  • 51 |
  • Logout
  • 52 | 53 | ) : ( 54 |
  • Login
  • 55 | )} 56 |
57 |
58 |
59 | 60 | ) 61 | } 62 | 63 | export default Navbar -------------------------------------------------------------------------------- /frontend/src/components/SignupForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import useFetch from '../hooks/useFetch'; 4 | import validateManyFields from '../validations'; 5 | import Input from './utils/Input'; 6 | import Loader from './utils/Loader'; 7 | 8 | const SignupForm = () => { 9 | 10 | const [formErrors, setFormErrors] = useState({}); 11 | const [formData, setFormData] = useState({ 12 | name: "", 13 | email: "", 14 | password: "" 15 | }); 16 | const [fetchData, { loading }] = useFetch(); 17 | const navigate = useNavigate(); 18 | 19 | const handleChange = e => { 20 | setFormData({ 21 | ...formData, [e.target.name]: e.target.value 22 | }); 23 | } 24 | 25 | const handleSubmit = e => { 26 | e.preventDefault(); 27 | const errors = validateManyFields("signup", formData); 28 | setFormErrors({}); 29 | if (errors.length > 0) { 30 | setFormErrors(errors.reduce((total, ob) => ({ ...total, [ob.field]: ob.err }), {})); 31 | return; 32 | } 33 | 34 | const config = { url: "/auth/signup", method: "post", data: formData }; 35 | fetchData(config).then(() => { 36 | navigate("/login"); 37 | }); 38 | 39 | } 40 | 41 | const fieldError = (field) => ( 42 |

43 | 44 | {formErrors[field]} 45 |

46 | ) 47 | 48 | return ( 49 | <> 50 |
51 | {loading ? ( 52 | 53 | ) : ( 54 | <> 55 |

Welcome user, please signup here

56 |
57 | 58 | 59 | {fieldError("name")} 60 |
61 | 62 |
63 | 64 | 65 | {fieldError("email")} 66 |
67 | 68 |
69 | 70 | 71 | {fieldError("password")} 72 |
73 | 74 | 75 | 76 |
77 | Already have an account? Login here 78 |
79 | 80 | )} 81 | 82 | 83 | 84 | ) 85 | } 86 | 87 | export default SignupForm -------------------------------------------------------------------------------- /frontend/src/components/Tasks.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react' 2 | import { useSelector } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import useFetch from '../hooks/useFetch'; 5 | import Loader from './utils/Loader'; 6 | import Tooltip from './utils/Tooltip'; 7 | 8 | const Tasks = () => { 9 | 10 | const authState = useSelector(state => state.authReducer); 11 | const [tasks, setTasks] = useState([]); 12 | const [fetchData, { loading }] = useFetch(); 13 | 14 | const fetchTasks = useCallback(() => { 15 | const config = { url: "/tasks", method: "get", headers: { Authorization: authState.token } }; 16 | fetchData(config, { showSuccessToast: false }).then(data => setTasks(data.tasks)); 17 | }, [authState.token, fetchData]); 18 | 19 | useEffect(() => { 20 | if (!authState.isLoggedIn) return; 21 | fetchTasks(); 22 | }, [authState.isLoggedIn, fetchTasks]); 23 | 24 | 25 | const handleDelete = (id) => { 26 | const config = { url: `/tasks/${id}`, method: "delete", headers: { Authorization: authState.token } }; 27 | fetchData(config).then(() => fetchTasks()); 28 | } 29 | 30 | 31 | return ( 32 | <> 33 |
34 | 35 | {tasks.length !== 0 &&

Your tasks ({tasks.length})

} 36 | {loading ? ( 37 | 38 | ) : ( 39 |
40 | {tasks.length === 0 ? ( 41 | 42 |
43 | No tasks found 44 | + Add new task 45 |
46 | 47 | ) : ( 48 | tasks.map((task, index) => ( 49 |
50 |
51 | 52 | Task #{index + 1} 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | handleDelete(task._id)}> 62 | 63 | 64 | 65 | 66 |
67 |
{task.description}
68 |
69 | )) 70 | 71 | )} 72 |
73 | )} 74 |
75 | 76 | ) 77 | 78 | } 79 | 80 | export default Tasks -------------------------------------------------------------------------------- /frontend/src/components/utils/Input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Input = ({ id, name, type, value, className = "", disabled = false, placeholder, onChange }) => { 4 | return ( 5 | 6 | ) 7 | } 8 | export default Input 9 | 10 | export const Textarea = ({ id, name, type, value, className = "", placeholder, onChange }) => { 11 | return ( 12 |