├── .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 | 
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 |
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 |
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 |
13 | )
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/frontend/src/components/utils/Loader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Loader = () => {
4 | return (
5 | <>
6 |
9 | >
10 | )
11 | }
12 |
13 | export default Loader
--------------------------------------------------------------------------------
/frontend/src/components/utils/Tooltip.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from 'react'
2 | import ReactDom from 'react-dom';
3 |
4 | const Portal = ({ children }) => {
5 | return ReactDom.createPortal(children, document.body);
6 | }
7 |
8 | // @param space: it is the dist between the tip and the element
9 | // @param children: It is expected to have only 1 child
10 | const Tooltip = ({ children, text, position = "bottom", space = 5 }) => {
11 |
12 | if (!React.isValidElement(children)) {
13 | children = children[0]
14 | }
15 |
16 | const [open, setOpen] = useState(false);
17 | const tooltipRef = useRef();
18 | const elementRef = useRef();
19 |
20 | const handleMouseEnter = () => {
21 | setOpen(true);
22 | const { x, y } = getPoint(elementRef.current, tooltipRef.current, position, space);
23 | tooltipRef.current.style.left = `${x}px`;
24 | tooltipRef.current.style.top = `${y}px`;
25 | }
26 |
27 | const getPoint = (element, tooltip, position, space) => {
28 | const eleRect = element.getBoundingClientRect();
29 | const pt = { x: 0, y: 0 };
30 | switch (position) {
31 | case "bottom": {
32 | pt.x = eleRect.left + (element.offsetWidth - tooltip.offsetWidth) / 2;
33 | pt.y = eleRect.bottom + (space + 10);
34 | break;
35 | }
36 | case "left": {
37 | pt.x = eleRect.left - (tooltip.offsetWidth + (space + 10));
38 | pt.y = eleRect.top + (element.offsetHeight - tooltip.offsetHeight) / 2;
39 | break;
40 | }
41 | case "right": {
42 | pt.x = eleRect.right + (space + 10);
43 | pt.y = eleRect.top + (element.offsetHeight - tooltip.offsetHeight) / 2;
44 | break;
45 | }
46 | case "top": {
47 | pt.x = eleRect.left + (element.offsetWidth - tooltip.offsetWidth) / 2;
48 | pt.y = eleRect.top - (tooltip.offsetHeight + (space + 10));
49 | break;
50 | }
51 | default: {
52 | break;
53 | }
54 | }
55 | return pt;
56 | }
57 |
58 | const tooltipClasses =
59 | `fixed transition ${open ? "opacity-100" : "opacity-0 "} pointer-events-none z-50 rounded-md bg-black text-white px-4 py-2 text-center w-max max-w-[150px]
60 | ${position === "top" && " after:absolute after:content-[''] after:left-1/2 after:top-full after:-translate-x-1/2 after:border-[10px] after:border-transparent after:border-t-black"}
61 | ${position === "bottom" && " after:absolute after:content-[''] after:left-1/2 after:bottom-full after:-translate-x-1/2 after:border-[10px] after:border-transparent after:border-b-black"}
62 | ${position === "left" && " after:absolute after:content-[''] after:top-1/2 after:left-full after:-translate-y-1/2 after:border-[10px] after:border-transparent after:border-l-black"}
63 | ${position === "right" && " after:absolute after:content-[''] after:top-1/2 after:right-full after:-translate-y-1/2 after:border-[10px] after:border-transparent after:border-r-black"}
64 | `;
65 |
66 | return (
67 | <>
68 | {React.cloneElement(children, {
69 | onMouseEnter: handleMouseEnter,
70 | onMouseLeave: () => setOpen(false),
71 | ref: elementRef
72 | })}
73 |
74 |
75 | {text}
76 |
77 | >
78 | )
79 | }
80 |
81 | export default Tooltip
--------------------------------------------------------------------------------
/frontend/src/hooks/useFetch.jsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react"
2 | import { toast } from "react-toastify";
3 | import api from "../api";
4 |
5 | const useFetch = () => {
6 |
7 | const [state, setState] = useState({
8 | loading: false,
9 | data: null,
10 | successMsg: "",
11 | errorMsg: "",
12 | });
13 |
14 | const fetchData = useCallback(async (config, otherOptions) => {
15 | const { showSuccessToast = true, showErrorToast = true } = otherOptions || {};
16 | setState(state => ({ ...state, loading: true }));
17 |
18 | try {
19 | const { data } = await api.request(config);
20 | setState({
21 | loading: false,
22 | data,
23 | successMsg: data.msg || "success",
24 | errorMsg: ""
25 | });
26 |
27 | if (showSuccessToast) toast.success(data.msg);
28 | return Promise.resolve(data);
29 | }
30 | catch (error) {
31 | const msg = error.response?.data?.msg || error.message || "error";
32 | setState({
33 | loading: false,
34 | data: null,
35 | errorMsg: msg,
36 | successMsg: ""
37 | });
38 |
39 | if (showErrorToast) toast.error(msg);
40 | return Promise.reject();
41 | }
42 | }, []);
43 |
44 | return [fetchData, state];
45 | }
46 |
47 | export default useFetch
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body
6 | {
7 | font-family: "Roboto", sans-serif;
8 | }
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import { Provider } from "react-redux"
6 | import store from './redux/store';
7 | import { ToastContainer } from 'react-toastify';
8 | import 'react-toastify/dist/ReactToastify.css';
9 |
10 | const root = ReactDOM.createRoot(document.getElementById('root'));
11 | root.render(
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
--------------------------------------------------------------------------------
/frontend/src/layouts/MainLayout.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Navbar from '../components/Navbar';
3 |
4 | const MainLayout = ({ children }) => {
5 | return (
6 | <>
7 |
8 |
9 | {children}
10 |
11 | >
12 | )
13 | }
14 |
15 | export default MainLayout;
--------------------------------------------------------------------------------
/frontend/src/pages/Home.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useSelector } from 'react-redux';
3 | import { Link } from 'react-router-dom';
4 | import Tasks from '../components/Tasks';
5 | import MainLayout from '../layouts/MainLayout';
6 |
7 | const Home = () => {
8 |
9 | const authState = useSelector(state => state.authReducer);
10 | const { isLoggedIn } = authState;
11 |
12 | useEffect(() => {
13 | document.title = authState.isLoggedIn ? `${authState.user.name}'s tasks` : "Task Manager";
14 | }, [authState]);
15 |
16 |
17 |
18 | return (
19 | <>
20 |
21 | {!isLoggedIn ? (
22 |
23 |
Welcome to Task Manager App
24 |
25 | Join now to manage your tasks
26 |
27 |
28 |
29 | ) : (
30 | <>
31 | Welcome {authState.user.name}
32 |
33 | >
34 | )}
35 |
36 | >
37 | )
38 | }
39 |
40 | export default Home
--------------------------------------------------------------------------------
/frontend/src/pages/Login.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useLocation } from 'react-router-dom';
3 | import LoginForm from '../components/LoginForm';
4 | import MainLayout from '../layouts/MainLayout'
5 |
6 | const Login = () => {
7 | const { state } = useLocation();
8 | const redirectUrl = state?.redirectUrl || null;
9 |
10 | useEffect(() => {
11 | document.title = "Login";
12 | }, []);
13 |
14 | return (
15 | <>
16 |
17 |
18 |
19 | >
20 | )
21 | }
22 |
23 | export default Login
--------------------------------------------------------------------------------
/frontend/src/pages/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import MainLayout from '../layouts/MainLayout'
3 |
4 | const NotFound = () => {
5 | return (
6 |
7 |
8 |
404
9 | The page you are looking for doesn't exist
10 |
11 |
12 | )
13 | }
14 |
15 | export default NotFound
--------------------------------------------------------------------------------
/frontend/src/pages/Signup.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import SignupForm from '../components/SignupForm';
3 | import MainLayout from '../layouts/MainLayout'
4 |
5 | const Signup = () => {
6 |
7 | useEffect(() => {
8 | document.title = "Signup";
9 | }, []);
10 | return (
11 | <>
12 |
13 |
14 |
15 | >
16 | )
17 | }
18 |
19 | export default Signup
--------------------------------------------------------------------------------
/frontend/src/pages/Task.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { useSelector } from 'react-redux';
3 | import { useNavigate, useParams } from 'react-router-dom';
4 | import { Textarea } from '../components/utils/Input';
5 | import Loader from '../components/utils/Loader';
6 | import useFetch from '../hooks/useFetch';
7 | import MainLayout from '../layouts/MainLayout';
8 | import validateManyFields from '../validations';
9 |
10 | const Task = () => {
11 |
12 | const authState = useSelector(state => state.authReducer);
13 | const navigate = useNavigate();
14 | const [fetchData, { loading }] = useFetch();
15 | const { taskId } = useParams();
16 |
17 | const mode = taskId === undefined ? "add" : "update";
18 | const [task, setTask] = useState(null);
19 | const [formData, setFormData] = useState({
20 | description: ""
21 | });
22 | const [formErrors, setFormErrors] = useState({});
23 |
24 |
25 | useEffect(() => {
26 | document.title = mode === "add" ? "Add task" : "Update Task";
27 | }, [mode]);
28 |
29 |
30 | useEffect(() => {
31 | if (mode === "update") {
32 | const config = { url: `/tasks/${taskId}`, method: "get", headers: { Authorization: authState.token } };
33 | fetchData(config, { showSuccessToast: false }).then((data) => {
34 | setTask(data.task);
35 | setFormData({ description: data.task.description });
36 | });
37 | }
38 | }, [mode, authState, taskId, fetchData]);
39 |
40 |
41 |
42 | const handleChange = e => {
43 | setFormData({
44 | ...formData, [e.target.name]: e.target.value
45 | });
46 | }
47 |
48 | const handleReset = e => {
49 | e.preventDefault();
50 | setFormData({
51 | description: task.description
52 | });
53 | }
54 |
55 | const handleSubmit = e => {
56 | e.preventDefault();
57 | const errors = validateManyFields("task", formData);
58 | setFormErrors({});
59 |
60 | if (errors.length > 0) {
61 | setFormErrors(errors.reduce((total, ob) => ({ ...total, [ob.field]: ob.err }), {}));
62 | return;
63 | }
64 |
65 | if (mode === "add") {
66 | const config = { url: "/tasks", method: "post", data: formData, headers: { Authorization: authState.token } };
67 | fetchData(config).then(() => {
68 | navigate("/");
69 | });
70 | }
71 | else {
72 | const config = { url: `/tasks/${taskId}`, method: "put", data: formData, headers: { Authorization: authState.token } };
73 | fetchData(config).then(() => {
74 | navigate("/");
75 | });
76 | }
77 | }
78 |
79 |
80 | const fieldError = (field) => (
81 |
82 |
83 | {formErrors[field]}
84 |
85 | )
86 |
87 | return (
88 | <>
89 |
90 |
108 |
109 | >
110 | )
111 | }
112 |
113 | export default Task
--------------------------------------------------------------------------------
/frontend/src/redux/actions/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const LOGIN_REQUEST = 'LOGIN_REQUEST'
2 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
3 | export const LOGIN_FAILURE = 'LOGIN_FAILURE'
4 | export const LOGOUT = 'LOGOUT'
5 | export const SAVE_PROFILE = 'SAVE_PROFILE'
6 |
7 | export const SIGNUP_REQUEST = 'SIGNUP_REQUEST'
8 | export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS'
9 | export const SIGNUP_FAILURE = 'SIGNUP_FAILURE'
--------------------------------------------------------------------------------
/frontend/src/redux/actions/authActions.js:
--------------------------------------------------------------------------------
1 | import api from "../../api"
2 | import { LOGIN_FAILURE, LOGIN_REQUEST, LOGIN_SUCCESS, LOGOUT, SAVE_PROFILE } from "./actionTypes"
3 | import { toast } from "react-toastify";
4 |
5 | export const postLoginData = (email, password) => async (dispatch) => {
6 | try {
7 | dispatch({ type: LOGIN_REQUEST });
8 | const { data } = await api.post('/auth/login', { email, password });
9 | dispatch({
10 | type: LOGIN_SUCCESS,
11 | payload: data,
12 | });
13 | localStorage.setItem('token', data.token);
14 | toast.success(data.msg);
15 |
16 | }
17 | catch (error) {
18 | const msg = error.response?.data?.msg || error.message;
19 | dispatch({
20 | type: LOGIN_FAILURE,
21 | payload: { msg }
22 | })
23 | toast.error(msg);
24 | }
25 | }
26 |
27 |
28 |
29 | export const saveProfile = (token) => async (dispatch) => {
30 | try {
31 | const { data } = await api.get('/profile', {
32 | headers: { Authorization: token }
33 | });
34 | dispatch({
35 | type: SAVE_PROFILE,
36 | payload: { user: data.user, token },
37 | });
38 | }
39 | catch (error) {
40 | // console.log(error);
41 | }
42 | }
43 |
44 |
45 |
46 | export const logout = () => (dispatch) => {
47 | localStorage.removeItem('token');
48 | dispatch({ type: LOGOUT });
49 | document.location.href = '/';
50 | }
--------------------------------------------------------------------------------
/frontend/src/redux/reducers/authReducer.js:
--------------------------------------------------------------------------------
1 | import { LOGIN_FAILURE, LOGIN_REQUEST, LOGIN_SUCCESS, LOGOUT, SAVE_PROFILE } from "../actions/actionTypes"
2 |
3 | const initialState = {
4 | loading: false,
5 | user: {},
6 | isLoggedIn: false,
7 | token: "",
8 | successMsg: "",
9 | errorMsg: "",
10 | }
11 |
12 | const authReducer = (state = initialState, action) => {
13 | switch (action.type) {
14 | case LOGIN_REQUEST:
15 | return { loading: true, user: {}, isLoggedIn: false, token: "", successMsg: "", errorMsg: "", };
16 | case LOGIN_SUCCESS:
17 | return { loading: false, user: action.payload.user, isLoggedIn: true, token: action.payload.token, successMsg: action.payload.msg, errorMsg: "" };
18 | case LOGIN_FAILURE:
19 | return { loading: false, user: {}, isLoggedIn: false, token: "", successMsg: "", errorMsg: action.payload.msg };
20 | case LOGOUT:
21 | return { loading: false, user: {}, isLoggedIn: false, token: "", successMsg: "", errorMsg: "" }
22 | case SAVE_PROFILE:
23 | return { loading: false, user: action.payload.user, isLoggedIn: true, token: action.payload.token, successMsg: "", errorMsg: "" }
24 | default:
25 | return state;
26 | }
27 | }
28 |
29 | export default authReducer;
--------------------------------------------------------------------------------
/frontend/src/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux"
2 | import authReducer from "./authReducer"
3 |
4 | const rootReducer = combineReducers({
5 | authReducer,
6 | });
7 |
8 | export default rootReducer;
--------------------------------------------------------------------------------
/frontend/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, createStore, compose } from "redux";
2 | import thunk from "redux-thunk";
3 | import rootReducer from "./reducers";
4 |
5 | const middleware = [thunk];
6 |
7 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
8 | const store = createStore(rootReducer,
9 | composeEnhancers(applyMiddleware(...middleware))
10 | );
11 | export default store;
--------------------------------------------------------------------------------
/frontend/src/validations/index.js:
--------------------------------------------------------------------------------
1 | const isValidEmail = (email) => {
2 | return String(email)
3 | .toLowerCase()
4 | .match(
5 | /^(([^<>()[\]\\.,;:\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,}))$/
6 | );
7 | };
8 |
9 | export const validate = (group, name, value) => {
10 |
11 | if (group === "signup") {
12 | switch (name) {
13 | case "name": {
14 | if (!value) return "This field is required";
15 | return null;
16 | }
17 | case "email": {
18 | if (!value) return "This field is required";
19 | if (!isValidEmail(value)) return "Please enter valid email address";
20 | return null;
21 | }
22 | case "password": {
23 | if (!value) return "This field is required";
24 | if (value.length < 4) return "Password should be atleast 4 chars long";
25 | return null;
26 | }
27 | default: return null;
28 | }
29 | }
30 |
31 | else if (group === "login") {
32 | switch (name) {
33 | case "email": {
34 | if (!value) return "This field is required";
35 | if (!isValidEmail(value)) return "Please enter valid email address";
36 | return null;
37 | }
38 | case "password": {
39 | if (!value) return "This field is required";
40 | return null;
41 | }
42 | default: return null;
43 | }
44 | }
45 | else if (group === "task") {
46 | switch (name) {
47 | case "description": {
48 | if (!value) return "This field is required";
49 | if (value.length > 100) return "Max. limit is 100 characters.";
50 | return null;
51 | }
52 | default: return null;
53 | }
54 | }
55 |
56 | else {
57 | return null;
58 | }
59 |
60 | }
61 |
62 |
63 | const validateManyFields = (group, list) => {
64 | const errors = [];
65 | for (const field in list) {
66 | const err = validate(group, field, list[field]);
67 | if (err) errors.push({ field, err });
68 | }
69 | return errors;
70 | }
71 | export default validateManyFields;
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | "./src/**/*.{js,jsx,ts,tsx}",
4 | ],
5 | theme: {
6 | extend: {
7 | colors: {
8 | "primary": "#24ab8f",
9 | "primary-dark": "#268d77",
10 | },
11 | animation: {
12 | "loader": "loader 1s linear infinite",
13 | },
14 | keyframes: {
15 | loader: {
16 | "0%": { transform: "rotate(0) scale(1)" },
17 | "50%": { transform: "rotate(180deg) scale(1.5)" },
18 | "100%": { transform: "rotate(360deg) scale(1)" }
19 | }
20 | }
21 | },
22 | },
23 | plugins: [],
24 | }
25 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mern-task-manager",
3 | "version": "1.0.0",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "mern-task-manager",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "devDependencies": {
12 | "concurrently": "^7.1.0"
13 | }
14 | },
15 | "node_modules/ansi-regex": {
16 | "version": "5.0.1",
17 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
18 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
19 | "dev": true,
20 | "engines": {
21 | "node": ">=8"
22 | }
23 | },
24 | "node_modules/ansi-styles": {
25 | "version": "4.3.0",
26 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
27 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
28 | "dev": true,
29 | "dependencies": {
30 | "color-convert": "^2.0.1"
31 | },
32 | "engines": {
33 | "node": ">=8"
34 | },
35 | "funding": {
36 | "url": "https://github.com/chalk/ansi-styles?sponsor=1"
37 | }
38 | },
39 | "node_modules/chalk": {
40 | "version": "4.1.2",
41 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
42 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
43 | "dev": true,
44 | "dependencies": {
45 | "ansi-styles": "^4.1.0",
46 | "supports-color": "^7.1.0"
47 | },
48 | "engines": {
49 | "node": ">=10"
50 | },
51 | "funding": {
52 | "url": "https://github.com/chalk/chalk?sponsor=1"
53 | }
54 | },
55 | "node_modules/chalk/node_modules/supports-color": {
56 | "version": "7.2.0",
57 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
58 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
59 | "dev": true,
60 | "dependencies": {
61 | "has-flag": "^4.0.0"
62 | },
63 | "engines": {
64 | "node": ">=8"
65 | }
66 | },
67 | "node_modules/cliui": {
68 | "version": "7.0.4",
69 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
70 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
71 | "dev": true,
72 | "dependencies": {
73 | "string-width": "^4.2.0",
74 | "strip-ansi": "^6.0.0",
75 | "wrap-ansi": "^7.0.0"
76 | }
77 | },
78 | "node_modules/color-convert": {
79 | "version": "2.0.1",
80 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
81 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
82 | "dev": true,
83 | "dependencies": {
84 | "color-name": "~1.1.4"
85 | },
86 | "engines": {
87 | "node": ">=7.0.0"
88 | }
89 | },
90 | "node_modules/color-name": {
91 | "version": "1.1.4",
92 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
93 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
94 | "dev": true
95 | },
96 | "node_modules/concurrently": {
97 | "version": "7.1.0",
98 | "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.1.0.tgz",
99 | "integrity": "sha512-Bz0tMlYKZRUDqJlNiF/OImojMB9ruKUz6GCfmhFnSapXgPe+3xzY4byqoKG9tUZ7L2PGEUjfLPOLfIX3labnmw==",
100 | "dev": true,
101 | "dependencies": {
102 | "chalk": "^4.1.0",
103 | "date-fns": "^2.16.1",
104 | "lodash": "^4.17.21",
105 | "rxjs": "^6.6.3",
106 | "spawn-command": "^0.0.2-1",
107 | "supports-color": "^8.1.0",
108 | "tree-kill": "^1.2.2",
109 | "yargs": "^16.2.0"
110 | },
111 | "bin": {
112 | "concurrently": "dist/bin/concurrently.js"
113 | },
114 | "engines": {
115 | "node": "^12.20.0 || ^14.13.0 || >=16.0.0"
116 | }
117 | },
118 | "node_modules/date-fns": {
119 | "version": "2.28.0",
120 | "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
121 | "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
122 | "dev": true,
123 | "engines": {
124 | "node": ">=0.11"
125 | },
126 | "funding": {
127 | "type": "opencollective",
128 | "url": "https://opencollective.com/date-fns"
129 | }
130 | },
131 | "node_modules/emoji-regex": {
132 | "version": "8.0.0",
133 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
134 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
135 | "dev": true
136 | },
137 | "node_modules/escalade": {
138 | "version": "3.1.1",
139 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
140 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
141 | "dev": true,
142 | "engines": {
143 | "node": ">=6"
144 | }
145 | },
146 | "node_modules/get-caller-file": {
147 | "version": "2.0.5",
148 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
149 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
150 | "dev": true,
151 | "engines": {
152 | "node": "6.* || 8.* || >= 10.*"
153 | }
154 | },
155 | "node_modules/has-flag": {
156 | "version": "4.0.0",
157 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
158 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
159 | "dev": true,
160 | "engines": {
161 | "node": ">=8"
162 | }
163 | },
164 | "node_modules/is-fullwidth-code-point": {
165 | "version": "3.0.0",
166 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
167 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
168 | "dev": true,
169 | "engines": {
170 | "node": ">=8"
171 | }
172 | },
173 | "node_modules/lodash": {
174 | "version": "4.17.21",
175 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
176 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
177 | "dev": true
178 | },
179 | "node_modules/require-directory": {
180 | "version": "2.1.1",
181 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
182 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
183 | "dev": true,
184 | "engines": {
185 | "node": ">=0.10.0"
186 | }
187 | },
188 | "node_modules/rxjs": {
189 | "version": "6.6.7",
190 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
191 | "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
192 | "dev": true,
193 | "dependencies": {
194 | "tslib": "^1.9.0"
195 | },
196 | "engines": {
197 | "npm": ">=2.0.0"
198 | }
199 | },
200 | "node_modules/spawn-command": {
201 | "version": "0.0.2-1",
202 | "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
203 | "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=",
204 | "dev": true
205 | },
206 | "node_modules/string-width": {
207 | "version": "4.2.3",
208 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
209 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
210 | "dev": true,
211 | "dependencies": {
212 | "emoji-regex": "^8.0.0",
213 | "is-fullwidth-code-point": "^3.0.0",
214 | "strip-ansi": "^6.0.1"
215 | },
216 | "engines": {
217 | "node": ">=8"
218 | }
219 | },
220 | "node_modules/strip-ansi": {
221 | "version": "6.0.1",
222 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
223 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
224 | "dev": true,
225 | "dependencies": {
226 | "ansi-regex": "^5.0.1"
227 | },
228 | "engines": {
229 | "node": ">=8"
230 | }
231 | },
232 | "node_modules/supports-color": {
233 | "version": "8.1.1",
234 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
235 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
236 | "dev": true,
237 | "dependencies": {
238 | "has-flag": "^4.0.0"
239 | },
240 | "engines": {
241 | "node": ">=10"
242 | },
243 | "funding": {
244 | "url": "https://github.com/chalk/supports-color?sponsor=1"
245 | }
246 | },
247 | "node_modules/tree-kill": {
248 | "version": "1.2.2",
249 | "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
250 | "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
251 | "dev": true,
252 | "bin": {
253 | "tree-kill": "cli.js"
254 | }
255 | },
256 | "node_modules/tslib": {
257 | "version": "1.14.1",
258 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
259 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
260 | "dev": true
261 | },
262 | "node_modules/wrap-ansi": {
263 | "version": "7.0.0",
264 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
265 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
266 | "dev": true,
267 | "dependencies": {
268 | "ansi-styles": "^4.0.0",
269 | "string-width": "^4.1.0",
270 | "strip-ansi": "^6.0.0"
271 | },
272 | "engines": {
273 | "node": ">=10"
274 | },
275 | "funding": {
276 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
277 | }
278 | },
279 | "node_modules/y18n": {
280 | "version": "5.0.8",
281 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
282 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
283 | "dev": true,
284 | "engines": {
285 | "node": ">=10"
286 | }
287 | },
288 | "node_modules/yargs": {
289 | "version": "16.2.0",
290 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
291 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
292 | "dev": true,
293 | "dependencies": {
294 | "cliui": "^7.0.2",
295 | "escalade": "^3.1.1",
296 | "get-caller-file": "^2.0.5",
297 | "require-directory": "^2.1.1",
298 | "string-width": "^4.2.0",
299 | "y18n": "^5.0.5",
300 | "yargs-parser": "^20.2.2"
301 | },
302 | "engines": {
303 | "node": ">=10"
304 | }
305 | },
306 | "node_modules/yargs-parser": {
307 | "version": "20.2.9",
308 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
309 | "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
310 | "dev": true,
311 | "engines": {
312 | "node": ">=10"
313 | }
314 | }
315 | },
316 | "dependencies": {
317 | "ansi-regex": {
318 | "version": "5.0.1",
319 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
320 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
321 | "dev": true
322 | },
323 | "ansi-styles": {
324 | "version": "4.3.0",
325 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
326 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
327 | "dev": true,
328 | "requires": {
329 | "color-convert": "^2.0.1"
330 | }
331 | },
332 | "chalk": {
333 | "version": "4.1.2",
334 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
335 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
336 | "dev": true,
337 | "requires": {
338 | "ansi-styles": "^4.1.0",
339 | "supports-color": "^7.1.0"
340 | },
341 | "dependencies": {
342 | "supports-color": {
343 | "version": "7.2.0",
344 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
345 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
346 | "dev": true,
347 | "requires": {
348 | "has-flag": "^4.0.0"
349 | }
350 | }
351 | }
352 | },
353 | "cliui": {
354 | "version": "7.0.4",
355 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
356 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
357 | "dev": true,
358 | "requires": {
359 | "string-width": "^4.2.0",
360 | "strip-ansi": "^6.0.0",
361 | "wrap-ansi": "^7.0.0"
362 | }
363 | },
364 | "color-convert": {
365 | "version": "2.0.1",
366 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
367 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
368 | "dev": true,
369 | "requires": {
370 | "color-name": "~1.1.4"
371 | }
372 | },
373 | "color-name": {
374 | "version": "1.1.4",
375 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
376 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
377 | "dev": true
378 | },
379 | "concurrently": {
380 | "version": "7.1.0",
381 | "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.1.0.tgz",
382 | "integrity": "sha512-Bz0tMlYKZRUDqJlNiF/OImojMB9ruKUz6GCfmhFnSapXgPe+3xzY4byqoKG9tUZ7L2PGEUjfLPOLfIX3labnmw==",
383 | "dev": true,
384 | "requires": {
385 | "chalk": "^4.1.0",
386 | "date-fns": "^2.16.1",
387 | "lodash": "^4.17.21",
388 | "rxjs": "^6.6.3",
389 | "spawn-command": "^0.0.2-1",
390 | "supports-color": "^8.1.0",
391 | "tree-kill": "^1.2.2",
392 | "yargs": "^16.2.0"
393 | }
394 | },
395 | "date-fns": {
396 | "version": "2.28.0",
397 | "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
398 | "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
399 | "dev": true
400 | },
401 | "emoji-regex": {
402 | "version": "8.0.0",
403 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
404 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
405 | "dev": true
406 | },
407 | "escalade": {
408 | "version": "3.1.1",
409 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
410 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
411 | "dev": true
412 | },
413 | "get-caller-file": {
414 | "version": "2.0.5",
415 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
416 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
417 | "dev": true
418 | },
419 | "has-flag": {
420 | "version": "4.0.0",
421 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
422 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
423 | "dev": true
424 | },
425 | "is-fullwidth-code-point": {
426 | "version": "3.0.0",
427 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
428 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
429 | "dev": true
430 | },
431 | "lodash": {
432 | "version": "4.17.21",
433 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
434 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
435 | "dev": true
436 | },
437 | "require-directory": {
438 | "version": "2.1.1",
439 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
440 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
441 | "dev": true
442 | },
443 | "rxjs": {
444 | "version": "6.6.7",
445 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
446 | "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
447 | "dev": true,
448 | "requires": {
449 | "tslib": "^1.9.0"
450 | }
451 | },
452 | "spawn-command": {
453 | "version": "0.0.2-1",
454 | "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
455 | "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=",
456 | "dev": true
457 | },
458 | "string-width": {
459 | "version": "4.2.3",
460 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
461 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
462 | "dev": true,
463 | "requires": {
464 | "emoji-regex": "^8.0.0",
465 | "is-fullwidth-code-point": "^3.0.0",
466 | "strip-ansi": "^6.0.1"
467 | }
468 | },
469 | "strip-ansi": {
470 | "version": "6.0.1",
471 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
472 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
473 | "dev": true,
474 | "requires": {
475 | "ansi-regex": "^5.0.1"
476 | }
477 | },
478 | "supports-color": {
479 | "version": "8.1.1",
480 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
481 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
482 | "dev": true,
483 | "requires": {
484 | "has-flag": "^4.0.0"
485 | }
486 | },
487 | "tree-kill": {
488 | "version": "1.2.2",
489 | "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
490 | "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
491 | "dev": true
492 | },
493 | "tslib": {
494 | "version": "1.14.1",
495 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
496 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
497 | "dev": true
498 | },
499 | "wrap-ansi": {
500 | "version": "7.0.0",
501 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
502 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
503 | "dev": true,
504 | "requires": {
505 | "ansi-styles": "^4.0.0",
506 | "string-width": "^4.1.0",
507 | "strip-ansi": "^6.0.0"
508 | }
509 | },
510 | "y18n": {
511 | "version": "5.0.8",
512 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
513 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
514 | "dev": true
515 | },
516 | "yargs": {
517 | "version": "16.2.0",
518 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
519 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
520 | "dev": true,
521 | "requires": {
522 | "cliui": "^7.0.2",
523 | "escalade": "^3.1.1",
524 | "get-caller-file": "^2.0.5",
525 | "require-directory": "^2.1.1",
526 | "string-width": "^4.2.0",
527 | "y18n": "^5.0.5",
528 | "yargs-parser": "^20.2.2"
529 | }
530 | },
531 | "yargs-parser": {
532 | "version": "20.2.9",
533 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
534 | "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
535 | "dev": true
536 | }
537 | }
538 | }
539 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mern-task-manager",
3 | "version": "1.0.0",
4 | "description": "A MERN application for basic tasks management",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev-server": "npm run dev --prefix backend",
8 | "dev-client": "npm start --prefix frontend",
9 | "dev": "concurrently \"npm run dev-server\" \"npm run dev-client\"",
10 | "install-all": "npm install && npm install --prefix frontend && npm install --prefix backend",
11 | "heroku-postbuild": "npm install --prefix frontend && npm run build --prefix frontend && npm install --prefix backend",
12 | "build": "npm install --prefix frontend && npm run build --prefix frontend && npm install --prefix backend",
13 | "start": "cd backend && node app.js"
14 | },
15 | "keywords": ["MERN", "Tasks","Task manager", "Full stack application"],
16 | "author": "Aayush",
17 | "license": "ISC",
18 | "devDependencies": {
19 | "concurrently": "^7.1.0"
20 | }
21 | }
--------------------------------------------------------------------------------