├── LICENSE ├── Readme.md ├── backend ├── .gitignore ├── config │ └── jwtProvider.js ├── controllers │ ├── analytics.js │ ├── auth.js │ ├── task.js │ └── user.js ├── middlewares │ ├── authorization.js │ └── wrapAsync.js ├── models │ ├── task.js │ └── user.js ├── package-lock.json ├── package.json ├── routes │ ├── analytics.js │ ├── auth.js │ ├── task.js │ └── user.js ├── server.js └── vercel.json └── frontend ├── .eslintrc.cjs ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.jsx ├── assets │ ├── AuthImage.png │ ├── checkbox_select.png │ └── checkbox_unselect.png ├── components │ ├── Loading.jsx │ ├── Model.jsx │ ├── PopUp.jsx │ ├── PrivateRoute.jsx │ └── TaskBox.jsx ├── css │ ├── Analytics.css │ ├── Dashboard.css │ ├── Model.css │ ├── Navbar.css │ ├── Page_Not_Found.css │ ├── PopUp.css │ ├── Register_Login.css │ └── Task.css ├── hooks │ ├── useAddTask.js │ ├── useAddToBoard.js │ ├── useAllTask.js │ ├── useAllTaskFilter.js │ ├── useDeleteTask.js │ ├── useGetAnalytics.js │ ├── useGetTask.js │ ├── useUpdateCategory.js │ └── useUpdateTask.js ├── index.css ├── main.jsx ├── pages │ ├── Analytics.jsx │ ├── Backlog.jsx │ ├── Board.jsx │ ├── Dashboard.jsx │ ├── Done.jsx │ ├── InProgress.jsx │ ├── Navbar.jsx │ ├── PageNotFound.jsx │ ├── Register_Login.jsx │ ├── Settings.jsx │ └── ToDo.jsx ├── redux │ ├── slices │ │ ├── authSlice.js │ │ ├── stateSlice.js │ │ └── taskSlice.js │ └── store.js └── utils │ ├── generateDate.js │ ├── header.js │ └── validate.js ├── vercel.json └── vite.config.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Akash Deep 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # **Task Management Application** 2 | 3 | This is a Task Management Application built with a MERN stack. The front end is developed using React with Vite, and the back-end is built using Express.js with MongoDB for the database. 4 | 5 | --- 6 | 7 |

8 | Visitor count 9 |
10 | 11 | 12 | 13 |

14 | 15 | ## 📜 Project Overview 16 | 17 | _A task management app, where users can organize their personal and team-based tasks, track progress, and share task updates. Users can manage their task boards with features to create, edit, assign, and delete tasks for themselves and other members._ 18 | 19 |

Register Page

20 | 21 | 22 | ![Macbook-Air-localhost (1)](https://github.com/user-attachments/assets/d05bd3e9-e349-469b-a4d2-d828e3e76414) 23 | 24 | 25 |

Dashboard Page

26 | 27 | 28 | ![Macbook-Air-localhost](https://github.com/user-attachments/assets/5643d5a6-b8ad-4516-a3dd-d12b0d9793e3) 29 | 30 | 31 |

Public View

32 | 33 | 34 | ![Macbook-Air-localhost (2)](https://github.com/user-attachments/assets/cde4fcb3-80c4-410d-9763-50281c9b2233) 35 | 36 | 37 | 38 | ## **Table of Contents** 39 | 40 | - [Features](#features) 41 | - [Tech Stack](#tech-stack) 42 | - [Setup Instructions](#setup-instructions) 43 | - [Scripts](#scripts) 44 | - [Live Demo](#live-demo) 45 | - [Author](#author) 46 | - [License](#license) 47 | 48 | --- 49 | 50 | ## **Features** 51 | 52 | - User authentication using JWT. 53 | - Secure password handling with bcrypt.js. 54 | - State management with Redux Toolkit. 55 | - User-friendly interface and Only public page Responsive. 56 | - Toast notifications for feedback. 57 | - RESTful APIs for seamless communication between frontend and backend. 58 | 59 | ### **Core Functionalities** 60 | 61 | - **User Authentication:** 62 | 63 | - Users can register and log in. 64 | - Only authenticated users can create and manage tasks. 65 | 66 | - **Task Management:** 67 | 68 | - Create tasks with properties like priority, optional due dates, categories, and the ability to share tasks with others (read-only public access for shared tasks). 69 | - Update tasks, including title, priority, and due dates. 70 | - Delete tasks. 71 | - Change task statuses across four categories: **Backlog**, **To-Do**, **In-Progress**, and **Done**. 72 | - Automatically highlight overdue tasks with red and completed tasks with green. 73 | 74 | - **User Management:** 75 | 76 | - Users can update their name, email, or password via the settings page. 77 | - Changes to email or password will log users out to ensure security. 78 | 79 | - **Analytics & Filtering:** 80 | 81 | - Review task analytics in a dedicated section. 82 | - Filter tasks by **Today**, **This Week**, or **This Month** (default is the current week). 83 | 84 | - **User-Friendly Interface:** 85 | 86 | - Task titles are truncated on the board for readability, with full titles accessible via tooltips. 87 | - Mandatory fields are marked with a red asterisk (\*). 88 | - Notifications and alerts are provided via toast messages. 89 | 90 | - **Collaboration:** 91 | - Add members to task boards. 92 | - Assign members to tasks during creation. 93 | 94 | ### **Additional Features** 95 | 96 | - Visual indicators for task statuses based on due dates: 97 | - **Red**: Overdue tasks in active categories. 98 | - **Green**: Tasks marked as done. 99 | - Pre-filled user information for seamless updates on the settings page. 100 | 101 | ## 🛠️ **Tech Stack** 102 | 103 | ### **Frontend** 104 | 105 | - **React**: UI library. 106 | - **React Router DOM**: For routing. 107 | - **Redux Toolkit**: State management. 108 | - **React Icons**: Icon library. 109 | - **React Toastify**: Notification handling. 110 | - **Vite**: Frontend build tool. 111 | - **Eslint**: Code quality and linting. 112 | 113 | ### **Backend** 114 | 115 | - **Express.js**: Backend framework. 116 | - **Mongoose**: MongoDB object modeling. 117 | - **JWT**: Secure token-based authentication. 118 | - **Bcrypt.js**: Password encryption. 119 | - **Dotenv**: Environment variable management. 120 | - **Cors**: Cross-origin resource sharing. 121 | 122 | --- 123 | 124 | ## **Setup Instructions** 125 | 126 | ### **Prerequisites** 127 | 128 | - Install **Node.js (18.17.1)**. 129 | - Install **npm** or **yarn**. 130 | - MongoDB database. 131 | 132 | ### **Backend Setup** 133 | 134 | 1. Navigate to the backend directory: 135 | ```bash 136 | cd backend 137 | ``` 138 | 2. Install dependencies: 139 | ```bash 140 | npm install 141 | ``` 142 | 3. Create a `.env` file in the backend directory and add: 143 | ```env 144 | MONGODB_URI=mongodb://127.0.0.1:27017/task_management 145 | FRONTEND_URL=http://localhost:5173 146 | PORT=9000 147 | JWT_SECRET=secret-kJKJllKKJJghLjOiUfcHGkMLgdJlLKDtrdyKLBJbRdesEkj 148 | ``` 149 | 4. Start the server in development mode: 150 | ```bash 151 | npm run dev 152 | ``` 153 | 154 | ### **Frontend Setup** 155 | 156 | 1. Navigate to the frontend directory: 157 | ```bash 158 | cd frontend 159 | ``` 160 | 2. Install dependencies: 161 | ```bash 162 | npm install 163 | ``` 164 | 3. Create a `.env` file in the frontend directory and add: 165 | ```env 166 | VITE_BACKEND_URL=http://localhost:9000 167 | VITE_FRONTEND_URL=http://localhost:5173 168 | ``` 169 | 4. Start the development server: 170 | ```bash 171 | npm run dev 172 | ``` 173 | 5. Open the application in your browser at `http://localhost:5173`. 174 | 175 | --- 176 | 177 | ## **Scripts** 178 | 179 | ### **Frontend** 180 | 181 | | Script | Description | 182 | | ----------------- | ------------------------------------------- | 183 | | `npm run dev` | Starts the development server. | 184 | | `npm run build` | Builds the production version of the app. | 185 | | `npm run lint` | Lints the codebase for errors and warnings. | 186 | | `npm run preview` | Previews the built application. | 187 | 188 | ### **Backend** 189 | 190 | | Script | Description | 191 | | --------------- | ---------------------------------------------------- | 192 | | `npm run start` | Starts the server in production mode. | 193 | | `npm run dev` | Starts the server in development mode using nodemon. | 194 | 195 | --- 196 | 197 | ## **Live Demo** 198 | 199 | Check out the live demo of Task Management here: [Task Management](https://task-management-org.vercel.app) 200 | 201 | ## **Author** 202 | 203 | Akash Deep \ 204 | Email: contact.akashdeep023@gmail.com \ 205 | LinkedIn: https://www.linkedin.com/in/akashdeep023/ 206 | 207 | ## **Contributors** 208 | 209 | We'd like to acknowledge the efforts and contributions of the following individuals: 210 | 211 | - **[Akash Deep](https://github.com/akashdeep023)** - Full Stack development and Project lead. 212 | - **[Ekant Verma](https://github.com/ekantverma)** - Full Stack development. 213 | - **[Anjali Kumari](https://github.com/Anjali17aj)** - Full Stack development. 214 | - **[Shanedra Singh](https://github.com/shanedraSingh/)** - Full Stack development. 215 | 216 | ## **License** 217 | 218 | This project is licensed under the [MIT License](LICENSE). 219 | 220 | --- 221 | 222 | Thank you for checking out my Task Management project! If you have any feedback or suggestions, I would love to hear from you. 223 | Feel free to contribute, report issues, or suggest improvements! 😊 224 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | -------------------------------------------------------------------------------- /backend/config/jwtProvider.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const JWT_SECRET = process.env.JWT_SECRET; 3 | 4 | const generateToken = (userId) => { 5 | const token = jwt.sign({ userId }, JWT_SECRET, { expiresIn: "48h" }); 6 | return token; 7 | }; 8 | const getUserIdFromToken = (token) => { 9 | const decodedToken = jwt.verify(token, JWT_SECRET); 10 | return decodedToken.userId; 11 | }; 12 | 13 | module.exports = { 14 | generateToken, 15 | getUserIdFromToken, 16 | }; 17 | -------------------------------------------------------------------------------- /backend/controllers/analytics.js: -------------------------------------------------------------------------------- 1 | const Task = require("../models/task"); 2 | 3 | const getAnalytics = async (req, res) => { 4 | let backlog = await Task.find({ 5 | $or: [ 6 | { $and: [{ userName: req.user._id }, { category: "backlog" }] }, 7 | { $and: [{ assign: req.user.email }, { category: "backlog" }] }, 8 | ], 9 | }).countDocuments(); 10 | let todo = await Task.find({ 11 | $or: [ 12 | { $and: [{ userName: req.user._id }, { category: "to-do" }] }, 13 | { $and: [{ assign: req.user.email }, { category: "to-do" }] }, 14 | ], 15 | }).countDocuments(); 16 | let inProgress = await Task.find({ 17 | $or: [ 18 | { $and: [{ userName: req.user._id }, { category: "in-progress" }] }, 19 | { $and: [{ assign: req.user.email }, { category: "in-progress" }] }, 20 | ], 21 | }).countDocuments(); 22 | let done = await Task.find({ 23 | $or: [ 24 | { $and: [{ userName: req.user._id }, { category: "done" }] }, 25 | { $and: [{ assign: req.user.email }, { category: "done" }] }, 26 | ], 27 | }).countDocuments(); 28 | let high = await Task.find({ 29 | $or: [ 30 | { 31 | $and: [ 32 | { userName: req.user._id }, 33 | { priority: "High Priority" }, 34 | ], 35 | }, 36 | { 37 | $and: [ 38 | { assign: req.user.email }, 39 | { priority: "High Priority" }, 40 | ], 41 | }, 42 | ], 43 | }).countDocuments(); 44 | let moderate = await Task.find({ 45 | $or: [ 46 | { 47 | $and: [ 48 | { userName: req.user._id }, 49 | { priority: "Moderate Priority" }, 50 | ], 51 | }, 52 | { 53 | $and: [ 54 | { assign: req.user.email }, 55 | { priority: "Moderate Priority" }, 56 | ], 57 | }, 58 | ], 59 | }).countDocuments(); 60 | let low = await Task.find({ 61 | $or: [ 62 | { 63 | $and: [ 64 | { userName: req.user._id }, 65 | { priority: "Low Priority" }, 66 | ], 67 | }, 68 | { 69 | $and: [ 70 | { assign: req.user.email }, 71 | { priority: "Low Priority" }, 72 | ], 73 | }, 74 | ], 75 | }).countDocuments(); 76 | let dueDate = await Task.find({ 77 | $or: [ 78 | { $and: [{ userName: req.user._id }, { dueDate: { $ne: "" } }] }, 79 | { $and: [{ assign: req.user.email }, { dueDate: { $ne: "" } }] }, 80 | ], 81 | }).countDocuments(); 82 | res.status(200).send({ 83 | message: "success", 84 | data: { backlog, todo, inProgress, done, high, moderate, low, dueDate }, 85 | }); 86 | }; 87 | 88 | module.exports = { getAnalytics }; 89 | -------------------------------------------------------------------------------- /backend/controllers/auth.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/user"); 2 | const bcrypt = require("bcryptjs"); 3 | const { generateToken } = require("../config/jwtProvider"); 4 | 5 | const registerUser = async (req, res, next) => { 6 | let { name, email, password } = req.body; 7 | const existingUser = await User.findOne({ email: email }); 8 | if (existingUser) { 9 | return res.status(400).json({ message: `User Already Exist` }); 10 | } 11 | password = bcrypt.hashSync(password, 8); 12 | const userData = new User({ 13 | name, 14 | email, 15 | password, 16 | }); 17 | const user = await userData.save(); 18 | const jwt = generateToken(user._id); 19 | res.status(200).json({ 20 | message: "Registration Successfully", 21 | token: jwt, 22 | }); 23 | }; 24 | 25 | const loginUser = async (req, res) => { 26 | let { email, password } = req.body; 27 | let user = await User.findOne({ email: email }); 28 | if (!user) { 29 | return res.status(404).json({ message: `User Not Found` }); 30 | } 31 | const isPasswordValid = bcrypt.compareSync(password, user.password); 32 | if (!isPasswordValid) { 33 | return res.status(401).json({ message: "Incorrect Password" }); 34 | } 35 | const jwt = generateToken(user._id); 36 | user.password = null; 37 | res.status(200).json({ 38 | message: "Login Successfully", 39 | data: user, 40 | token: jwt, 41 | }); 42 | }; 43 | 44 | module.exports = { registerUser, loginUser }; 45 | -------------------------------------------------------------------------------- /backend/controllers/task.js: -------------------------------------------------------------------------------- 1 | const Task = require("../models/task"); 2 | 3 | const getTask = async (req, res) => { 4 | const { id } = req.params; 5 | let task = await Task.findById(id); 6 | res.status(200).send({ message: "success", data: task }); 7 | }; 8 | 9 | const getAllTask = async (req, res) => { 10 | const daysAgo = (days) => new Date(new Date() - days * 24 * 3600000); 11 | let backlog = await Task.find({ 12 | $or: [ 13 | { 14 | $and: [ 15 | { userName: req.user._id }, 16 | { category: "backlog" }, 17 | { createdAt: { $gte: daysAgo(req.query.days || 365) } }, 18 | ], 19 | }, 20 | { 21 | $and: [ 22 | { assign: req.user.email }, 23 | { category: "backlog" }, 24 | { createdAt: { $gte: daysAgo(req.query.days || 365) } }, 25 | ], 26 | }, 27 | ], 28 | }).populate({ 29 | path: "userName", 30 | select: "name", 31 | }); 32 | let todo = await Task.find({ 33 | $or: [ 34 | { 35 | $and: [ 36 | { userName: req.user._id }, 37 | { category: "to-do" }, 38 | { createdAt: { $gte: daysAgo(req.query.days || 365) } }, 39 | ], 40 | }, 41 | { 42 | $and: [ 43 | { assign: req.user.email }, 44 | { category: "to-do" }, 45 | { createdAt: { $gte: daysAgo(req.query.days || 365) } }, 46 | ], 47 | }, 48 | ], 49 | }).populate({ 50 | path: "userName", 51 | select: "name", 52 | }); 53 | let inProgress = await Task.find({ 54 | $or: [ 55 | { 56 | $and: [ 57 | { userName: req.user._id }, 58 | { category: "in-progress" }, 59 | { createdAt: { $gte: daysAgo(req.query.days || 365) } }, 60 | ], 61 | }, 62 | { 63 | $and: [ 64 | { assign: req.user.email }, 65 | { category: "in-progress" }, 66 | { createdAt: { $gte: daysAgo(req.query.days || 365) } }, 67 | ], 68 | }, 69 | ], 70 | }).populate({ 71 | path: "userName", 72 | select: "name", 73 | }); 74 | let done = await Task.find({ 75 | $or: [ 76 | { 77 | $and: [ 78 | { userName: req.user._id }, 79 | { category: "done" }, 80 | { createdAt: { $gte: daysAgo(req.query.days || 365) } }, 81 | ], 82 | }, 83 | { 84 | $and: [ 85 | { assign: req.user.email }, 86 | { category: "done" }, 87 | { createdAt: { $gte: daysAgo(req.query.days || 365) } }, 88 | ], 89 | }, 90 | ], 91 | }).populate({ 92 | path: "userName", 93 | select: "name", 94 | }); 95 | res.status(200).send({ 96 | message: "success", 97 | data: { backlog, todo, inProgress, done }, 98 | }); 99 | }; 100 | 101 | const addTask = async (req, res) => { 102 | let { title, priority, checklist, dueDate, assign } = req.body; 103 | let userName = req.user._id; 104 | let newTask = new Task({ 105 | title, 106 | priority, 107 | checklist, 108 | dueDate, 109 | userName, 110 | assign, 111 | }); 112 | let createTask = await newTask.save(); 113 | let task = await Task.findById(createTask._id).populate({ 114 | path: "userName", 115 | select: "name", 116 | }); 117 | res.status(200).send({ message: "success", data: task }); 118 | }; 119 | 120 | const updateTask = async (req, res) => { 121 | let { title, priority, checklist, dueDate, assign } = req.body; 122 | const { id } = req.params; 123 | let oldTask = await Task.findById(id); 124 | let updatedTask = await Task.findByIdAndUpdate( 125 | id, 126 | { title, priority, checklist, dueDate, assign }, 127 | { new: true } 128 | ).populate({ 129 | path: "userName", 130 | select: "name", 131 | }); 132 | if ( 133 | (assign == "" && oldTask.assign == req.user.email) || 134 | (assign != "" && 135 | assign != req.user.email && 136 | updatedTask.userName._id.toString() != req.user._id.toString()) 137 | ) { 138 | res.status(200).send({ 139 | message: "success", 140 | data: updatedTask, 141 | removeAssignCategory: oldTask.category, 142 | }); 143 | } else { 144 | res.status(200).send({ message: "success", data: updatedTask }); 145 | } 146 | }; 147 | const deleteTask = async (req, res) => { 148 | const { id } = req.params; 149 | const deleteTask = await Task.findByIdAndDelete(id); 150 | res.status(200).send({ message: "success", data: deleteTask }); 151 | }; 152 | 153 | const updateCategory = async (req, res) => { 154 | const { id } = req.params; 155 | const { category } = req.body; 156 | let task = await Task.findByIdAndUpdate( 157 | id, 158 | { category: category }, 159 | { new: true } 160 | ).populate({ 161 | path: "userName", 162 | select: "name", 163 | }); 164 | 165 | res.status(200).send({ 166 | message: "success", 167 | data: task, 168 | }); 169 | }; 170 | 171 | module.exports = { 172 | getTask, 173 | getAllTask, 174 | addTask, 175 | updateTask, 176 | deleteTask, 177 | updateCategory, 178 | }; 179 | -------------------------------------------------------------------------------- /backend/controllers/user.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/user"); 2 | const bcrypt = require("bcryptjs"); 3 | 4 | const getAuthUser = async (req, res) => { 5 | if (!req.user) { 6 | return res.status(404).json({ message: `User Not Found` }); 7 | } 8 | res.status(200).json({ 9 | data: req.user, 10 | }); 11 | }; 12 | 13 | const updateUser = async (req, res) => { 14 | let { name, email, oldPassword, newPassword } = req.body; 15 | if (req.user.email != email) { 16 | const existingUser = await User.findOne({ email: email }); 17 | if (existingUser) { 18 | return res.status(400).json({ message: `Email Already Used` }); 19 | } 20 | } 21 | const user = await User.findById(req.user.id); 22 | const passwordEqual = bcrypt.compareSync(oldPassword, user.password); 23 | if (passwordEqual) { 24 | newPassword = bcrypt.hashSync(newPassword, 8); 25 | const userData = await User.findByIdAndUpdate( 26 | req.user.id, 27 | { 28 | name: name, 29 | email: email, 30 | password: newPassword, 31 | }, 32 | { new: true } 33 | ); 34 | userData.password = null; 35 | res.status(200).json({ 36 | message: "success", 37 | data: userData, 38 | }); 39 | } else { 40 | return res.status(400).json({ message: "Password is incorrect" }); 41 | } 42 | }; 43 | 44 | const getAllUsers = async (req, res) => { 45 | const allUsers = await User.find({ _id: { $ne: req.user._id } }) 46 | .select("-password") 47 | .sort({ _id: -1 }); 48 | res.status(200).send({ data: allUsers }); 49 | }; 50 | const updateBoard = async (req, res) => { 51 | let { email } = req.body; 52 | const userData = await User.findByIdAndUpdate( 53 | req.user.id, 54 | { $push: { board: email } }, 55 | { new: true } 56 | ); 57 | userData.password = null; 58 | res.status(200).json({ 59 | message: "success", 60 | data: userData, 61 | email: email, 62 | }); 63 | }; 64 | 65 | module.exports = { getAuthUser, updateUser, getAllUsers, updateBoard }; 66 | -------------------------------------------------------------------------------- /backend/middlewares/authorization.js: -------------------------------------------------------------------------------- 1 | const { getUserIdFromToken } = require("../config/jwtProvider"); 2 | const User = require("../models/user"); 3 | const wrapAsync = require("./wrapAsync"); 4 | 5 | const authorization = wrapAsync(async (req, res, next) => { 6 | const token = req.headers.authorization?.split(" ")[1]; 7 | if (!token) { 8 | return res.status(404).send({ message: "Token not found" }); 9 | } 10 | const userId = getUserIdFromToken(token); 11 | if (userId) { 12 | req.user = await User.findById(userId).select("-password"); 13 | next(); 14 | } else { 15 | return res.status(404).send({ message: "Something went wrong" }); 16 | } 17 | }); 18 | 19 | module.exports = { authorization }; 20 | -------------------------------------------------------------------------------- /backend/middlewares/wrapAsync.js: -------------------------------------------------------------------------------- 1 | module.exports = (fn) => { 2 | return (req, res, next) => { 3 | fn(req, res, next).catch(next); 4 | }; 5 | }; 6 | -------------------------------------------------------------------------------- /backend/models/task.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const taskSchema = new mongoose.Schema( 4 | { 5 | title: { 6 | type: String, 7 | required: true, 8 | unique: true, 9 | trim: true, 10 | }, 11 | priority: { 12 | type: String, 13 | required: true, 14 | }, 15 | category: { 16 | type: String, 17 | required: true, 18 | default: "to-do", 19 | }, 20 | checklist: [ 21 | { 22 | name: { 23 | type: String, 24 | trim: true, 25 | required: true, 26 | }, 27 | isDone: { 28 | type: Boolean, 29 | default: false, 30 | required: true, 31 | }, 32 | }, 33 | ], 34 | userName: { 35 | type: mongoose.Schema.ObjectId, 36 | ref: "User", 37 | required: true, 38 | }, 39 | assign: String, 40 | 41 | dueDate: Date, 42 | }, 43 | { 44 | timestamps: true, 45 | } 46 | ); 47 | 48 | const Task = mongoose.model("Task", taskSchema); 49 | module.exports = Task; 50 | -------------------------------------------------------------------------------- /backend/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const userSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: true, 8 | }, 9 | password: { 10 | type: String, 11 | required: true, 12 | }, 13 | email: { 14 | type: String, 15 | required: true, 16 | unique: true, 17 | }, 18 | board: [String], 19 | }, 20 | { 21 | timestamps: true, 22 | } 23 | ); 24 | 25 | const User = mongoose.model("User", userSchema); 26 | module.exports = User; 27 | -------------------------------------------------------------------------------- /backend/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "backend", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "bcryptjs": "^2.4.3", 13 | "cors": "^2.8.5", 14 | "dotenv": "^16.4.5", 15 | "express": "^4.19.2", 16 | "jsonwebtoken": "^9.0.2", 17 | "mongoose": "^8.4.1" 18 | }, 19 | "engines": { 20 | "node": "18.17.1" 21 | } 22 | }, 23 | "node_modules/@mongodb-js/saslprep": { 24 | "version": "1.1.7", 25 | "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz", 26 | "integrity": "sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==", 27 | "dependencies": { 28 | "sparse-bitfield": "^3.0.3" 29 | } 30 | }, 31 | "node_modules/@types/webidl-conversions": { 32 | "version": "7.0.3", 33 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", 34 | "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" 35 | }, 36 | "node_modules/@types/whatwg-url": { 37 | "version": "11.0.5", 38 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", 39 | "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", 40 | "dependencies": { 41 | "@types/webidl-conversions": "*" 42 | } 43 | }, 44 | "node_modules/accepts": { 45 | "version": "1.3.8", 46 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 47 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 48 | "dependencies": { 49 | "mime-types": "~2.1.34", 50 | "negotiator": "0.6.3" 51 | }, 52 | "engines": { 53 | "node": ">= 0.6" 54 | } 55 | }, 56 | "node_modules/array-flatten": { 57 | "version": "1.1.1", 58 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 59 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 60 | }, 61 | "node_modules/bcryptjs": { 62 | "version": "2.4.3", 63 | "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", 64 | "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" 65 | }, 66 | "node_modules/body-parser": { 67 | "version": "1.20.2", 68 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", 69 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", 70 | "dependencies": { 71 | "bytes": "3.1.2", 72 | "content-type": "~1.0.5", 73 | "debug": "2.6.9", 74 | "depd": "2.0.0", 75 | "destroy": "1.2.0", 76 | "http-errors": "2.0.0", 77 | "iconv-lite": "0.4.24", 78 | "on-finished": "2.4.1", 79 | "qs": "6.11.0", 80 | "raw-body": "2.5.2", 81 | "type-is": "~1.6.18", 82 | "unpipe": "1.0.0" 83 | }, 84 | "engines": { 85 | "node": ">= 0.8", 86 | "npm": "1.2.8000 || >= 1.4.16" 87 | } 88 | }, 89 | "node_modules/bson": { 90 | "version": "6.7.0", 91 | "resolved": "https://registry.npmjs.org/bson/-/bson-6.7.0.tgz", 92 | "integrity": "sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==", 93 | "engines": { 94 | "node": ">=16.20.1" 95 | } 96 | }, 97 | "node_modules/buffer-equal-constant-time": { 98 | "version": "1.0.1", 99 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 100 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" 101 | }, 102 | "node_modules/bytes": { 103 | "version": "3.1.2", 104 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 105 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 106 | "engines": { 107 | "node": ">= 0.8" 108 | } 109 | }, 110 | "node_modules/call-bind": { 111 | "version": "1.0.7", 112 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 113 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 114 | "dependencies": { 115 | "es-define-property": "^1.0.0", 116 | "es-errors": "^1.3.0", 117 | "function-bind": "^1.1.2", 118 | "get-intrinsic": "^1.2.4", 119 | "set-function-length": "^1.2.1" 120 | }, 121 | "engines": { 122 | "node": ">= 0.4" 123 | }, 124 | "funding": { 125 | "url": "https://github.com/sponsors/ljharb" 126 | } 127 | }, 128 | "node_modules/content-disposition": { 129 | "version": "0.5.4", 130 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 131 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 132 | "dependencies": { 133 | "safe-buffer": "5.2.1" 134 | }, 135 | "engines": { 136 | "node": ">= 0.6" 137 | } 138 | }, 139 | "node_modules/content-type": { 140 | "version": "1.0.5", 141 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 142 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 143 | "engines": { 144 | "node": ">= 0.6" 145 | } 146 | }, 147 | "node_modules/cookie": { 148 | "version": "0.6.0", 149 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", 150 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", 151 | "engines": { 152 | "node": ">= 0.6" 153 | } 154 | }, 155 | "node_modules/cookie-signature": { 156 | "version": "1.0.6", 157 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 158 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 159 | }, 160 | "node_modules/cors": { 161 | "version": "2.8.5", 162 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 163 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 164 | "dependencies": { 165 | "object-assign": "^4", 166 | "vary": "^1" 167 | }, 168 | "engines": { 169 | "node": ">= 0.10" 170 | } 171 | }, 172 | "node_modules/debug": { 173 | "version": "2.6.9", 174 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 175 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 176 | "dependencies": { 177 | "ms": "2.0.0" 178 | } 179 | }, 180 | "node_modules/define-data-property": { 181 | "version": "1.1.4", 182 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 183 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 184 | "dependencies": { 185 | "es-define-property": "^1.0.0", 186 | "es-errors": "^1.3.0", 187 | "gopd": "^1.0.1" 188 | }, 189 | "engines": { 190 | "node": ">= 0.4" 191 | }, 192 | "funding": { 193 | "url": "https://github.com/sponsors/ljharb" 194 | } 195 | }, 196 | "node_modules/depd": { 197 | "version": "2.0.0", 198 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 199 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 200 | "engines": { 201 | "node": ">= 0.8" 202 | } 203 | }, 204 | "node_modules/destroy": { 205 | "version": "1.2.0", 206 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 207 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 208 | "engines": { 209 | "node": ">= 0.8", 210 | "npm": "1.2.8000 || >= 1.4.16" 211 | } 212 | }, 213 | "node_modules/dotenv": { 214 | "version": "16.4.5", 215 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 216 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 217 | "engines": { 218 | "node": ">=12" 219 | }, 220 | "funding": { 221 | "url": "https://dotenvx.com" 222 | } 223 | }, 224 | "node_modules/ecdsa-sig-formatter": { 225 | "version": "1.0.11", 226 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 227 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 228 | "dependencies": { 229 | "safe-buffer": "^5.0.1" 230 | } 231 | }, 232 | "node_modules/ee-first": { 233 | "version": "1.1.1", 234 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 235 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 236 | }, 237 | "node_modules/encodeurl": { 238 | "version": "1.0.2", 239 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 240 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 241 | "engines": { 242 | "node": ">= 0.8" 243 | } 244 | }, 245 | "node_modules/es-define-property": { 246 | "version": "1.0.0", 247 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 248 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 249 | "dependencies": { 250 | "get-intrinsic": "^1.2.4" 251 | }, 252 | "engines": { 253 | "node": ">= 0.4" 254 | } 255 | }, 256 | "node_modules/es-errors": { 257 | "version": "1.3.0", 258 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 259 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 260 | "engines": { 261 | "node": ">= 0.4" 262 | } 263 | }, 264 | "node_modules/escape-html": { 265 | "version": "1.0.3", 266 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 267 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 268 | }, 269 | "node_modules/etag": { 270 | "version": "1.8.1", 271 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 272 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 273 | "engines": { 274 | "node": ">= 0.6" 275 | } 276 | }, 277 | "node_modules/express": { 278 | "version": "4.19.2", 279 | "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", 280 | "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", 281 | "dependencies": { 282 | "accepts": "~1.3.8", 283 | "array-flatten": "1.1.1", 284 | "body-parser": "1.20.2", 285 | "content-disposition": "0.5.4", 286 | "content-type": "~1.0.4", 287 | "cookie": "0.6.0", 288 | "cookie-signature": "1.0.6", 289 | "debug": "2.6.9", 290 | "depd": "2.0.0", 291 | "encodeurl": "~1.0.2", 292 | "escape-html": "~1.0.3", 293 | "etag": "~1.8.1", 294 | "finalhandler": "1.2.0", 295 | "fresh": "0.5.2", 296 | "http-errors": "2.0.0", 297 | "merge-descriptors": "1.0.1", 298 | "methods": "~1.1.2", 299 | "on-finished": "2.4.1", 300 | "parseurl": "~1.3.3", 301 | "path-to-regexp": "0.1.7", 302 | "proxy-addr": "~2.0.7", 303 | "qs": "6.11.0", 304 | "range-parser": "~1.2.1", 305 | "safe-buffer": "5.2.1", 306 | "send": "0.18.0", 307 | "serve-static": "1.15.0", 308 | "setprototypeof": "1.2.0", 309 | "statuses": "2.0.1", 310 | "type-is": "~1.6.18", 311 | "utils-merge": "1.0.1", 312 | "vary": "~1.1.2" 313 | }, 314 | "engines": { 315 | "node": ">= 0.10.0" 316 | } 317 | }, 318 | "node_modules/finalhandler": { 319 | "version": "1.2.0", 320 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 321 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 322 | "dependencies": { 323 | "debug": "2.6.9", 324 | "encodeurl": "~1.0.2", 325 | "escape-html": "~1.0.3", 326 | "on-finished": "2.4.1", 327 | "parseurl": "~1.3.3", 328 | "statuses": "2.0.1", 329 | "unpipe": "~1.0.0" 330 | }, 331 | "engines": { 332 | "node": ">= 0.8" 333 | } 334 | }, 335 | "node_modules/forwarded": { 336 | "version": "0.2.0", 337 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 338 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 339 | "engines": { 340 | "node": ">= 0.6" 341 | } 342 | }, 343 | "node_modules/fresh": { 344 | "version": "0.5.2", 345 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 346 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 347 | "engines": { 348 | "node": ">= 0.6" 349 | } 350 | }, 351 | "node_modules/function-bind": { 352 | "version": "1.1.2", 353 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 354 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 355 | "funding": { 356 | "url": "https://github.com/sponsors/ljharb" 357 | } 358 | }, 359 | "node_modules/get-intrinsic": { 360 | "version": "1.2.4", 361 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 362 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 363 | "dependencies": { 364 | "es-errors": "^1.3.0", 365 | "function-bind": "^1.1.2", 366 | "has-proto": "^1.0.1", 367 | "has-symbols": "^1.0.3", 368 | "hasown": "^2.0.0" 369 | }, 370 | "engines": { 371 | "node": ">= 0.4" 372 | }, 373 | "funding": { 374 | "url": "https://github.com/sponsors/ljharb" 375 | } 376 | }, 377 | "node_modules/gopd": { 378 | "version": "1.0.1", 379 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 380 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 381 | "dependencies": { 382 | "get-intrinsic": "^1.1.3" 383 | }, 384 | "funding": { 385 | "url": "https://github.com/sponsors/ljharb" 386 | } 387 | }, 388 | "node_modules/has-property-descriptors": { 389 | "version": "1.0.2", 390 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 391 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 392 | "dependencies": { 393 | "es-define-property": "^1.0.0" 394 | }, 395 | "funding": { 396 | "url": "https://github.com/sponsors/ljharb" 397 | } 398 | }, 399 | "node_modules/has-proto": { 400 | "version": "1.0.3", 401 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 402 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 403 | "engines": { 404 | "node": ">= 0.4" 405 | }, 406 | "funding": { 407 | "url": "https://github.com/sponsors/ljharb" 408 | } 409 | }, 410 | "node_modules/has-symbols": { 411 | "version": "1.0.3", 412 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 413 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 414 | "engines": { 415 | "node": ">= 0.4" 416 | }, 417 | "funding": { 418 | "url": "https://github.com/sponsors/ljharb" 419 | } 420 | }, 421 | "node_modules/hasown": { 422 | "version": "2.0.2", 423 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 424 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 425 | "dependencies": { 426 | "function-bind": "^1.1.2" 427 | }, 428 | "engines": { 429 | "node": ">= 0.4" 430 | } 431 | }, 432 | "node_modules/http-errors": { 433 | "version": "2.0.0", 434 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 435 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 436 | "dependencies": { 437 | "depd": "2.0.0", 438 | "inherits": "2.0.4", 439 | "setprototypeof": "1.2.0", 440 | "statuses": "2.0.1", 441 | "toidentifier": "1.0.1" 442 | }, 443 | "engines": { 444 | "node": ">= 0.8" 445 | } 446 | }, 447 | "node_modules/iconv-lite": { 448 | "version": "0.4.24", 449 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 450 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 451 | "dependencies": { 452 | "safer-buffer": ">= 2.1.2 < 3" 453 | }, 454 | "engines": { 455 | "node": ">=0.10.0" 456 | } 457 | }, 458 | "node_modules/inherits": { 459 | "version": "2.0.4", 460 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 461 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 462 | }, 463 | "node_modules/ipaddr.js": { 464 | "version": "1.9.1", 465 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 466 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 467 | "engines": { 468 | "node": ">= 0.10" 469 | } 470 | }, 471 | "node_modules/jsonwebtoken": { 472 | "version": "9.0.2", 473 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", 474 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", 475 | "dependencies": { 476 | "jws": "^3.2.2", 477 | "lodash.includes": "^4.3.0", 478 | "lodash.isboolean": "^3.0.3", 479 | "lodash.isinteger": "^4.0.4", 480 | "lodash.isnumber": "^3.0.3", 481 | "lodash.isplainobject": "^4.0.6", 482 | "lodash.isstring": "^4.0.1", 483 | "lodash.once": "^4.0.0", 484 | "ms": "^2.1.1", 485 | "semver": "^7.5.4" 486 | }, 487 | "engines": { 488 | "node": ">=12", 489 | "npm": ">=6" 490 | } 491 | }, 492 | "node_modules/jsonwebtoken/node_modules/ms": { 493 | "version": "2.1.3", 494 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 495 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 496 | }, 497 | "node_modules/jwa": { 498 | "version": "1.4.1", 499 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 500 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 501 | "dependencies": { 502 | "buffer-equal-constant-time": "1.0.1", 503 | "ecdsa-sig-formatter": "1.0.11", 504 | "safe-buffer": "^5.0.1" 505 | } 506 | }, 507 | "node_modules/jws": { 508 | "version": "3.2.2", 509 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 510 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 511 | "dependencies": { 512 | "jwa": "^1.4.1", 513 | "safe-buffer": "^5.0.1" 514 | } 515 | }, 516 | "node_modules/kareem": { 517 | "version": "2.6.3", 518 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", 519 | "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", 520 | "engines": { 521 | "node": ">=12.0.0" 522 | } 523 | }, 524 | "node_modules/lodash.includes": { 525 | "version": "4.3.0", 526 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 527 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" 528 | }, 529 | "node_modules/lodash.isboolean": { 530 | "version": "3.0.3", 531 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 532 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" 533 | }, 534 | "node_modules/lodash.isinteger": { 535 | "version": "4.0.4", 536 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 537 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" 538 | }, 539 | "node_modules/lodash.isnumber": { 540 | "version": "3.0.3", 541 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 542 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" 543 | }, 544 | "node_modules/lodash.isplainobject": { 545 | "version": "4.0.6", 546 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 547 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" 548 | }, 549 | "node_modules/lodash.isstring": { 550 | "version": "4.0.1", 551 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 552 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" 553 | }, 554 | "node_modules/lodash.once": { 555 | "version": "4.1.1", 556 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 557 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" 558 | }, 559 | "node_modules/media-typer": { 560 | "version": "0.3.0", 561 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 562 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 563 | "engines": { 564 | "node": ">= 0.6" 565 | } 566 | }, 567 | "node_modules/memory-pager": { 568 | "version": "1.5.0", 569 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 570 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" 571 | }, 572 | "node_modules/merge-descriptors": { 573 | "version": "1.0.1", 574 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 575 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 576 | }, 577 | "node_modules/methods": { 578 | "version": "1.1.2", 579 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 580 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 581 | "engines": { 582 | "node": ">= 0.6" 583 | } 584 | }, 585 | "node_modules/mime": { 586 | "version": "1.6.0", 587 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 588 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 589 | "bin": { 590 | "mime": "cli.js" 591 | }, 592 | "engines": { 593 | "node": ">=4" 594 | } 595 | }, 596 | "node_modules/mime-db": { 597 | "version": "1.52.0", 598 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 599 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 600 | "engines": { 601 | "node": ">= 0.6" 602 | } 603 | }, 604 | "node_modules/mime-types": { 605 | "version": "2.1.35", 606 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 607 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 608 | "dependencies": { 609 | "mime-db": "1.52.0" 610 | }, 611 | "engines": { 612 | "node": ">= 0.6" 613 | } 614 | }, 615 | "node_modules/mongodb": { 616 | "version": "6.6.2", 617 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.6.2.tgz", 618 | "integrity": "sha512-ZF9Ugo2JCG/GfR7DEb4ypfyJJyiKbg5qBYKRintebj8+DNS33CyGMkWbrS9lara+u+h+yEOGSRiLhFO/g1s1aw==", 619 | "dependencies": { 620 | "@mongodb-js/saslprep": "^1.1.5", 621 | "bson": "^6.7.0", 622 | "mongodb-connection-string-url": "^3.0.0" 623 | }, 624 | "engines": { 625 | "node": ">=16.20.1" 626 | }, 627 | "peerDependencies": { 628 | "@aws-sdk/credential-providers": "^3.188.0", 629 | "@mongodb-js/zstd": "^1.1.0", 630 | "gcp-metadata": "^5.2.0", 631 | "kerberos": "^2.0.1", 632 | "mongodb-client-encryption": ">=6.0.0 <7", 633 | "snappy": "^7.2.2", 634 | "socks": "^2.7.1" 635 | }, 636 | "peerDependenciesMeta": { 637 | "@aws-sdk/credential-providers": { 638 | "optional": true 639 | }, 640 | "@mongodb-js/zstd": { 641 | "optional": true 642 | }, 643 | "gcp-metadata": { 644 | "optional": true 645 | }, 646 | "kerberos": { 647 | "optional": true 648 | }, 649 | "mongodb-client-encryption": { 650 | "optional": true 651 | }, 652 | "snappy": { 653 | "optional": true 654 | }, 655 | "socks": { 656 | "optional": true 657 | } 658 | } 659 | }, 660 | "node_modules/mongodb-connection-string-url": { 661 | "version": "3.0.1", 662 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", 663 | "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", 664 | "dependencies": { 665 | "@types/whatwg-url": "^11.0.2", 666 | "whatwg-url": "^13.0.0" 667 | } 668 | }, 669 | "node_modules/mongoose": { 670 | "version": "8.4.1", 671 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.4.1.tgz", 672 | "integrity": "sha512-odQ2WEWGL3hb0Qex+QMN4eH6D34WdMEw7F1If2MGABApSDmG9cMmqv/G1H6WsXmuaH9mkuuadW/WbLE5+tHJwA==", 673 | "dependencies": { 674 | "bson": "^6.7.0", 675 | "kareem": "2.6.3", 676 | "mongodb": "6.6.2", 677 | "mpath": "0.9.0", 678 | "mquery": "5.0.0", 679 | "ms": "2.1.3", 680 | "sift": "17.1.3" 681 | }, 682 | "engines": { 683 | "node": ">=16.20.1" 684 | }, 685 | "funding": { 686 | "type": "opencollective", 687 | "url": "https://opencollective.com/mongoose" 688 | } 689 | }, 690 | "node_modules/mongoose/node_modules/ms": { 691 | "version": "2.1.3", 692 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 693 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 694 | }, 695 | "node_modules/mpath": { 696 | "version": "0.9.0", 697 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", 698 | "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", 699 | "engines": { 700 | "node": ">=4.0.0" 701 | } 702 | }, 703 | "node_modules/mquery": { 704 | "version": "5.0.0", 705 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", 706 | "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", 707 | "dependencies": { 708 | "debug": "4.x" 709 | }, 710 | "engines": { 711 | "node": ">=14.0.0" 712 | } 713 | }, 714 | "node_modules/mquery/node_modules/debug": { 715 | "version": "4.3.5", 716 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", 717 | "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", 718 | "dependencies": { 719 | "ms": "2.1.2" 720 | }, 721 | "engines": { 722 | "node": ">=6.0" 723 | }, 724 | "peerDependenciesMeta": { 725 | "supports-color": { 726 | "optional": true 727 | } 728 | } 729 | }, 730 | "node_modules/mquery/node_modules/ms": { 731 | "version": "2.1.2", 732 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 733 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 734 | }, 735 | "node_modules/ms": { 736 | "version": "2.0.0", 737 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 738 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 739 | }, 740 | "node_modules/negotiator": { 741 | "version": "0.6.3", 742 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 743 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 744 | "engines": { 745 | "node": ">= 0.6" 746 | } 747 | }, 748 | "node_modules/object-assign": { 749 | "version": "4.1.1", 750 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 751 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 752 | "engines": { 753 | "node": ">=0.10.0" 754 | } 755 | }, 756 | "node_modules/object-inspect": { 757 | "version": "1.13.1", 758 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", 759 | "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", 760 | "funding": { 761 | "url": "https://github.com/sponsors/ljharb" 762 | } 763 | }, 764 | "node_modules/on-finished": { 765 | "version": "2.4.1", 766 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 767 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 768 | "dependencies": { 769 | "ee-first": "1.1.1" 770 | }, 771 | "engines": { 772 | "node": ">= 0.8" 773 | } 774 | }, 775 | "node_modules/parseurl": { 776 | "version": "1.3.3", 777 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 778 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 779 | "engines": { 780 | "node": ">= 0.8" 781 | } 782 | }, 783 | "node_modules/path-to-regexp": { 784 | "version": "0.1.7", 785 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 786 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 787 | }, 788 | "node_modules/proxy-addr": { 789 | "version": "2.0.7", 790 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 791 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 792 | "dependencies": { 793 | "forwarded": "0.2.0", 794 | "ipaddr.js": "1.9.1" 795 | }, 796 | "engines": { 797 | "node": ">= 0.10" 798 | } 799 | }, 800 | "node_modules/punycode": { 801 | "version": "2.3.1", 802 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 803 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 804 | "engines": { 805 | "node": ">=6" 806 | } 807 | }, 808 | "node_modules/qs": { 809 | "version": "6.11.0", 810 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 811 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 812 | "dependencies": { 813 | "side-channel": "^1.0.4" 814 | }, 815 | "engines": { 816 | "node": ">=0.6" 817 | }, 818 | "funding": { 819 | "url": "https://github.com/sponsors/ljharb" 820 | } 821 | }, 822 | "node_modules/range-parser": { 823 | "version": "1.2.1", 824 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 825 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 826 | "engines": { 827 | "node": ">= 0.6" 828 | } 829 | }, 830 | "node_modules/raw-body": { 831 | "version": "2.5.2", 832 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 833 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 834 | "dependencies": { 835 | "bytes": "3.1.2", 836 | "http-errors": "2.0.0", 837 | "iconv-lite": "0.4.24", 838 | "unpipe": "1.0.0" 839 | }, 840 | "engines": { 841 | "node": ">= 0.8" 842 | } 843 | }, 844 | "node_modules/safe-buffer": { 845 | "version": "5.2.1", 846 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 847 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 848 | "funding": [ 849 | { 850 | "type": "github", 851 | "url": "https://github.com/sponsors/feross" 852 | }, 853 | { 854 | "type": "patreon", 855 | "url": "https://www.patreon.com/feross" 856 | }, 857 | { 858 | "type": "consulting", 859 | "url": "https://feross.org/support" 860 | } 861 | ] 862 | }, 863 | "node_modules/safer-buffer": { 864 | "version": "2.1.2", 865 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 866 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 867 | }, 868 | "node_modules/semver": { 869 | "version": "7.6.2", 870 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", 871 | "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", 872 | "bin": { 873 | "semver": "bin/semver.js" 874 | }, 875 | "engines": { 876 | "node": ">=10" 877 | } 878 | }, 879 | "node_modules/send": { 880 | "version": "0.18.0", 881 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 882 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 883 | "dependencies": { 884 | "debug": "2.6.9", 885 | "depd": "2.0.0", 886 | "destroy": "1.2.0", 887 | "encodeurl": "~1.0.2", 888 | "escape-html": "~1.0.3", 889 | "etag": "~1.8.1", 890 | "fresh": "0.5.2", 891 | "http-errors": "2.0.0", 892 | "mime": "1.6.0", 893 | "ms": "2.1.3", 894 | "on-finished": "2.4.1", 895 | "range-parser": "~1.2.1", 896 | "statuses": "2.0.1" 897 | }, 898 | "engines": { 899 | "node": ">= 0.8.0" 900 | } 901 | }, 902 | "node_modules/send/node_modules/ms": { 903 | "version": "2.1.3", 904 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 905 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 906 | }, 907 | "node_modules/serve-static": { 908 | "version": "1.15.0", 909 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 910 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 911 | "dependencies": { 912 | "encodeurl": "~1.0.2", 913 | "escape-html": "~1.0.3", 914 | "parseurl": "~1.3.3", 915 | "send": "0.18.0" 916 | }, 917 | "engines": { 918 | "node": ">= 0.8.0" 919 | } 920 | }, 921 | "node_modules/set-function-length": { 922 | "version": "1.2.2", 923 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 924 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 925 | "dependencies": { 926 | "define-data-property": "^1.1.4", 927 | "es-errors": "^1.3.0", 928 | "function-bind": "^1.1.2", 929 | "get-intrinsic": "^1.2.4", 930 | "gopd": "^1.0.1", 931 | "has-property-descriptors": "^1.0.2" 932 | }, 933 | "engines": { 934 | "node": ">= 0.4" 935 | } 936 | }, 937 | "node_modules/setprototypeof": { 938 | "version": "1.2.0", 939 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 940 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 941 | }, 942 | "node_modules/side-channel": { 943 | "version": "1.0.6", 944 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 945 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 946 | "dependencies": { 947 | "call-bind": "^1.0.7", 948 | "es-errors": "^1.3.0", 949 | "get-intrinsic": "^1.2.4", 950 | "object-inspect": "^1.13.1" 951 | }, 952 | "engines": { 953 | "node": ">= 0.4" 954 | }, 955 | "funding": { 956 | "url": "https://github.com/sponsors/ljharb" 957 | } 958 | }, 959 | "node_modules/sift": { 960 | "version": "17.1.3", 961 | "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", 962 | "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" 963 | }, 964 | "node_modules/sparse-bitfield": { 965 | "version": "3.0.3", 966 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 967 | "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", 968 | "dependencies": { 969 | "memory-pager": "^1.0.2" 970 | } 971 | }, 972 | "node_modules/statuses": { 973 | "version": "2.0.1", 974 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 975 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 976 | "engines": { 977 | "node": ">= 0.8" 978 | } 979 | }, 980 | "node_modules/toidentifier": { 981 | "version": "1.0.1", 982 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 983 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 984 | "engines": { 985 | "node": ">=0.6" 986 | } 987 | }, 988 | "node_modules/tr46": { 989 | "version": "4.1.1", 990 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", 991 | "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", 992 | "dependencies": { 993 | "punycode": "^2.3.0" 994 | }, 995 | "engines": { 996 | "node": ">=14" 997 | } 998 | }, 999 | "node_modules/type-is": { 1000 | "version": "1.6.18", 1001 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1002 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1003 | "dependencies": { 1004 | "media-typer": "0.3.0", 1005 | "mime-types": "~2.1.24" 1006 | }, 1007 | "engines": { 1008 | "node": ">= 0.6" 1009 | } 1010 | }, 1011 | "node_modules/unpipe": { 1012 | "version": "1.0.0", 1013 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1014 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1015 | "engines": { 1016 | "node": ">= 0.8" 1017 | } 1018 | }, 1019 | "node_modules/utils-merge": { 1020 | "version": "1.0.1", 1021 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1022 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 1023 | "engines": { 1024 | "node": ">= 0.4.0" 1025 | } 1026 | }, 1027 | "node_modules/vary": { 1028 | "version": "1.1.2", 1029 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1030 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1031 | "engines": { 1032 | "node": ">= 0.8" 1033 | } 1034 | }, 1035 | "node_modules/webidl-conversions": { 1036 | "version": "7.0.0", 1037 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 1038 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", 1039 | "engines": { 1040 | "node": ">=12" 1041 | } 1042 | }, 1043 | "node_modules/whatwg-url": { 1044 | "version": "13.0.0", 1045 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", 1046 | "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", 1047 | "dependencies": { 1048 | "tr46": "^4.1.1", 1049 | "webidl-conversions": "^7.0.0" 1050 | }, 1051 | "engines": { 1052 | "node": ">=16" 1053 | } 1054 | } 1055 | } 1056 | } 1057 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "Task_Management", 5 | "main": "server.js", 6 | "engines": { 7 | "node": "18.17.1" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "start": "node server.js", 12 | "dev": "nodemon server.js" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "bcryptjs": "^2.4.3", 18 | "cors": "^2.8.5", 19 | "dotenv": "^16.4.5", 20 | "express": "^4.19.2", 21 | "jsonwebtoken": "^9.0.2", 22 | "mongoose": "^8.4.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/routes/analytics.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { getAnalytics } = require("../controllers/analytics"); 4 | const wrapAsync = require("../middlewares/wrapAsync"); 5 | const { authorization } = require("../middlewares/authorization"); 6 | 7 | router.get("/", authorization, wrapAsync(getAnalytics)); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /backend/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const authControllers = require("../controllers/auth"); 4 | const wrapAsync = require("../middlewares/wrapAsync"); 5 | 6 | router.post("/register", wrapAsync(authControllers.registerUser)); 7 | router.post("/login", wrapAsync(authControllers.loginUser)); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /backend/routes/task.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { 4 | getTask, 5 | getAllTask, 6 | addTask, 7 | updateTask, 8 | deleteTask, 9 | updateCategory, 10 | } = require("../controllers/task"); 11 | const wrapAsync = require("../middlewares/wrapAsync"); 12 | const { authorization } = require("../middlewares/authorization"); 13 | 14 | router.get("/all", authorization, wrapAsync(getAllTask)); 15 | router.get("/:id", wrapAsync(getTask)); 16 | router.post("/add", authorization, wrapAsync(addTask)); 17 | router.put("/:id", authorization, wrapAsync(updateTask)); 18 | router.delete("/:id", authorization, wrapAsync(deleteTask)); 19 | router.put("/category/:id", authorization, wrapAsync(updateCategory)); 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /backend/routes/user.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const userControllers = require("../controllers/user"); 4 | const wrapAsync = require("../middlewares/wrapAsync"); 5 | const { authorization } = require("../middlewares/authorization"); 6 | 7 | router.get("/profile", authorization, wrapAsync(userControllers.getAuthUser)); 8 | router.put("/update", authorization, wrapAsync(userControllers.updateUser)); 9 | router.get("/users", authorization, wrapAsync(userControllers.getAllUsers)); 10 | router.post("/board", authorization, wrapAsync(userControllers.updateBoard)); 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | dotenv.config(); 3 | const express = require("express"); 4 | const cors = require("cors"); 5 | const mongoose = require("mongoose"); 6 | 7 | const app = express(); 8 | 9 | const corsOptions = { 10 | origin: process.env.FRONTEND_URL, 11 | methods: ["GET", "POST", "DELETE", "PUT"], 12 | allowedHeaders: ["Content-Type", "Authorization"], 13 | credentials: true, 14 | }; 15 | 16 | app.use(cors(corsOptions)); 17 | app.use(express.json()); 18 | app.use(express.urlencoded({ extended: true })); 19 | const PORT = process.env.PORT || 3000; 20 | 21 | // All routers 22 | const authRouter = require("./routes/auth"); 23 | const userRouter = require("./routes/user"); 24 | const taskRoute = require("./routes/task"); 25 | const analyticsRoute = require("./routes/analytics"); 26 | 27 | // Connect to Database 28 | main() 29 | .then(() => console.log("Database Connection established")) 30 | .catch((err) => console.log(err)); 31 | 32 | async function main() { 33 | await mongoose.connect(process.env.MONGODB_URI); 34 | } 35 | 36 | // Root route 37 | app.get("/", (req, res) => { 38 | res.json({ 39 | message: "Welcome to Task_Management!", 40 | frontend_url: process.env.FRONTEND_URL, 41 | }); 42 | }); 43 | 44 | // All routes 45 | app.use("/api/auth", authRouter); 46 | app.use("/api/user", userRouter); 47 | app.use("/api/task", taskRoute); 48 | app.use("/api/analytics", analyticsRoute); 49 | 50 | // Invaild routes 51 | app.all("*", (req, res) => { 52 | res.json({ error: "Invaild Route" }); 53 | }); 54 | 55 | // Error handling middleware 56 | app.use((err, req, res, next) => { 57 | const errorMessage = err.message || "Something Went Wrong!"; 58 | res.status(500).json({ message: errorMessage }); 59 | }); 60 | 61 | // Start the server 62 | const server = app.listen(PORT, async () => { 63 | console.log(`Server listening on ${PORT}`); 64 | }); 65 | -------------------------------------------------------------------------------- /backend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "./server.js", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "/server.js" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | .env 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | Task_Management 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@reduxjs/toolkit": "^2.2.5", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-icons": "^5.2.1", 17 | "react-redux": "^9.1.2", 18 | "react-router-dom": "^6.23.1", 19 | "react-toastify": "^10.0.6" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.2.66", 23 | "@types/react-dom": "^18.2.22", 24 | "@vitejs/plugin-react": "^4.2.1", 25 | "eslint": "^8.57.0", 26 | "eslint-plugin-react": "^7.34.1", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.4.6", 29 | "vite": "^5.2.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, useEffect } from "react"; 2 | import { Route, Routes, useLocation, useNavigate } from "react-router-dom"; 3 | import Dashboard from "./pages/Dashboard"; 4 | import PageNotFound from "./pages/PageNotFound"; 5 | import Register_Login from "./pages/Register_Login"; 6 | import PrivateRoute from "./components/PrivateRoute"; 7 | import Loading from "./components/Loading"; 8 | import { useDispatch, useSelector } from "react-redux"; 9 | import { addAuth } from "./redux/slices/authSlice"; 10 | import { setLoading } from "./redux/slices/stateSlice"; 11 | import { TaskCardPublic } from "./components/Model"; 12 | import getHeader from "./utils/header"; 13 | import { toast } from "react-toastify"; 14 | 15 | function App() { 16 | const dispatch = useDispatch(); 17 | const navigate = useNavigate(); 18 | const { pathname } = useLocation(); 19 | const loading = useSelector((store) => store.state.loading); 20 | const auth = useSelector((store) => store.auth); 21 | const token = localStorage.getItem("token"); 22 | const getAuthUser = () => { 23 | dispatch(setLoading(true)); 24 | fetch(`${import.meta.env.VITE_BACKEND_URL}/api/user/profile`, { 25 | method: "GET", 26 | headers: getHeader(), 27 | }) 28 | .then((res) => res.json()) 29 | .then((json) => { 30 | dispatch(setLoading(false)); 31 | if (json?.data) { 32 | dispatch(addAuth(json.data)); 33 | navigate("/"); 34 | } else { 35 | navigate("/login"); 36 | toast.error("You are not logged in"); 37 | } 38 | }) 39 | .catch((err) => { 40 | toast.error("Something went wrong"); 41 | dispatch(setLoading(false)); 42 | }); 43 | }; 44 | 45 | useEffect(() => { 46 | if (token && !auth) { 47 | getAuthUser(); 48 | } 49 | }, [token, pathname, auth]); 50 | 51 | return ( 52 |
53 | {loading && } 54 | }> 55 | 56 | 60 | 61 | 62 | } 63 | /> 64 | } /> 65 | } /> 66 | } /> 67 | 68 | 69 |
70 | ); 71 | } 72 | 73 | export default App; 74 | -------------------------------------------------------------------------------- /frontend/src/assets/AuthImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akashdeep023/Task_Management/9c1a4a94edc6c3a5a5e1cc8edb2cf3e7d448c726/frontend/src/assets/AuthImage.png -------------------------------------------------------------------------------- /frontend/src/assets/checkbox_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akashdeep023/Task_Management/9c1a4a94edc6c3a5a5e1cc8edb2cf3e7d448c726/frontend/src/assets/checkbox_select.png -------------------------------------------------------------------------------- /frontend/src/assets/checkbox_unselect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akashdeep023/Task_Management/9c1a4a94edc6c3a5a5e1cc8edb2cf3e7d448c726/frontend/src/assets/checkbox_unselect.png -------------------------------------------------------------------------------- /frontend/src/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Loading = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 | ); 11 | }; 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /frontend/src/components/Model.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import "../css/Model.css"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { 5 | setAddedPeopleM, 6 | setAddPeopleM, 7 | setLogoutM, 8 | setTaskDeleteM, 9 | setTaskCardM, 10 | setTaskM, 11 | setBoardEmail, 12 | setCategoryName, 13 | setUpdateCategoryM, 14 | } from "../redux/slices/stateSlice"; 15 | import { AiFillDelete, AiOutlinePlus } from "react-icons/ai"; 16 | import { PiCodesandboxLogoDuotone } from "react-icons/pi"; 17 | import { Link, useNavigate, useParams } from "react-router-dom"; 18 | import { removeAuth } from "../redux/slices/authSlice"; 19 | import useAddTask from "../hooks/useAddTask"; 20 | import { toast } from "react-toastify"; 21 | import useDeleteTask from "../hooks/useDeleteTask"; 22 | import useGetTask from "../hooks/useGetTask"; 23 | import Loading from "./Loading"; 24 | import { getMonthDate, simpleDate } from "../utils/generateDate"; 25 | import useUpdateTask from "../hooks/useUpdateTask"; 26 | import CheckBoxUnselect from "../assets/checkbox_unselect.png"; 27 | import CheckBoxSelect from "../assets/checkbox_select.png"; 28 | import useAddToBoard from "../hooks/useAddToBoard"; 29 | import { checkValidEmail } from "../utils/validate"; 30 | import useUpdateCategory from "../hooks/useUpdateCategory"; 31 | 32 | export const AddPeople = () => { 33 | const dispatch = useDispatch(); 34 | const [email, setEmail] = useState(""); 35 | const [load, setLoad] = useState(""); 36 | const handleAddToBoard = (e) => { 37 | const validError = checkValidEmail(email); 38 | if (validError) { 39 | toast.error(validError); 40 | return; 41 | } 42 | useAddToBoard(e, setLoad, dispatch, email); 43 | }; 44 | return ( 45 |
46 |
47 |

Add people to the board

48 | setEmail(e.target.value)} 54 | /> 55 |
56 | 62 | 68 |
69 |
70 |
71 | ); 72 | }; 73 | export const AddedPeople = () => { 74 | const dispatch = useDispatch(); 75 | const email = useSelector((store) => store.state.boardEmail); 76 | return ( 77 |
78 |
79 |

{email} added to board

80 |
81 | 90 |
91 |
92 |
93 | ); 94 | }; 95 | 96 | export const UpdateCategory = () => { 97 | const dispatch = useDispatch(); 98 | const [load, setLoad] = useState(""); 99 | const { oldCategory, newCategory } = useSelector( 100 | (store) => store.state.categoryName 101 | ); 102 | const taskId = useSelector((store) => store.state.taskMId); 103 | const handleUpdateCategory = (e) => { 104 | useUpdateCategory( 105 | e, 106 | setLoad, 107 | dispatch, 108 | taskId, 109 | newCategory, 110 | oldCategory 111 | ); 112 | }; 113 | return ( 114 |
115 |
116 |

117 | Change the status of task ( 118 | {newCategory.charAt(0).toUpperCase() + newCategory.slice(1)} 119 | ) 120 |

121 |
122 | 128 | 137 |
138 |
139 |
140 | ); 141 | }; 142 | 143 | export const Logout = () => { 144 | const dispatch = useDispatch(); 145 | const navigate = useNavigate(); 146 | const handleLogout = () => { 147 | localStorage.removeItem("token"); 148 | dispatch(removeAuth()); 149 | dispatch(setLogoutM(false)); 150 | navigate("/login"); 151 | }; 152 | return ( 153 |
154 |
155 |

156 | Are you sure you want to Logout? 157 |

158 |
159 | 162 | 168 |
169 |
170 |
171 | ); 172 | }; 173 | export const TaskDelete = () => { 174 | const dispatch = useDispatch(); 175 | const id = useSelector((store) => store.state.taskMId); 176 | const [load, setLoad] = useState(""); 177 | const handleTaskDelete = (e) => { 178 | useDeleteTask(e, setLoad, dispatch, id); 179 | }; 180 | 181 | return ( 182 |
183 |
184 |

185 | Are you sure you want to Delete? 186 |

187 |
188 | 194 | 200 |
201 |
202 |
203 | ); 204 | }; 205 | export const TaskCard = () => { 206 | const task = useSelector((store) => store.state.taskM); 207 | const auth = useSelector((store) => store.auth); 208 | 209 | const dispatch = useDispatch(); 210 | const [dueDate, setDueDate] = useState(task?.dueDate || ""); 211 | const [title, setTitle] = useState(task?.title || ""); 212 | const [priority, setPriority] = useState(task?.priority || ""); 213 | const [checklist, setChecklist] = useState(task?.checklist || []); 214 | const [listBox, setListBox] = useState(task?.checklist?.length || 0); 215 | const [load, setLoad] = useState(""); 216 | const [assignBox, setAssignBox] = useState(false); 217 | const [assign, setAssign] = useState(task?.assign || ""); 218 | 219 | const handleAddTask = (e) => { 220 | if (title && priority && checklist.length > 0) { 221 | const listName = checklist.filter((list) => { 222 | return list.name == ""; 223 | }); 224 | if (listName.length == 0) { 225 | if (task == "") { 226 | useAddTask( 227 | e, 228 | setLoad, 229 | title, 230 | priority, 231 | checklist, 232 | assign, 233 | dueDate, 234 | dispatch 235 | ); 236 | } else { 237 | useUpdateTask( 238 | e, 239 | setLoad, 240 | title, 241 | priority, 242 | checklist, 243 | assign, 244 | dueDate, 245 | dispatch, 246 | task._id 247 | ); 248 | } 249 | } else { 250 | toast.error("Checklist is required"); 251 | } 252 | } else { 253 | toast.error("All fields are required"); 254 | } 255 | }; 256 | 257 | const handleAddChecklist = () => { 258 | let list = checklist; 259 | list = [...list, { name: "", isDone: false }]; 260 | setChecklist(list); 261 | setListBox(list.length); 262 | }; 263 | const handleDeleteChecklist = (idx) => { 264 | if (listBox > 0) { 265 | let list = checklist; 266 | list = list.filter((item, i) => i != idx); 267 | setChecklist(list); 268 | setListBox(list.length); 269 | } 270 | }; 271 | 272 | const handleTitle = (name) => { 273 | name = name.charAt(0).toUpperCase() + name.slice(1); 274 | setTitle(name); 275 | }; 276 | return ( 277 |
278 |
{ 281 | e.preventDefault(); 282 | handleAddTask(e); 283 | }} 284 | > 285 |
286 | 287 | Title * 288 | 289 | handleTitle(e.target.value)} 296 | /> 297 |
298 | 299 | Select Priority * 300 | 301 |
302 | setPriority("High Priority")} 307 | id="high" 308 | /> 309 | 322 | setPriority("Moderate Priority")} 327 | id="moderate" 328 | /> 329 | 342 | setPriority("Low Priority")} 347 | id="low" 348 | /> 349 | 362 |
363 |
364 | {auth?.board?.length != 0 && ( 365 |
366 | Assign to 367 |
368 |
setAssignBox(true)} 371 | > 372 | {assign ? ( 373 | assign 374 | ) : ( 375 |
376 | Add a assignee 377 |
378 | )} 379 |
380 |
386 |
387 |
Don't assign to any email
388 | 397 |
398 | {assign && ( 399 |
400 |
401 | Don't change to assign email 402 |
403 | 411 |
412 | )} 413 | {auth?.board?.map((item, idx) => { 414 | return ( 415 |
416 |
417 | 418 | {item 419 | .split("")[0] 420 | .toUpperCase()} 421 | {" "} 422 | {item} 423 |
424 | 433 |
434 | ); 435 | })} 436 |
437 |
438 |
439 | )} 440 | 441 | Checklist ( 442 | {checklist.filter((item) => item.isDone == true).length} 443 | /{listBox}) * 444 | 445 |
446 | {checklist?.map((el, idx) => { 447 | return ( 448 |
452 | 455 | setChecklist( 456 | checklist.map((item, i) => 457 | i === idx 458 | ? { 459 | ...item, 460 | isDone: !el.isDone, 461 | } 462 | : item 463 | ) 464 | ) 465 | } 466 | > 467 | {!el.isDone ? ( 468 | ⬜ 473 | ) : ( 474 | ✅ 479 | )} 480 | 481 | 488 | setChecklist( 489 | checklist.map((item, i) => 490 | i === idx 491 | ? { 492 | ...item, 493 | name: e.target 494 | .value, 495 | } 496 | : item 497 | ) 498 | ) 499 | } 500 | /> 501 | 504 | handleDeleteChecklist(idx) 505 | } 506 | > 507 | 508 | 509 |
510 | ); 511 | })} 512 |
513 | 517 | 518 | Add New 519 | 520 |
521 |
522 | 534 | { 540 | setDueDate(e.target?.value); 541 | }} 542 | /> 543 | 553 | 556 |
557 |
558 |
559 | ); 560 | }; 561 | export const TaskCardPublic = () => { 562 | const { id } = useParams(); 563 | const [task, setTask] = useState([]); 564 | useGetTask(id, setTask); 565 | 566 | return task?.length == 0 ? ( 567 | 568 | ) : ( 569 |
570 | 571 | 572 | Pro Manage 573 | 574 | 575 | {task == null ? ( 576 |
577 |

Task Not Found

578 |

579 | The task you are looking for does not exist or has been 580 | deleted. Please check the URL and try again. 581 |

582 | 583 | Back to Home 584 | 585 |
586 | ) : ( 587 |
588 |
589 |
590 | 594 | {task?.priority} 595 |
596 |

{task?.title}

597 |
598 | Checklist ( 599 | { 600 | task?.checklist?.filter( 601 | (list) => list.isDone == true 602 | ).length 603 | } 604 | /{task?.checklist?.length}) 605 |
606 |
607 | {task?.checklist?.map((list, idx) => { 608 | return ( 609 |
613 | 614 | {!list.isDone ? ( 615 | ⬜ 619 | ) : ( 620 | ✅ 624 | )} 625 | 626 |

627 | {list.name} 628 |

629 |
630 | ); 631 | })} 632 |
633 |
634 |
635 | {task?.dueDate && ( 636 | <> 637 | Due Date 638 |
{getMonthDate(task?.dueDate)}
639 | 640 | )} 641 |
642 |
643 | )} 644 |
645 | ); 646 | }; 647 | -------------------------------------------------------------------------------- /frontend/src/components/PopUp.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../css/PopUp.css"; 3 | import { useDispatch } from "react-redux"; 4 | import { 5 | setTaskCardM, 6 | setTaskDeleteM, 7 | setTaskFilterName, 8 | setTaskFilterP, 9 | setTaskM, 10 | } from "../redux/slices/stateSlice"; 11 | import { toast } from "react-toastify"; 12 | import useAllTaskFilter from "../hooks/useAllTaskFilter"; 13 | 14 | export const TaskMenu = ({ setTaskMenuP, id, task }) => { 15 | const dispatch = useDispatch(); 16 | 17 | const handleTaskEdit = () => { 18 | dispatch(setTaskM(task)); 19 | dispatch(setTaskCardM(true)); 20 | setTaskMenuP(false); 21 | }; 22 | 23 | const handleTaskDelete = () => { 24 | dispatch(setTaskDeleteM(true)); 25 | setTaskMenuP(false); 26 | }; 27 | const handleSharelink = (id) => { 28 | if (!navigator.clipboard) { 29 | toast.error( 30 | "Your browser does not support copying text to the clipboard." 31 | ); 32 | return; 33 | } 34 | navigator.clipboard.writeText( 35 | `${import.meta.env.VITE_FRONTEND_URL}/task/${id}` 36 | ); 37 | 38 | setTaskMenuP(false); 39 | toast.success("Link Copied"); 40 | }; 41 | return ( 42 |
43 | 44 | 45 | 48 |
49 | ); 50 | }; 51 | export const TaskFilter = () => { 52 | const dispatch = useDispatch(); 53 | const handleFilter = (days) => { 54 | dispatch(setTaskFilterP(false)); 55 | useAllTaskFilter(dispatch, days); 56 | }; 57 | 58 | return ( 59 |
60 | 68 | 76 | 84 |
85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /frontend/src/components/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import { Navigate } from "react-router-dom"; 3 | 4 | const PrivateRoute = ({ children }) => { 5 | const auth = useSelector((store) => store.auth); 6 | 7 | if (!auth) { 8 | return ; 9 | } 10 | 11 | return children; 12 | }; 13 | 14 | export default PrivateRoute; 15 | -------------------------------------------------------------------------------- /frontend/src/components/TaskBox.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { IoIosArrowDown, IoIosArrowUp } from "react-icons/io"; 3 | import { TbDots } from "react-icons/tb"; 4 | import { TaskMenu } from "./PopUp"; 5 | import { getMonthDate } from "../utils/generateDate"; 6 | import { useDispatch } from "react-redux"; 7 | import { 8 | setCategoryName, 9 | setTaskMId, 10 | setUpdateCategoryM, 11 | } from "../redux/slices/stateSlice"; 12 | import CheckBoxUnselect from "../assets/checkbox_unselect.png"; 13 | import CheckBoxSelect from "../assets/checkbox_select.png"; 14 | 15 | const TaskBox = ({ 16 | backlogCollapse, 17 | todoCollapse, 18 | progressCollapse, 19 | doneCollapse, 20 | task, 21 | }) => { 22 | const [collapse, setCollapse] = useState(true); 23 | const [taskMenuP, setTaskMenuP] = useState(false); 24 | const dispatch = useDispatch(); 25 | const handleClickOutside = (event) => { 26 | if (taskMenuP && !event?.target.closest(".popup-box")) { 27 | setTaskMenuP(false); 28 | } 29 | }; 30 | 31 | const handleUpdateCategory = (category) => { 32 | dispatch(setUpdateCategoryM(true)); 33 | dispatch( 34 | setCategoryName({ 35 | newCategory: category, 36 | oldCategory: task?.category, 37 | }) 38 | ); 39 | dispatch(setTaskMId(task?._id)); 40 | }; 41 | 42 | useEffect(() => { 43 | document.addEventListener("mousedown", handleClickOutside); 44 | return () => { 45 | document.removeEventListener("mousedown", handleClickOutside); 46 | }; 47 | }, [taskMenuP]); 48 | 49 | useEffect(() => { 50 | setCollapse(true); 51 | }, [backlogCollapse, todoCollapse, progressCollapse, doneCollapse]); 52 | return ( 53 |
54 |
55 |
56 |
61 |

{task?.priority}

62 |
63 | {task?.userName?.name?.split(" ")[0]?.slice(0, 1) + 64 | (task?.userName?.name?.split(" ")[1] == undefined 65 | ? "" 66 | : task?.userName?.name 67 | ?.split(" ")[1] 68 | ?.slice(0, 1) 69 | .toUpperCase())} 70 |
71 |
72 |
73 | { 77 | setTaskMenuP(true); 78 | dispatch(setTaskMId(task?._id)); 79 | }} 80 | /> 81 | {taskMenuP && ( 82 | 87 | )} 88 |
89 |
90 |

{task?.title}

91 |
92 |

93 | Checklist ( 94 | { 95 | task?.checklist.filter((list) => list.isDone == true) 96 | .length 97 | } 98 | /{task?.checklist.length}) 99 |

100 |
setCollapse(!collapse)}> 101 | {collapse ? : } 102 |
103 |
104 |
109 | {task?.checklist.map((list, idx) => { 110 | return ( 111 | 123 | ); 124 | })} 125 |
126 |
127 | {task?.dueDate ? ( 128 |
133 | {getMonthDate(task?.dueDate)} 134 |
135 | ) : ( 136 |
137 | )} 138 |
139 | {task?.category != "backlog" && ( 140 |
handleUpdateCategory("backlog")} 143 | > 144 | BACKLOG 145 |
146 | )} 147 | {task?.category != "to-do" && ( 148 |
handleUpdateCategory("to-do")} 151 | > 152 | TO-DO 153 |
154 | )} 155 | {task?.category != "in-progress" && ( 156 |
handleUpdateCategory("in-progress")} 159 | > 160 | PROGRESS 161 |
162 | )} 163 | {task?.category != "done" && ( 164 |
handleUpdateCategory("done")} 167 | > 168 | DONE 169 |
170 | )} 171 |
172 |
173 |
174 | ); 175 | }; 176 | 177 | export default TaskBox; 178 | -------------------------------------------------------------------------------- /frontend/src/css/Analytics.css: -------------------------------------------------------------------------------- 1 | .analytics-container { 2 | display: flex; 3 | justify-content: start; 4 | align-items: center; 5 | gap: 16px; 6 | } 7 | 8 | .analytics-box { 9 | background: #f9fcff; 10 | padding: 16px; 11 | border-radius: 12px; 12 | width: 40%; 13 | min-width: 300px; 14 | } 15 | 16 | .analytics-circle { 17 | height: 14px; 18 | width: 14px; 19 | border-radius: 50%; 20 | background: #90c4cc; 21 | } 22 | 23 | .analytics-task { 24 | display: flex; 25 | justify-content: space-between; 26 | align-items: center; 27 | padding: 12px; 28 | } 29 | .analytics-task > div { 30 | display: flex; 31 | justify-content: start; 32 | align-items: center; 33 | gap: 10px; 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/css/Dashboard.css: -------------------------------------------------------------------------------- 1 | .dashboard-container { 2 | display: flex; 3 | flex-direction: column; 4 | overflow-x: auto; 5 | height: 100vh; 6 | padding-left: 16px; 7 | gap: 16px; 8 | margin-left: 200px; 9 | width: calc(100% - 200px); 10 | min-width: 725px; 11 | } 12 | 13 | .dashboard-header { 14 | padding-right: 16px; 15 | } 16 | 17 | .dashboard-header div h2, 18 | .dashboard-header div h3, 19 | .dashboard-header div h4 { 20 | font-weight: 600 !important; 21 | } 22 | 23 | .dashboard-header > .header-user { 24 | margin: 20px 0px; 25 | } 26 | 27 | .dashboard-header div h4 { 28 | opacity: 0.7; 29 | } 30 | 31 | .dashboard-header > div { 32 | display: flex; 33 | justify-content: space-between; 34 | align-items: center; 35 | } 36 | 37 | .header-board > div { 38 | display: flex; 39 | justify-content: start; 40 | align-items: center; 41 | gap: 10px; 42 | } 43 | .add-people { 44 | cursor: pointer; 45 | margin-left: 10px; 46 | font-size: 14px; 47 | display: flex; 48 | align-items: center; 49 | justify-content: start; 50 | gap: 4px; 51 | opacity: 0.7; 52 | } 53 | 54 | .dashboard-header .header-time span { 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | gap: 4px; 59 | font-size: 15px; 60 | cursor: pointer; 61 | } 62 | 63 | .dashboard-column { 64 | display: flex; 65 | overflow-x: auto; 66 | height: 100vh; 67 | padding-bottom: 16px; 68 | width: 100%; 69 | } 70 | 71 | .column { 72 | min-width: 350px; 73 | background-color: #f5f5f5; 74 | border-radius: 8px; 75 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); 76 | overflow-y: hidden; 77 | height: 100%; 78 | background: #eef2f5; 79 | margin-right: 16px; 80 | } 81 | 82 | .column > h4 { 83 | padding: 16px; 84 | height: fit-content; 85 | font-weight: 500; 86 | width: 100%; 87 | display: flex; 88 | justify-content: space-between; 89 | align-items: center; 90 | } 91 | .column > h4 > span { 92 | display: flex; 93 | justify-content: space-between; 94 | align-items: center; 95 | gap: 10px; 96 | } 97 | 98 | /* Scrollbar Style */ 99 | .dashboard-column::-webkit-scrollbar { 100 | height: 8px; 101 | } 102 | 103 | .dashboard-column::-webkit-scrollbar-thumb { 104 | background: #8daab9cc; 105 | cursor: grab; 106 | border-radius: 4px; 107 | } 108 | 109 | .dashboard-column::-webkit-scrollbar-thumb:active { 110 | cursor: grabbing; 111 | } 112 | 113 | .dashboard-column::-webkit-scrollbar-track { 114 | background: #d6eaf4; 115 | } 116 | 117 | .column > div::-webkit-scrollbar { 118 | width: 8px; 119 | } 120 | 121 | .column > div::-webkit-scrollbar-thumb { 122 | background: #8daab9cc; 123 | cursor: grab; 124 | border-radius: 4px; 125 | } 126 | 127 | .column > div::-webkit-scrollbar-thumb:active { 128 | cursor: grabbing; 129 | } 130 | 131 | .column > div::-webkit-scrollbar-track { 132 | background: #d6eaf4; 133 | } 134 | -------------------------------------------------------------------------------- /frontend/src/css/Model.css: -------------------------------------------------------------------------------- 1 | .model-container { 2 | position: absolute; 3 | top: 0px; 4 | left: 0px; 5 | height: 100vh; 6 | width: 100vw; 7 | z-index: 110; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | background: #303d438c; 12 | } 13 | 14 | .model-box { 15 | background: #fff; 16 | padding: 30px 20px; 17 | border-radius: 12px; 18 | min-width: 400px; 19 | width: 35%; 20 | min-height: 250px; 21 | display: flex; 22 | flex-direction: column; 23 | align-items: start; 24 | justify-content: center; 25 | gap: 12px; 26 | border: 1px solid #efefef; 27 | box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.07); 28 | } 29 | 30 | .model-card { 31 | height: 70%; 32 | justify-content: space-between; 33 | font-size: 16px; 34 | } 35 | .assign-unselect-color { 36 | color: gray; 37 | } 38 | .model-task-null { 39 | align-items: center; 40 | } 41 | .model-task-null p { 42 | text-align: center; 43 | opacity: 0.9; 44 | font-size: 14px; 45 | } 46 | .model-card-details { 47 | height: 100%; 48 | width: 100%; 49 | } 50 | .model-assign { 51 | display: flex; 52 | align-items: center; 53 | } 54 | .model-assign span { 55 | font-size: 14px; 56 | margin-right: 10px; 57 | font-weight: 500; 58 | text-wrap: nowrap; 59 | } 60 | .model-box span { 61 | font-weight: 500; 62 | } 63 | 64 | .model-box-s { 65 | width: fit-content; 66 | } 67 | .model-box h3 { 68 | font-weight: 500; 69 | } 70 | 71 | .model-center { 72 | text-align: center; 73 | width: 100%; 74 | margin: 16px 0px; 75 | } 76 | 77 | .checklist-box { 78 | width: 100%; 79 | max-height: 26vh; 80 | overflow-y: auto; 81 | padding-right: 5px; 82 | } 83 | 84 | .checklist-box::-webkit-scrollbar, 85 | .assign-selection-box::-webkit-scrollbar { 86 | width: 8px; 87 | } 88 | 89 | .checklist-box::-webkit-scrollbar-thumb, 90 | .assign-selection-box::-webkit-scrollbar-thumb { 91 | background: #8daab9cc; 92 | cursor: grab; 93 | border-radius: 4px; 94 | } 95 | 96 | .checklist-box::-webkit-scrollbar-thumb:active, 97 | .assign-selection-box::-webkit-scrollbar-thumb:active { 98 | cursor: grabbing; 99 | } 100 | 101 | .checklist-box::-webkit-scrollbar-track, 102 | .assign-selection-box::-webkit-scrollbar-track { 103 | background: #d6eaf4; 104 | } 105 | 106 | .assign-model-box { 107 | width: 100%; 108 | position: relative; 109 | } 110 | .model-input { 111 | width: 100%; 112 | padding: 8px 10px; 113 | border: 1px solid #ccc; 114 | border-radius: 5px; 115 | outline: none; 116 | font-size: 16px; 117 | margin: 6px 0px; 118 | } 119 | .model-assign-input { 120 | cursor: pointer; 121 | } 122 | .assign-selection-box { 123 | position: absolute; 124 | width: 100%; 125 | background-color: #fff; 126 | top: 100%; 127 | left: 0px; 128 | border-radius: 5px; 129 | z-index: 99; 130 | display: flex; 131 | gap: 20px; 132 | flex-direction: column; 133 | height: 0px; 134 | overflow-y: scroll; 135 | transition: all 0.1s ease; 136 | padding: 0px 10px; 137 | } 138 | .assign-selection-box-exist { 139 | height: fit-content; 140 | max-height: 30vh; 141 | padding: 8px 10px; 142 | border: 1px solid #ccc; 143 | } 144 | 145 | .assign-selection-box > div { 146 | display: flex; 147 | justify-content: space-between; 148 | align-items: center; 149 | width: 100%; 150 | } 151 | .assign-selection-box > div > div { 152 | display: flex; 153 | align-items: center; 154 | width: calc(100% - 140px); 155 | overflow: hidden; 156 | } 157 | .assign-selection-box button { 158 | width: fit-content; 159 | padding: 4px 40px; 160 | border-radius: 10px; 161 | cursor: pointer; 162 | font-weight: 500; 163 | border: 1px solid #ccc; 164 | color: gray; 165 | } 166 | .assign-selection-box span { 167 | font-weight: 500; 168 | } 169 | .assign-circel { 170 | display: flex; 171 | justify-content: center; 172 | align-items: center; 173 | min-height: 40px; 174 | min-width: 40px; 175 | border-radius: 50%; 176 | background-color: #ffebeb; 177 | margin-right: 5px; 178 | } 179 | 180 | .checklist-input-box { 181 | position: relative; 182 | } 183 | .model-checkbox { 184 | cursor: pointer; 185 | } 186 | .model-input-btn { 187 | padding: 8px 35px; 188 | } 189 | .checklist-btn { 190 | display: flex; 191 | justify-content: center; 192 | align-items: center; 193 | position: absolute; 194 | top: 50%; 195 | transform: translateY(-50%); 196 | /* cursor: pointer; */ 197 | } 198 | .checklist-btn-l { 199 | left: 8px; 200 | } 201 | .checklist-btn-r { 202 | right: 8px; 203 | color: red; 204 | } 205 | .model-btns { 206 | display: flex; 207 | gap: 12px; 208 | justify-content: space-between; 209 | width: 100%; 210 | } 211 | .model-box-s .model-btns { 212 | flex-direction: column; 213 | } 214 | 215 | .model-btns button { 216 | width: 100%; 217 | padding: 8px 10px; 218 | border-radius: 10px; 219 | cursor: pointer; 220 | font-weight: 500; 221 | } 222 | 223 | .model-cancel { 224 | border: 1px solid red; 225 | color: red; 226 | } 227 | 228 | .model-submit { 229 | background: #17a2b8; 230 | color: white; 231 | border: 1px solid #17a2b8; 232 | } 233 | 234 | .model-due-date { 235 | margin-right: 20px; 236 | border: 1px solid #ccc; 237 | text-wrap: nowrap; 238 | } 239 | 240 | .model-btns label { 241 | cursor: pointer; 242 | width: 100%; 243 | } 244 | 245 | #model-card-date { 246 | height: 0px; 247 | width: 0px; 248 | opacity: 0; 249 | } 250 | .require { 251 | color: red; 252 | font-size: 14px !important; 253 | margin-bottom: 5px; 254 | } 255 | .priority-box { 256 | display: flex; 257 | justify-content: start; 258 | align-items: center; 259 | gap: 10px; 260 | margin: 5px 0; 261 | font-size: 14px; 262 | margin: 12px 0; 263 | } 264 | .priority-box span { 265 | text-wrap: nowrap; 266 | font-size: 14px; 267 | } 268 | .priority-box label span { 269 | text-wrap: nowrap; 270 | font-size: 12px; 271 | text-transform: uppercase; 272 | } 273 | .priority-box > div { 274 | width: 100%; 275 | display: flex; 276 | align-items: center; 277 | justify-content: start; 278 | gap: 5px; 279 | } 280 | .priority-box > div label { 281 | display: flex; 282 | align-items: center; 283 | justify-content: center; 284 | gap: 5px; 285 | width: fit-content; 286 | border: 1px solid #ccc; 287 | padding: 2px 7px; 288 | border-radius: 8px; 289 | cursor: pointer; 290 | text-wrap: nowrap; 291 | background: #fff; 292 | } 293 | 294 | .priority-box input[type="radio"] { 295 | display: none; 296 | } 297 | 298 | .priority-circel { 299 | min-width: 7px; 300 | min-height: 7px; 301 | border-radius: 50%; 302 | } 303 | 304 | .selected-priority { 305 | background: #dad8d8 !important; 306 | } 307 | .checklist-head { 308 | font-size: 14px; 309 | } 310 | .checklist-add { 311 | display: flex; 312 | align-items: center; 313 | justify-content: start; 314 | gap: 5px; 315 | opacity: 0.8; 316 | cursor: pointer; 317 | margin: 16px 0; 318 | width: fit-content; 319 | } 320 | 321 | /* Model public view */ 322 | .nav-logo-public { 323 | position: absolute; 324 | top: 30px; 325 | left: 20px; 326 | width: fit-content; 327 | text-decoration: none; 328 | } 329 | .model-public { 330 | background-color: #fff; 331 | } 332 | .model-due { 333 | display: flex; 334 | justify-content: start; 335 | align-items: center; 336 | gap: 16px; 337 | font-size: 14px; 338 | font-weight: 500; 339 | } 340 | .model-due > div { 341 | padding: 8px 10px; 342 | border-radius: 10px; 343 | background: #cf3636; 344 | color: white; 345 | } 346 | .priority-public span { 347 | font-size: 10px; 348 | text-transform: uppercase; 349 | } 350 | .checklist-public { 351 | margin: 10px 0 6px 0; 352 | } 353 | 354 | .checklist-box-public { 355 | max-height: 40vh; 356 | } 357 | 358 | /* Responsive */ 359 | @media (max-width: 768px) { 360 | .model-box { 361 | min-width: 90%; 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /frontend/src/css/Navbar.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | width: 200px; 3 | height: 100vh; 4 | background-color: white; 5 | color: black; 6 | position: fixed; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | padding: 25px 0; 11 | border-right: 1px solid #8daab952; 12 | z-index: 100; 13 | top: 0; 14 | left: 0; 15 | } 16 | 17 | .nav-logo { 18 | font-size: 16px; 19 | font-weight: 700; 20 | margin-bottom: 30px; 21 | color: black; 22 | letter-spacing: 0.1px; 23 | display: flex; 24 | align-items: center; 25 | justify-content: start; 26 | gap: 15px; 27 | width: 100%; 28 | padding-left: 20px; 29 | text-decoration: none; 30 | } 31 | 32 | .nav-links { 33 | display: flex; 34 | flex-direction: column; 35 | gap: 15px; 36 | width: 100%; 37 | height: 100%; 38 | justify-content: space-between; 39 | } 40 | .nav-links > div { 41 | display: flex; 42 | flex-direction: column; 43 | gap: 10px; 44 | justify-content: space-between; 45 | } 46 | .nav-logout { 47 | padding: 10px; 48 | padding-left: 20px; 49 | margin-bottom: 20px; 50 | } 51 | .nav-log { 52 | text-align: center; 53 | text-decoration: none; 54 | color: black; 55 | border-radius: 2px; 56 | font-weight: 500; 57 | font-size: 16px; 58 | display: flex; 59 | align-items: center; 60 | justify-content: start; 61 | gap: 15px; 62 | width: fit-content; 63 | color: red; 64 | cursor: pointer; 65 | } 66 | .nav-log:hover { 67 | color: rgb(123, 0, 0); 68 | } 69 | 70 | .nav-link { 71 | padding: 10px; 72 | text-align: center; 73 | text-decoration: none; 74 | color: black; 75 | border-radius: 2px; 76 | font-weight: 500; 77 | font-size: 16px; 78 | display: flex; 79 | align-items: center; 80 | justify-content: start; 81 | gap: 15px; 82 | width: 100%; 83 | padding-left: 20px; 84 | cursor: pointer; 85 | } 86 | 87 | .nav-active { 88 | background: #4392ed29; 89 | } 90 | 91 | .nav-link:hover { 92 | background: #4391ed1a; 93 | } 94 | 95 | .nav-link.active { 96 | background: #4391ed1a; 97 | } 98 | -------------------------------------------------------------------------------- /frontend/src/css/Page_Not_Found.css: -------------------------------------------------------------------------------- 1 | .page_not_found { 2 | height: 100%; 3 | width: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | background: #317f8b; 9 | color: #fff; 10 | gap: 10px; 11 | } 12 | 13 | .page_not_found_link { 14 | color: #fff; 15 | text-decoration: none; 16 | transition: color 0.3s ease; 17 | &:hover { 18 | color: #90b4db; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/css/PopUp.css: -------------------------------------------------------------------------------- 1 | .popup-box { 2 | height: fit-content; 3 | width: 120px; 4 | background: #fff; 5 | padding: 14px 20px; 6 | position: absolute; 7 | top: 90%; 8 | right: 0px; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: start; 13 | gap: 7px; 14 | z-index: 90; 15 | border-radius: 12px; 16 | font-weight: 500; 17 | box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.218); 18 | } 19 | .pupup-filter { 20 | top: 100%; 21 | } 22 | 23 | .popup-box button { 24 | background: transparent; 25 | color: black; 26 | border: none; 27 | cursor: pointer; 28 | } 29 | .popup-delete { 30 | color: red !important; 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/css/Register_Login.css: -------------------------------------------------------------------------------- 1 | .register_login_box { 2 | height: 100%; 3 | width: 100%; 4 | display: flex; 5 | } 6 | .img_box { 7 | height: 100%; 8 | width: 60%; 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | justify-content: center; 13 | background: #17a2b8; 14 | color: #fff; 15 | letter-spacing: 0.7px; 16 | } 17 | .authimage_box { 18 | position: relative; 19 | height: 20vw; 20 | width: 20vw; 21 | margin-bottom: 5%; 22 | } 23 | .authimage_back { 24 | border-radius: 50%; 25 | background: #317f8b; 26 | width: 100%; 27 | height: 100%; 28 | position: absolute; 29 | top: 0; 30 | } 31 | .authimage { 32 | top: 10%; 33 | position: absolute; 34 | height: 100%; 35 | width: 100%; 36 | } 37 | .img_box h2, 38 | .form_box h2 { 39 | font-weight: 500; 40 | margin-bottom: 10px; 41 | } 42 | .img_box span { 43 | font-size: 0.9em; 44 | font-weight: 100; 45 | opacity: 0.9; 46 | } 47 | 48 | .form_box { 49 | height: 100%; 50 | width: 40%; 51 | display: flex; 52 | flex-direction: column; 53 | align-items: center; 54 | justify-content: center; 55 | gap: 15px; 56 | } 57 | .form_box_s { 58 | height: 100%; 59 | width: 40%; 60 | display: flex; 61 | flex-direction: column; 62 | align-items: start; 63 | justify-content: center; 64 | gap: 15px; 65 | } 66 | 67 | .form_box input[type="text"], 68 | .form_box input[type="password"], 69 | .form_box input[type="email"] { 70 | width: 100%; 71 | padding: 10px 35px; 72 | border: 1px solid #ccc; 73 | border-radius: 5px; 74 | outline: none; 75 | font-size: 16px; 76 | } 77 | 78 | .form_box .input_p_box { 79 | width: 80%; 80 | position: relative; 81 | display: flex; 82 | align-items: center; 83 | justify-content: center; 84 | } 85 | .form_box .hide_show_btn { 86 | position: absolute; 87 | top: 50%; 88 | right: 10px; 89 | transform: translateY(-50%); 90 | font-size: 14px; 91 | cursor: pointer; 92 | } 93 | .form_box .hide_show_btn_s { 94 | position: absolute; 95 | top: 50%; 96 | left: 8px; 97 | transform: translateY(-50%); 98 | font-size: 14px; 99 | cursor: pointer; 100 | } 101 | 102 | .form_box .submit_button { 103 | padding: 10px 20px; 104 | width: 80%; 105 | background: #317f8b; 106 | border: 1px solid #317f8b !important; 107 | color: #fff; 108 | border: none; 109 | border-radius: 50px; 110 | cursor: pointer; 111 | font-size: smaller; 112 | margin: 15px 0px; 113 | } 114 | .form_box .update_button { 115 | padding: 12px 20px; 116 | width: 80%; 117 | background: #17a2b8; 118 | border: 1px solid #17a2b8 !important; 119 | color: #fff; 120 | border: none; 121 | border-radius: 50px; 122 | cursor: pointer; 123 | font-size: smaller; 124 | margin: 15px 0px; 125 | } 126 | .form_box .submit_button:active, 127 | .form_box .update_button:active { 128 | background: #005f6e; 129 | border: 1px solid #005f6e !important; 130 | } 131 | .form_box .auth_change { 132 | opacity: 0.8; 133 | font-size: smaller; 134 | } 135 | .form_box .button_change { 136 | font-size: smaller; 137 | width: 80%; 138 | padding: 10px 20px; 139 | border: 1px solid #317f8b; 140 | background: transparent; 141 | color: #317f8b; 142 | cursor: pointer; 143 | border-radius: 50px; 144 | } 145 | .form_box .button_change:active { 146 | border: 1px solid #005f6e; 147 | background: #005f6e; 148 | color: white; 149 | } 150 | 151 | .form_box button:disabled { 152 | cursor: not-allowed; 153 | } 154 | -------------------------------------------------------------------------------- /frontend/src/css/Task.css: -------------------------------------------------------------------------------- 1 | .task-container { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 16px; 5 | overflow-y: auto; 6 | padding: 0px 12px; 7 | height: 95%; 8 | padding-bottom: 50px; 9 | } 10 | 11 | .task-box { 12 | background-color: white; 13 | width: 100%; 14 | padding: 16px; 15 | border-radius: 20px; 16 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); 17 | } 18 | 19 | .relative { 20 | position: relative; 21 | } 22 | .priority-menu { 23 | display: flex; 24 | width: 100%; 25 | justify-content: space-between; 26 | align-items: center; 27 | margin-bottom: 10px; 28 | } 29 | 30 | .priority-menu p { 31 | text-transform: uppercase; 32 | } 33 | .priority-menu > div { 34 | display: flex; 35 | justify-content: start; 36 | align-items: center; 37 | font-size: 9px; 38 | gap: 5px; 39 | } 40 | .priority-menu .circle-box { 41 | height: 10px; 42 | width: 10px; 43 | border-radius: 50%; 44 | } 45 | .high-box { 46 | background-color: #ff2473; 47 | } 48 | .moderate-box { 49 | background-color: #18b0ff; 50 | } 51 | .low-box { 52 | background-color: #63c05b; 53 | } 54 | .priority-menu .circle-name-box { 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | height: 20px; 59 | width: 20px; 60 | background-color: rgba(255, 202, 202, 0.408); 61 | border-radius: 50%; 62 | } 63 | 64 | .task-box h3 { 65 | margin: 0; 66 | width: fit-content; 67 | font-size: 18px; 68 | font-weight: 500; 69 | -webkit-line-clamp: 2; 70 | -webkit-box-orient: vertical; 71 | display: -webkit-box; 72 | overflow: hidden; 73 | } 74 | 75 | .task-box > p { 76 | margin: 8px 0 0; 77 | color: #666; 78 | } 79 | 80 | .task-checklist { 81 | display: flex; 82 | align-items: center; 83 | justify-content: space-between; 84 | margin-top: 14px; 85 | font-size: 14px; 86 | } 87 | 88 | .task-checklist > div { 89 | padding: 5px; 90 | border-radius: 3px; 91 | background: #eeecec; 92 | height: 22px; 93 | width: 22px; 94 | display: flex; 95 | align-items: center; 96 | justify-content: center; 97 | cursor: pointer; 98 | opacity: 0.7; 99 | } 100 | 101 | .task-checklist-details { 102 | display: flex; 103 | flex-direction: column; 104 | gap: 8px; 105 | font-size: 14px; 106 | margin-top: 15px; 107 | height: fit-content; 108 | transition: all 0.1s ease; 109 | } 110 | .task-checklist-details-collapse { 111 | height: 0px; 112 | overflow: hidden; 113 | margin: 0px; 114 | } 115 | 116 | .checklist-details-box { 117 | display: flex; 118 | align-items: center; 119 | justify-content: start; 120 | gap: 10px; 121 | width: 100%; 122 | border: 1px solid #66666638; 123 | border-radius: 8px; 124 | padding: 6px 8px; 125 | } 126 | 127 | .task-btns { 128 | display: flex; 129 | justify-content: space-between; 130 | align-items: center; 131 | font-size: 8px; 132 | color: #767575; 133 | font-weight: 500; 134 | margin-top: 15px; 135 | } 136 | 137 | .task-btn { 138 | background: #eeecec; 139 | border-radius: 8px; 140 | padding: 6px 8px; 141 | cursor: pointer; 142 | } 143 | 144 | .task-btns > div { 145 | display: flex; 146 | gap: 5px; 147 | } 148 | 149 | .task-btn-red { 150 | background: #cf3636; 151 | color: white; 152 | cursor: default; 153 | } 154 | .task-btn-green { 155 | background: #63c05b; 156 | color: white; 157 | cursor: default; 158 | } 159 | -------------------------------------------------------------------------------- /frontend/src/hooks/useAddTask.js: -------------------------------------------------------------------------------- 1 | import getHeader from "../utils/header"; 2 | import { toast } from "react-toastify"; 3 | import { setTaskCardM, setTaskM } from "../redux/slices/stateSlice"; 4 | import { addTodoTask } from "../redux/slices/taskSlice"; 5 | const useAddTask = ( 6 | e, 7 | setLoad, 8 | title, 9 | priority, 10 | checklist, 11 | assign, 12 | dueDate, 13 | dispatch 14 | ) => { 15 | setLoad("Loading..."); 16 | e.target.disabled = true; 17 | fetch(`${import.meta.env.VITE_BACKEND_URL}/api/task/add`, { 18 | method: "POST", 19 | headers: getHeader(), 20 | body: JSON.stringify({ 21 | title: title, 22 | priority: priority, 23 | checklist: checklist, 24 | assign: assign, 25 | dueDate: dueDate, 26 | }), 27 | }) 28 | .then((response) => response.json()) 29 | .then((json) => { 30 | setLoad(""); 31 | e.target.disabled = false; 32 | if (json?.message === "success") { 33 | toast.success("Task Added Successfully"); 34 | dispatch(addTodoTask(json.data)); 35 | dispatch(setTaskCardM(false)); 36 | dispatch(setTaskM("")); 37 | } else { 38 | toast.error(json?.message); 39 | } 40 | }) 41 | .catch((error) => { 42 | console.error("Error:", error); 43 | setLoad(""); 44 | toast.error("Something went wrong"); 45 | e.target.disabled = false; 46 | }); 47 | }; 48 | 49 | export default useAddTask; 50 | -------------------------------------------------------------------------------- /frontend/src/hooks/useAddToBoard.js: -------------------------------------------------------------------------------- 1 | import getHeader from "../utils/header"; 2 | import { toast } from "react-toastify"; 3 | import { 4 | setAddedPeopleM, 5 | setAddPeopleM, 6 | setBoardEmail, 7 | } from "../redux/slices/stateSlice"; 8 | import { addAuth } from "../redux/slices/authSlice"; 9 | const useAddToBoard = (e, setLoad, dispatch, email) => { 10 | setLoad("Loading..."); 11 | e.target.disabled = true; 12 | fetch(`${import.meta.env.VITE_BACKEND_URL}/api/user/board`, { 13 | method: "POST", 14 | headers: getHeader(), 15 | body: JSON.stringify({ 16 | email: email, 17 | }), 18 | }) 19 | .then((response) => response.json()) 20 | .then((json) => { 21 | setLoad(""); 22 | e.target.disabled = false; 23 | if (json?.message === "success") { 24 | toast.success("Added Email to the Board"); 25 | dispatch(addAuth(json.data)); 26 | dispatch(setAddPeopleM(false)); 27 | dispatch(setAddedPeopleM(true)); 28 | dispatch(setBoardEmail(json.email)); 29 | } else { 30 | toast.error(json?.message); 31 | } 32 | }) 33 | .catch((error) => { 34 | console.error("Error:", error); 35 | setLoad(""); 36 | toast.error("Something went wrong"); 37 | e.target.disabled = false; 38 | }); 39 | }; 40 | 41 | export default useAddToBoard; 42 | -------------------------------------------------------------------------------- /frontend/src/hooks/useAllTask.js: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux"; 2 | import getHeader from "../utils/header"; 3 | import { useEffect } from "react"; 4 | import { 5 | setBacklog, 6 | setTodo, 7 | setInProgress, 8 | setDone, 9 | } from "../redux/slices/taskSlice"; 10 | import { setLoading } from "../redux/slices/stateSlice"; 11 | import { toast } from "react-toastify"; 12 | 13 | const useAllTask = () => { 14 | const dispatch = useDispatch(); 15 | const fetchData = () => { 16 | dispatch(setLoading(true)); 17 | fetch(`${import.meta.env.VITE_BACKEND_URL}/api/task/all`, { 18 | method: "GET", 19 | headers: getHeader(), 20 | }) 21 | .then((response) => response.json()) 22 | .then((json) => { 23 | if (json?.message === "success") { 24 | dispatch(setBacklog(json.data.backlog)); 25 | dispatch(setTodo(json.data.todo)); 26 | dispatch(setInProgress(json.data.inProgress)); 27 | dispatch(setDone(json.data.done)); 28 | } 29 | dispatch(setLoading(false)); 30 | }) 31 | .catch((error) => { 32 | console.error("Error:", error); 33 | toast.error("Something went wrong"); 34 | dispatch(setLoading(false)); 35 | }); 36 | }; 37 | 38 | useEffect(() => { 39 | fetchData(); 40 | }, []); 41 | }; 42 | 43 | export default useAllTask; 44 | -------------------------------------------------------------------------------- /frontend/src/hooks/useAllTaskFilter.js: -------------------------------------------------------------------------------- 1 | import getHeader from "../utils/header"; 2 | import { 3 | setBacklog, 4 | setTodo, 5 | setInProgress, 6 | setDone, 7 | } from "../redux/slices/taskSlice"; 8 | import { setLoading } from "../redux/slices/stateSlice"; 9 | import { toast } from "react-toastify"; 10 | 11 | const useAllTaskFilter = (dispatch, days) => { 12 | dispatch(setLoading(true)); 13 | fetch(`${import.meta.env.VITE_BACKEND_URL}/api/task/all?days=${days}`, { 14 | method: "GET", 15 | headers: getHeader(), 16 | }) 17 | .then((response) => response.json()) 18 | .then((json) => { 19 | if (json?.message === "success") { 20 | dispatch(setBacklog(json.data.backlog)); 21 | dispatch(setTodo(json.data.todo)); 22 | dispatch(setInProgress(json.data.inProgress)); 23 | dispatch(setDone(json.data.done)); 24 | } 25 | dispatch(setLoading(false)); 26 | }) 27 | .catch((error) => { 28 | console.error("Error:", error); 29 | toast.error("Something went wrong"); 30 | dispatch(setLoading(false)); 31 | }); 32 | }; 33 | 34 | export default useAllTaskFilter; 35 | -------------------------------------------------------------------------------- /frontend/src/hooks/useDeleteTask.js: -------------------------------------------------------------------------------- 1 | import getHeader from "../utils/header"; 2 | import { toast } from "react-toastify"; 3 | import { setTaskDeleteM } from "../redux/slices/stateSlice"; 4 | import { 5 | deleteBacklogTask, 6 | deleteDoneTask, 7 | deleteInProgressTask, 8 | deleteTodoTask, 9 | } from "../redux/slices/taskSlice"; 10 | const useDeleteTask = (e, setLoad, dispatch, id) => { 11 | setLoad("Loading..."); 12 | e.target.disabled = true; 13 | fetch(`${import.meta.env.VITE_BACKEND_URL}/api/task/${id}`, { 14 | method: "DELETE", 15 | headers: getHeader(), 16 | }) 17 | .then((response) => response.json()) 18 | .then((json) => { 19 | setLoad(""); 20 | e.target.disabled = false; 21 | if (json?.message === "success") { 22 | toast.success("Task Deleted Successfully"); 23 | if (json.data.category == "to-do") { 24 | dispatch(deleteTodoTask(json.data)); 25 | } else if (json.data.category == "backlog") { 26 | dispatch(deleteBacklogTask(json.data)); 27 | } else if (json.data.category == "in-progress") { 28 | dispatch(deleteInProgressTask(json.data)); 29 | } else { 30 | dispatch(deleteDoneTask(json.data)); 31 | } 32 | dispatch(setTaskDeleteM(false)); 33 | } else { 34 | toast.error(json?.message); 35 | } 36 | }) 37 | .catch((error) => { 38 | console.error("Error:", error); 39 | setLoad(""); 40 | toast.error("Something went wrong"); 41 | e.target.disabled = false; 42 | }); 43 | }; 44 | 45 | export default useDeleteTask; 46 | -------------------------------------------------------------------------------- /frontend/src/hooks/useGetAnalytics.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { toast } from "react-toastify"; 3 | import getHeader from "../utils/header"; 4 | const useGetAnalytics = (setAnalytics) => { 5 | const getAnalytics = () => { 6 | fetch(`${import.meta.env.VITE_BACKEND_URL}/api/analytics`, { 7 | method: "GET", 8 | headers: getHeader(), 9 | }) 10 | .then((response) => response.json()) 11 | .then((json) => { 12 | if (json?.message === "success") { 13 | setAnalytics(json.data); 14 | } else { 15 | toast.error("Something went wrong"); 16 | } 17 | }) 18 | .catch((error) => { 19 | console.error("Error:", error); 20 | toast.error("Something went wrong"); 21 | }); 22 | }; 23 | useEffect(() => { 24 | getAnalytics(); 25 | }, []); 26 | }; 27 | 28 | export default useGetAnalytics; 29 | -------------------------------------------------------------------------------- /frontend/src/hooks/useGetTask.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { toast } from "react-toastify"; 3 | const useGetTask = (id, setTask) => { 4 | const getTask = () => { 5 | fetch(`${import.meta.env.VITE_BACKEND_URL}/api/task/${id}`, { 6 | method: "GET", 7 | headers: { "Content-Type": "application/json" }, 8 | }) 9 | .then((response) => response.json()) 10 | .then((json) => { 11 | if (json?.message === "success") { 12 | setTask(json.data); 13 | } else { 14 | toast.error("Something went wrong"); 15 | setTask(null); 16 | } 17 | }) 18 | .catch((error) => { 19 | console.error("Error:", error); 20 | toast.error("Something went wrong"); 21 | }); 22 | }; 23 | useEffect(() => { 24 | getTask(); 25 | }, []); 26 | }; 27 | 28 | export default useGetTask; 29 | -------------------------------------------------------------------------------- /frontend/src/hooks/useUpdateCategory.js: -------------------------------------------------------------------------------- 1 | import getHeader from "../utils/header"; 2 | import { toast } from "react-toastify"; 3 | import { 4 | setCategoryName, 5 | setUpdateCategoryM, 6 | } from "../redux/slices/stateSlice"; 7 | import { 8 | addBacklogTask, 9 | addDoneTask, 10 | addInProgressTask, 11 | addTodoTask, 12 | deleteBacklogTask, 13 | deleteDoneTask, 14 | deleteInProgressTask, 15 | deleteTodoTask, 16 | } from "../redux/slices/taskSlice"; 17 | const useUpdateCategory = ( 18 | e, 19 | setLoad, 20 | dispatch, 21 | taskId, 22 | newCategory, 23 | oldCategory 24 | ) => { 25 | setLoad("Loading..."); 26 | e.target.disabled = true; 27 | fetch(`${import.meta.env.VITE_BACKEND_URL}/api/task/category/${taskId}`, { 28 | method: "PUT", 29 | headers: getHeader(), 30 | body: JSON.stringify({ 31 | category: newCategory, 32 | }), 33 | }) 34 | .then((response) => response.json()) 35 | .then((json) => { 36 | setLoad(""); 37 | e.target.disabled = false; 38 | if (json?.message === "success") { 39 | toast.success("Changed the status of task"); 40 | dispatch(setUpdateCategoryM(false)); 41 | dispatch(setCategoryName("")); 42 | if (json.data.category == "to-do") { 43 | dispatch(addTodoTask(json.data)); 44 | } else if (json.data.category == "backlog") { 45 | dispatch(addBacklogTask(json.data)); 46 | } else if (json.data.category == "in-progress") { 47 | dispatch(addInProgressTask(json.data)); 48 | } else { 49 | dispatch(addDoneTask(json.data)); 50 | } 51 | if (oldCategory == "to-do") { 52 | dispatch(deleteTodoTask(json.data)); 53 | } else if (oldCategory == "backlog") { 54 | dispatch(deleteBacklogTask(json.data)); 55 | } else if (oldCategory == "in-progress") { 56 | dispatch(deleteInProgressTask(json.data)); 57 | } else { 58 | dispatch(deleteDoneTask(json.data)); 59 | } 60 | } else { 61 | toast.error(json?.message); 62 | } 63 | }) 64 | .catch((error) => { 65 | console.error("Error:", error); 66 | setLoad(""); 67 | toast.error("Something went wrong"); 68 | e.target.disabled = false; 69 | }); 70 | }; 71 | 72 | export default useUpdateCategory; 73 | -------------------------------------------------------------------------------- /frontend/src/hooks/useUpdateTask.js: -------------------------------------------------------------------------------- 1 | import getHeader from "../utils/header"; 2 | import { toast } from "react-toastify"; 3 | import { setTaskCardM, setTaskM } from "../redux/slices/stateSlice"; 4 | import { 5 | deleteBacklogTask, 6 | deleteDoneTask, 7 | deleteInProgressTask, 8 | deleteTodoTask, 9 | updateBacklogTask, 10 | updateDoneTask, 11 | updateInProgressTask, 12 | updateTodoTask, 13 | } from "../redux/slices/taskSlice"; 14 | const useUpdateTask = ( 15 | e, 16 | setLoad, 17 | title, 18 | priority, 19 | checklist, 20 | assign, 21 | dueDate, 22 | dispatch, 23 | id 24 | ) => { 25 | setLoad("Loading..."); 26 | e.target.disabled = true; 27 | fetch(`${import.meta.env.VITE_BACKEND_URL}/api/task/${id}`, { 28 | method: "PUT", 29 | headers: getHeader(), 30 | body: JSON.stringify({ 31 | title: title, 32 | priority: priority, 33 | checklist: checklist, 34 | assign: assign, 35 | dueDate: dueDate, 36 | }), 37 | }) 38 | .then((response) => response.json()) 39 | .then((json) => { 40 | setLoad(""); 41 | e.target.disabled = false; 42 | if (json?.message === "success") { 43 | toast.success("Task Updated Successfully"); 44 | if (json?.removeAssignCategory == "to-do") { 45 | dispatch(deleteTodoTask(json.data)); 46 | } else if (json?.removeAssignCategory == "in-progress") { 47 | dispatch(deleteInProgressTask(json.data)); 48 | } else if (json?.removeAssignCategory == "backlog") { 49 | dispatch(deleteBacklogTask(json.data)); 50 | } else if (json?.removeAssignCategory == "done") { 51 | dispatch(deleteDoneTask(json.data)); 52 | } else { 53 | if (json.data.category == "to-do") { 54 | dispatch(updateTodoTask(json.data)); 55 | } else if (json.data.category == "backlog") { 56 | dispatch(updateBacklogTask(json.data)); 57 | } else if (json.data.category == "in-progress") { 58 | dispatch(updateInProgressTask(json.data)); 59 | } else { 60 | dispatch(updateDoneTask(json.data)); 61 | } 62 | } 63 | dispatch(setTaskCardM(false)); 64 | dispatch(setTaskM("")); 65 | } else { 66 | toast.error(json?.message); 67 | } 68 | }) 69 | .catch((error) => { 70 | console.error("Error:", error); 71 | setLoad(""); 72 | toast.error("Something went wrong"); 73 | e.target.disabled = false; 74 | }); 75 | }; 76 | 77 | export default useUpdateTask; 78 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | font-family: "Poppins", sans-serif; 6 | font-style: normal; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | } 12 | #root, 13 | .app-container { 14 | height: 100vh; 15 | width: 100vw; 16 | } 17 | 18 | /* Loading Style */ 19 | .loading-container { 20 | position: absolute; 21 | left: 0px; 22 | top: 0px; 23 | z-index: 99; 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | height: 100vh; 28 | width: 100vw; 29 | } 30 | 31 | .loading-box { 32 | transform: translateY(-200%); 33 | } 34 | 35 | .loading { 36 | height: 60px; 37 | width: 60px; 38 | border-radius: 50%; 39 | border: 10px solid #8daab9cc; 40 | border-bottom: 10px solid transparent; 41 | animation: loading 1.5s infinite linear; 42 | } 43 | 44 | @keyframes loading { 45 | to { 46 | transform: rotate(0deg); 47 | } 48 | from { 49 | transform: rotate(360deg); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import "./index.css"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import store from "./redux/store"; 7 | import { Provider } from "react-redux"; 8 | import { ToastContainer } from "react-toastify"; 9 | import "react-toastify/dist/ReactToastify.css"; 10 | 11 | ReactDOM.createRoot(document.getElementById("root")).render( 12 | 13 | 14 | 15 | 33 | 34 | 35 | 36 | 37 | ); 38 | -------------------------------------------------------------------------------- /frontend/src/pages/Analytics.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "../css/Analytics.css"; 3 | import useGetAnalytics from "../hooks/useGetAnalytics"; 4 | import Loading from "../components/Loading"; 5 | const Analytics = () => { 6 | const [analytics, setAnalytics] = useState(null); 7 | useGetAnalytics(setAnalytics); 8 | 9 | return analytics == null ? ( 10 | 11 | ) : ( 12 |
13 |
14 |
15 |

Analytics

16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Backlog Tasks 24 |
25 | {analytics?.backlog} 26 |
27 |
28 |
29 |
30 | To-do Tasks 31 |
32 | {analytics?.todo} 33 |
34 |
35 |
36 |
37 | In-Progress Tasks 38 |
39 | {analytics?.inProgress} 40 |
41 |
42 |
43 |
44 | Completed Tasks 45 |
46 | {analytics?.done} 47 |
48 |
49 |
50 |
51 |
52 |
53 | Low Priority 54 |
55 | {analytics?.low} 56 |
57 |
58 |
59 |
60 | Moderate Priority 61 |
62 | {analytics?.moderate} 63 |
64 |
65 |
66 |
67 | High Priority 68 |
69 | {analytics?.high} 70 |
71 |
72 |
73 |
74 | Due Date Tasks 75 |
76 | {analytics?.dueDate} 77 |
78 |
79 |
80 |
81 | ); 82 | }; 83 | 84 | export default Analytics; 85 | -------------------------------------------------------------------------------- /frontend/src/pages/Backlog.jsx: -------------------------------------------------------------------------------- 1 | import { TbDots } from "react-icons/tb"; 2 | import TaskBox from "../components/TaskBox"; 3 | import { useSelector } from "react-redux"; 4 | 5 | const Backlog = ({ backlogCollapse }) => { 6 | const backlog = useSelector((store) => store.task.backlog); 7 | return ( 8 |
9 | {backlog?.map((task, index) => ( 10 | 15 | ))} 16 |
17 | ); 18 | }; 19 | 20 | export default Backlog; 21 | -------------------------------------------------------------------------------- /frontend/src/pages/Board.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Backlog from "./Backlog"; 3 | import ToDo from "./ToDo"; 4 | import InProgress from "./InProgress"; 5 | import Done from "./Done"; 6 | import { VscCollapseAll } from "react-icons/vsc"; 7 | import { AiOutlinePlus } from "react-icons/ai"; 8 | import { IoIosArrowDown } from "react-icons/io"; 9 | import "../css/Task.css"; 10 | import { useDispatch, useSelector } from "react-redux"; 11 | import { generateDate } from "../utils/generateDate"; 12 | import { LuUsers2 } from "react-icons/lu"; 13 | import { 14 | setAddPeopleM, 15 | setTaskCardM, 16 | setTaskFilterP, 17 | } from "../redux/slices/stateSlice"; 18 | import { TaskFilter } from "../components/PopUp"; 19 | import useAllTask from "../hooks/useAllTask"; 20 | const Board = () => { 21 | const [backlogCollapse, setBacklogCollapse] = useState(false); 22 | const [todoCollapse, setTodoCollapse] = useState(false); 23 | const [progressCollapse, setProgressCollapse] = useState(false); 24 | const [doneCollapse, setDoneCollapse] = useState(false); 25 | const auth = useSelector((store) => store.auth); 26 | const taskFilter = useSelector((store) => store.state.taskFilterP); 27 | const taskFilterName = useSelector((store) => store.state.taskFilterName); 28 | const dispatch = useDispatch(); 29 | 30 | const handleClickOutside = (event) => { 31 | if (taskFilter && !event?.target.closest(".popup-box")) { 32 | dispatch(setTaskFilterP(false)); 33 | } 34 | }; 35 | useEffect(() => { 36 | document.addEventListener("mousedown", handleClickOutside); 37 | return () => { 38 | document.removeEventListener("mousedown", handleClickOutside); 39 | }; 40 | }, [taskFilter]); 41 | 42 | useAllTask(); 43 | 44 | return ( 45 |
46 |
47 |
48 |

Welcome! {auth.name.split(" ")[0]}

49 |

{generateDate()}

50 |
51 |
52 |
53 |

Board

54 | dispatch(setAddPeopleM(true))} 57 | > 58 | 59 | Add People 60 | 61 |
62 | 63 | dispatch(setTaskFilterP(true))}> 64 |

{taskFilterName}

65 | 66 |
67 | {taskFilter && } 68 |
69 |
70 |
71 |
72 |
73 |

74 | Backlog{" "} 75 | setBacklogCollapse(!backlogCollapse)} 79 | /> 80 |

81 | 82 |
83 |
84 |

85 | To Do 86 | 87 | dispatch(setTaskCardM(true))} 91 | /> 92 | setTodoCollapse(!todoCollapse)} 96 | /> 97 | 98 |

99 | 100 |
101 |
102 |

103 | In Progress{" "} 104 | 108 | setProgressCollapse(!progressCollapse) 109 | } 110 | /> 111 |

112 | 113 |
114 |
115 |

116 | Done{" "} 117 | setDoneCollapse(!doneCollapse)} 121 | /> 122 |

123 | 124 |
125 |
126 |
127 | ); 128 | }; 129 | 130 | export default Board; 131 | -------------------------------------------------------------------------------- /frontend/src/pages/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import Navbar from "./Navbar"; 3 | const Board = lazy(() => import("./Board.jsx")); 4 | const Analytics = lazy(() => import("./Analytics.jsx")); 5 | const Settings = lazy(() => import("./Settings.jsx")); 6 | import { useSelector } from "react-redux"; 7 | import "../css/Dashboard.css"; 8 | import { 9 | AddedPeople, 10 | AddPeople, 11 | Logout, 12 | TaskCard, 13 | TaskDelete, 14 | UpdateCategory, 15 | } from "../components/Model"; 16 | 17 | const Dashboard = () => { 18 | const state = useSelector((store) => store.state); 19 | return ( 20 | <> 21 | 22 | {state.dashboardSection == "board" && } 23 | {state.dashboardSection == "analytics" && } 24 | {state.dashboardSection == "settings" && } 25 | {state.addPeopleM && } 26 | {state.addedPeopleM && } 27 | {state.logoutM && } 28 | {state.taskDeleteM && } 29 | {state.taskCardM && } 30 | {state.updateCategoryM && } 31 | 32 | ); 33 | }; 34 | 35 | export default Dashboard; 36 | -------------------------------------------------------------------------------- /frontend/src/pages/Done.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import TaskBox from "../components/TaskBox"; 3 | 4 | const Done = ({ doneCollapse }) => { 5 | const done = useSelector((store) => store.task.done); 6 | return ( 7 |
8 | {done?.map((task, index) => ( 9 | 10 | ))} 11 |
12 | ); 13 | }; 14 | 15 | export default Done; 16 | -------------------------------------------------------------------------------- /frontend/src/pages/InProgress.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import TaskBox from "../components/TaskBox"; 3 | 4 | const InProgress = ({ progressCollapse }) => { 5 | const inProgress = useSelector((store) => store.task.inProgress); 6 | return ( 7 |
8 | {inProgress?.map((task, index) => ( 9 | 14 | ))} 15 |
16 | ); 17 | }; 18 | 19 | export default InProgress; 20 | -------------------------------------------------------------------------------- /frontend/src/pages/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { PiCodesandboxLogoDuotone } from "react-icons/pi"; 2 | import { MdOutlineSpaceDashboard } from "react-icons/md"; 3 | import { GoDatabase } from "react-icons/go"; 4 | import { IoLogOutOutline, IoSettingsOutline } from "react-icons/io5"; 5 | import "../css/Navbar.css"; 6 | import { useDispatch, useSelector } from "react-redux"; 7 | import { setLogoutM, updateDashboardSection } from "../redux/slices/stateSlice"; 8 | import { Link } from "react-router-dom"; 9 | 10 | const Navbar = () => { 11 | const dispatch = useDispatch(); 12 | const dashboardSection = useSelector( 13 | (store) => store.state.dashboardSection 14 | ); 15 | 16 | return ( 17 |
18 | 19 | 20 | Pro Manage 21 | 22 |
23 |
24 | 26 | dispatch(updateDashboardSection("board")) 27 | } 28 | className={`nav-link ${ 29 | dashboardSection == "board" && "nav-active" 30 | }`} 31 | > 32 | 33 | Board 34 | 35 | 37 | dispatch(updateDashboardSection("analytics")) 38 | } 39 | className={`nav-link ${ 40 | dashboardSection == "analytics" && "nav-active" 41 | }`} 42 | > 43 | 44 | Analytics 45 | 46 | 48 | dispatch(updateDashboardSection("settings")) 49 | } 50 | className={`nav-link ${ 51 | dashboardSection == "settings" && "nav-active" 52 | }`} 53 | > 54 | 55 | Settings 56 | 57 |
58 |
59 | dispatch(setLogoutM(true))} 62 | > 63 | 64 | Log out 65 | 66 |
67 |
68 |
69 | ); 70 | }; 71 | 72 | export default Navbar; 73 | -------------------------------------------------------------------------------- /frontend/src/pages/PageNotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../css/Page_Not_Found.css"; 3 | import { Link } from "react-router-dom"; 4 | const PageNotFound = () => { 5 | return ( 6 |
7 |

Page Not Found

8 |

404

9 | 10 | Back 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default PageNotFound; 17 | -------------------------------------------------------------------------------- /frontend/src/pages/Register_Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { toast } from "react-toastify"; 4 | import { checkValidSignInFrom, checkValidSignUpFrom } from "../utils/validate"; 5 | import { PiEye, PiEyeClosedLight } from "react-icons/pi"; 6 | import { CiLock, CiMail, CiUser } from "react-icons/ci"; 7 | import { useDispatch } from "react-redux"; 8 | import { addAuth } from "../redux/slices/authSlice"; 9 | import AuthImage from "../assets/AuthImage.png"; 10 | import "../css/Register_Login.css"; 11 | 12 | const Register_Login = () => { 13 | const [isRegister, setIsRegister] = useState(false); 14 | const [name, setName] = useState(""); 15 | const [email, setEmail] = useState(""); 16 | const [password, setPassword] = useState(""); 17 | const [cpassword, setCPassword] = useState(""); 18 | const [load, setLoad] = useState(""); 19 | const [isShow, setIsShow] = useState(false); 20 | const [isShowC, setIsShowC] = useState(false); 21 | const navigate = useNavigate(); 22 | const dispatch = useDispatch(); 23 | 24 | // Login 25 | const logInUser = (e) => { 26 | toast.loading("Wait until you SignIn"); 27 | e.target.disabled = true; 28 | fetch(`${import.meta.env.VITE_BACKEND_URL}/api/auth/login`, { 29 | method: "POST", 30 | headers: { 31 | "Content-Type": "application/json", 32 | }, 33 | body: JSON.stringify({ 34 | email: email, 35 | password: password, 36 | }), 37 | }) 38 | .then((response) => response.json()) 39 | .then((json) => { 40 | setLoad(""); 41 | e.target.disabled = false; 42 | toast.dismiss(); 43 | if (json.token) { 44 | localStorage.setItem("token", json.token); 45 | dispatch(addAuth(json.data)); 46 | navigate("/"); 47 | toast.success(json?.message); 48 | } else { 49 | toast.error(json?.message); 50 | } 51 | }) 52 | .catch((error) => { 53 | console.error("Error:", error); 54 | setLoad(""); 55 | toast.dismiss(); 56 | toast.error("Something went wrong"); 57 | e.target.disabled = false; 58 | }); 59 | }; 60 | const handleLogin = (e) => { 61 | if (email && password) { 62 | const validError = checkValidSignInFrom(email, password); 63 | if (validError) { 64 | toast.error(validError); 65 | return; 66 | } 67 | setLoad("Loading..."); 68 | logInUser(e); 69 | } else { 70 | toast.error("All fields are required"); 71 | } 72 | }; 73 | 74 | // Signup 75 | const registerUser = (e) => { 76 | toast.loading("Wait until you SignUp"); 77 | e.target.disabled = true; 78 | fetch(`${import.meta.env.VITE_BACKEND_URL}/api/auth/register`, { 79 | method: "POST", 80 | headers: { 81 | "Content-Type": "application/json", 82 | }, 83 | body: JSON.stringify({ 84 | name: name, 85 | email: email, 86 | password: password, 87 | }), 88 | }) 89 | .then((response) => response.json()) 90 | .then((json) => { 91 | setLoad(""); 92 | e.target.disabled = false; 93 | toast.dismiss(); 94 | if (json.token) { 95 | setIsRegister(false); 96 | setPassword(""); 97 | setCPassword(""); 98 | toast.success(json?.message); 99 | } else { 100 | toast.error(json?.message); 101 | } 102 | }) 103 | .catch((error) => { 104 | console.error("Error:", error); 105 | setLoad(""); 106 | toast.dismiss(); 107 | toast.error("Something went wrong"); 108 | e.target.disabled = false; 109 | }); 110 | }; 111 | const handleRegister = (e) => { 112 | if (name && email && password) { 113 | const validError = checkValidSignUpFrom(name, email, password); 114 | if (validError) { 115 | toast.error(validError); 116 | return; 117 | } 118 | if (password != cpassword) { 119 | toast.error("Passwords don't match"); 120 | return; 121 | } 122 | setLoad("Loading..."); 123 | registerUser(e); 124 | } else { 125 | toast.error("All fields are required"); 126 | } 127 | }; 128 | const handleName = (name) => { 129 | name = name.charAt(0).toUpperCase() + name.slice(1); 130 | setName(name); 131 | }; 132 | 133 | const handlePassword = (password) => { 134 | password = password.trim(); 135 | setPassword(password); 136 | }; 137 | return ( 138 |
139 |
140 |
141 |
142 | AuthImage 147 |
148 |

Welcome aboard my friend

149 | just a couple of clicks and we start 150 |
151 |
152 |

{isRegister ? "Register" : "Login"}

153 | {isRegister && ( 154 |
155 | 156 | 157 | 158 | handleName(e.target.value)} 165 | required 166 | /> 167 |
168 | )} 169 |
170 | 171 | 172 | 173 | setEmail(e.target.value)} 180 | required 181 | /> 182 |
183 |
184 | 185 | 186 | 187 | handlePassword(e.target.value)} 194 | /> 195 | setIsShow(!isShow)} 197 | className="hide_show_btn" 198 | > 199 | {isShow ? ( 200 | 201 | ) : ( 202 | 203 | )} 204 | 205 |
206 | {isRegister && ( 207 |
208 | 209 | 210 | 211 | setCPassword(e.target.value)} 217 | /> 218 | setIsShowC(!isShowC)} 220 | className="hide_show_btn" 221 | > 222 | {isShowC ? ( 223 | 224 | ) : ( 225 | 226 | )} 227 | 228 |
229 | )} 230 | 243 |

244 | {isRegister ? "Have an account ?" : "Have no account yet?"} 245 |

246 | 253 | {/*
*/} 254 | 255 | 256 | ); 257 | }; 258 | 259 | export default Register_Login; 260 | -------------------------------------------------------------------------------- /frontend/src/pages/Settings.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { toast } from "react-toastify"; 3 | import { checkValidSignUpFrom } from "../utils/validate"; 4 | import { PiEye, PiEyeClosedLight } from "react-icons/pi"; 5 | import { CiLock, CiMail, CiUser } from "react-icons/ci"; 6 | import { useDispatch, useSelector } from "react-redux"; 7 | import { useNavigate } from "react-router-dom"; 8 | import { removeAuth } from "../redux/slices/authSlice"; 9 | import getHeader from "../utils/header"; 10 | 11 | const Settings = () => { 12 | const auth = useSelector((store) => store.auth); 13 | const [name, setName] = useState(auth.name); 14 | const [email, setEmail] = useState(auth.email); 15 | const [oldPassword, setOldPassword] = useState(""); 16 | const [newPassword, setNewPassword] = useState(""); 17 | const [load, setLoad] = useState(""); 18 | const [isShowO, setIsShowO] = useState(false); 19 | const [isShowN, setIsShowN] = useState(false); 20 | const dashboardSection = useSelector( 21 | (store) => store.state.dashboardSection 22 | ); 23 | const dispatch = useDispatch(); 24 | const navigate = useNavigate(); 25 | 26 | const handleName = (name) => { 27 | name = name.charAt(0).toUpperCase() + name.slice(1); 28 | setName(name); 29 | }; 30 | 31 | const handlePasswordOld = (oldPassword) => { 32 | oldPassword = oldPassword.trim(); 33 | setOldPassword(oldPassword); 34 | }; 35 | const handlePasswordNew = (newPassword) => { 36 | newPassword = newPassword.trim(); 37 | setNewPassword(newPassword); 38 | }; 39 | // Update 40 | const updateUser = (e) => { 41 | toast.loading("Wait until you SignUp"); 42 | e.target.disabled = true; 43 | fetch(`${import.meta.env.VITE_BACKEND_URL}/api/user/update`, { 44 | method: "PUT", 45 | headers: getHeader(), 46 | body: JSON.stringify({ 47 | name: name, 48 | email: email, 49 | oldPassword: oldPassword, 50 | newPassword: newPassword, 51 | }), 52 | }) 53 | .then((response) => response.json()) 54 | .then((json) => { 55 | setLoad(""); 56 | e.target.disabled = false; 57 | toast.dismiss(); 58 | if (json?.message === "success") { 59 | toast.success("Update Successfully"); 60 | localStorage.removeItem("token"); 61 | dispatch(removeAuth()); 62 | navigate("/login"); 63 | } else { 64 | toast.error(json?.message); 65 | } 66 | }) 67 | .catch((error) => { 68 | console.error("Error:", error); 69 | setLoad(""); 70 | toast.dismiss(); 71 | toast.error("Something went wrong"); 72 | e.target.disabled = false; 73 | }); 74 | }; 75 | 76 | const handleUpdate = (e) => { 77 | if (name && email && newPassword && oldPassword) { 78 | const validError = checkValidSignUpFrom(name, email, newPassword); 79 | if (validError) { 80 | toast.error(validError); 81 | return; 82 | } 83 | setLoad("Loading..."); 84 | updateUser(e); 85 | } else { 86 | toast.error("All fields are required"); 87 | } 88 | }; 89 | return ( 90 |
91 |
92 |
93 |

Settings

94 |
95 |
96 |
97 |
98 |
99 | 100 | 101 | 102 | handleName(e.target.value)} 109 | required 110 | /> 111 |
112 |
113 | 114 | 115 | 116 | setEmail(e.target.value)} 123 | required 124 | /> 125 |
126 |
127 | 128 | 129 | 130 | handlePasswordOld(e.target.value)} 137 | /> 138 | setIsShowO(!isShowO)} 140 | className="hide_show_btn" 141 | > 142 | {isShowO ? ( 143 | 144 | ) : ( 145 | 146 | )} 147 | 148 |
149 |
150 | 151 | 152 | 153 | handlePasswordNew(e.target.value)} 160 | /> 161 | setIsShowN(!isShowN)} 163 | className="hide_show_btn" 164 | > 165 | {isShowN ? ( 166 | 167 | ) : ( 168 | 169 | )} 170 | 171 |
172 | 181 |
182 |
183 |
184 | ); 185 | }; 186 | 187 | export default Settings; 188 | -------------------------------------------------------------------------------- /frontend/src/pages/ToDo.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import TaskBox from "../components/TaskBox"; 3 | 4 | const ToDo = ({ todoCollapse }) => { 5 | const todo = useSelector((store) => store.task.todo); 6 | return ( 7 |
8 | {todo?.map((task, index) => ( 9 | 10 | ))} 11 |
12 | ); 13 | }; 14 | 15 | export default ToDo; 16 | -------------------------------------------------------------------------------- /frontend/src/redux/slices/authSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const authSlice = createSlice({ 4 | name: "auth", 5 | initialState: null, 6 | reducers: { 7 | addAuth: (state, action) => { 8 | return action.payload; 9 | }, 10 | removeAuth: (state) => { 11 | return null; 12 | }, 13 | }, 14 | }); 15 | export const { addAuth, removeAuth } = authSlice.actions; 16 | export default authSlice.reducer; 17 | -------------------------------------------------------------------------------- /frontend/src/redux/slices/stateSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const stateSlice = createSlice({ 4 | name: "state", 5 | initialState: { 6 | dashboardSection: "board", 7 | loading: false, 8 | addPeopleM: false, 9 | addedPeopleM: false, 10 | logoutM: false, 11 | taskDeleteM: false, 12 | taskCardM: false, 13 | taskFilterP: false, 14 | taskFilterName: "This month", 15 | taskMId: "", 16 | taskM: "", 17 | boardEmail: "", 18 | updateCategoryM: false, 19 | categoryName: "", 20 | }, 21 | reducers: { 22 | updateDashboardSection: (state, action) => { 23 | state.dashboardSection = action.payload; 24 | }, 25 | setLoading: (state, action) => { 26 | state.loading = action.payload; 27 | }, 28 | setAddPeopleM: (state, action) => { 29 | state.addPeopleM = action.payload; 30 | }, 31 | setAddedPeopleM: (state, action) => { 32 | state.addedPeopleM = action.payload; 33 | }, 34 | setLogoutM: (state, action) => { 35 | state.logoutM = action.payload; 36 | }, 37 | setTaskDeleteM: (state, action) => { 38 | state.taskDeleteM = action.payload; 39 | }, 40 | setTaskCardM: (state, action) => { 41 | state.taskCardM = action.payload; 42 | }, 43 | setTaskFilterP: (state, action) => { 44 | state.taskFilterP = action.payload; 45 | }, 46 | setTaskFilterName: (state, action) => { 47 | state.taskFilterName = action.payload; 48 | }, 49 | setTaskMId: (state, action) => { 50 | state.taskMId = action.payload; 51 | }, 52 | setTaskM: (state, action) => { 53 | state.taskM = action.payload; 54 | }, 55 | setBoardEmail: (state, action) => { 56 | state.boardEmail = action.payload; 57 | }, 58 | setUpdateCategoryM: (state, action) => { 59 | state.updateCategoryM = action.payload; 60 | }, 61 | setCategoryName: (state, action) => { 62 | state.categoryName = action.payload; 63 | }, 64 | }, 65 | }); 66 | export const { 67 | updateDashboardSection, 68 | setLoading, 69 | setAddPeopleM, 70 | setAddedPeopleM, 71 | setLogoutM, 72 | setTaskDeleteM, 73 | setTaskCardM, 74 | setTaskFilterP, 75 | setTaskFilterName, 76 | setTaskMId, 77 | setTaskM, 78 | setBoardEmail, 79 | setUpdateCategoryM, 80 | setCategoryName, 81 | } = stateSlice.actions; 82 | export default stateSlice.reducer; 83 | -------------------------------------------------------------------------------- /frontend/src/redux/slices/taskSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const taskSlice = createSlice({ 4 | name: "task", 5 | initialState: { 6 | backlog: [], 7 | todo: [], 8 | inProgress: [], 9 | done: [], 10 | }, 11 | reducers: { 12 | setBacklog: (state, action) => { 13 | state.backlog = action.payload; 14 | }, 15 | setTodo: (state, action) => { 16 | state.todo = action.payload; 17 | }, 18 | setInProgress: (state, action) => { 19 | state.inProgress = action.payload; 20 | }, 21 | setDone: (state, action) => { 22 | state.done = action.payload; 23 | }, 24 | addBacklogTask: (state, action) => { 25 | state.backlog = [...state.backlog, action.payload]; 26 | }, 27 | addTodoTask: (state, action) => { 28 | state.todo = [...state.todo, action.payload]; 29 | }, 30 | addInProgressTask: (state, action) => { 31 | state.inProgress = [...state.inProgress, action.payload]; 32 | }, 33 | addDoneTask: (state, action) => { 34 | state.done = [...state.done, action.payload]; 35 | }, 36 | updateBacklogTask: (state, action) => { 37 | state.backlog = state.backlog.map((t) => 38 | t._id == action.payload._id ? action.payload : t 39 | ); 40 | }, 41 | updateTodoTask: (state, action) => { 42 | state.todo = state.todo.map((t) => 43 | t._id == action.payload._id ? action.payload : t 44 | ); 45 | }, 46 | updateInProgressTask: (state, action) => { 47 | state.inProgress = state.inProgress.map((t) => 48 | t._id == action.payload._id ? action.payload : t 49 | ); 50 | }, 51 | updateDoneTask: (state, action) => { 52 | state.done = state.done.map((t) => 53 | t._id == action.payload._id ? action.payload : t 54 | ); 55 | }, 56 | deleteBacklogTask: (state, action) => { 57 | state.backlog = state.backlog.filter( 58 | (t) => t._id != action.payload._id 59 | ); 60 | }, 61 | deleteTodoTask: (state, action) => { 62 | state.todo = state.todo.filter((t) => t._id != action.payload._id); 63 | }, 64 | deleteInProgressTask: (state, action) => { 65 | state.inProgress = state.inProgress.filter( 66 | (t) => t._id != action.payload._id 67 | ); 68 | }, 69 | deleteDoneTask: (state, action) => { 70 | state.done = state.done.filter((t) => t._id != action.payload._id); 71 | }, 72 | }, 73 | }); 74 | export const { 75 | setBacklog, 76 | setTodo, 77 | setInProgress, 78 | setDone, 79 | addBacklogTask, 80 | addTodoTask, 81 | addInProgressTask, 82 | addDoneTask, 83 | updateBacklogTask, 84 | updateTodoTask, 85 | updateInProgressTask, 86 | updateDoneTask, 87 | deleteBacklogTask, 88 | deleteTodoTask, 89 | deleteInProgressTask, 90 | deleteDoneTask, 91 | } = taskSlice.actions; 92 | export default taskSlice.reducer; 93 | -------------------------------------------------------------------------------- /frontend/src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import authSlice from "./slices/authSlice"; 3 | import stateSlice from "./slices/stateSlice"; 4 | import taskSlice from "./slices/taskSlice"; 5 | 6 | const store = configureStore({ 7 | reducer: { 8 | auth: authSlice, 9 | state: stateSlice, 10 | task: taskSlice, 11 | }, 12 | }); 13 | 14 | export default store; 15 | -------------------------------------------------------------------------------- /frontend/src/utils/generateDate.js: -------------------------------------------------------------------------------- 1 | export const generateDate = () => { 2 | return new Date(Date.now()) 3 | .toDateString() 4 | .split(" ") 5 | .slice(2, 4) 6 | .join("th -, ") 7 | .replace("-", new Date(Date.now()).toDateString().split(" ")[1]); 8 | }; 9 | 10 | export const getMonthDate = (date) => { 11 | return new Date(date).toDateString().split(" ").splice(1, 2).join(" "); 12 | }; 13 | 14 | // simple date formate MM/DD/YYYY 15 | export const simpleDate = (date) => { 16 | return new Date(date) 17 | .toLocaleDateString() 18 | .split("/") 19 | .splice(1) 20 | .join("/" + new Date(date).getDate() + "/"); 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/src/utils/header.js: -------------------------------------------------------------------------------- 1 | const getHeader = () => { 2 | const token = localStorage.getItem("token"); 3 | return { 4 | "Content-Type": "application/json", 5 | Authorization: `Bearer ${token}`, 6 | }; 7 | }; 8 | 9 | export default getHeader; 10 | -------------------------------------------------------------------------------- /frontend/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | export const checkValidSignInFrom = (email, password) => { 2 | const isEmailValid = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/.test( 3 | email 4 | ); 5 | const isPasswordValid = 6 | /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^((0-9)|(a-z)|(A-Z)|\s)]).{8,}$/.test( 7 | password 8 | ); 9 | if (!isEmailValid) return "Invalid email format"; 10 | if (!isPasswordValid) return "Invalid password"; 11 | return null; 12 | }; 13 | export const checkValidSignUpFrom = (name, email, password) => { 14 | const isName = /\b([A-ZÀ-ÿ][-,a-z. ']+[ ]*)+/.test(name); 15 | const isEmailValid = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/.test( 16 | email 17 | ); 18 | if (!isName) return "Invalid name format"; 19 | if (!isEmailValid) return "Invalid email format"; 20 | if (password.length < 8) return "Min 8 characters"; 21 | if (!/[a-z]/.test(password)) return "Needs 1 lowercase letter"; 22 | if (!/[A-Z]/.test(password)) return "Needs 1 uppercase lette"; 23 | if (!/\d/.test(password)) return "Needs 1 number"; 24 | if (!/[^((0-9)|(a-z)|(A-Z)|\s)]/.test(password)) 25 | return "Needs 1 special char"; 26 | return null; 27 | }; 28 | 29 | export const checkValidEmail = (email) => { 30 | const isEmailValid = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/.test( 31 | email 32 | ); 33 | if (!isEmailValid) return "Invalid email format"; 34 | return null; 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }] 3 | } 4 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------