├── .eslintrc.cjs ├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── package.json ├── server ├── controllers │ ├── boardsController.ts │ ├── tasksController.ts │ └── userController.ts ├── models │ ├── boardModel.ts │ ├── cardModel.ts │ └── userModel.ts ├── routes │ ├── boardsRouter.ts │ ├── tasksRouter.ts │ └── userRouter.ts └── server.ts ├── src ├── App.tsx ├── assets │ ├── Dashboard.png │ ├── authPage.png │ ├── edit-cover-1481-svgrepo-com.svg │ ├── note-text-svgrepo-com.svg │ ├── projectFrame.png │ ├── react.svg │ └── taskCreation.png ├── components │ ├── Card.tsx │ ├── Column.tsx │ ├── CreateBoardModal.tsx │ ├── EditBoardModal.tsx │ ├── EditTaskModal.tsx │ ├── Login.tsx │ ├── NewTaskModal.tsx │ ├── Settings.tsx │ └── Signup.tsx ├── containers │ ├── ColumnContainer.tsx │ ├── LeftContainer.tsx │ └── MainContainer.tsx ├── main.tsx ├── routes │ ├── Authentication.tsx │ └── Dashboard.tsx ├── scss │ ├── app.scss │ ├── authContainer.scss │ ├── authWrapper.scss │ ├── columnContainer.scss │ ├── dashBoard.scss │ ├── leftContainer.scss │ ├── mainContainer.scss │ ├── modal.scss │ └── taskCard.scss ├── types.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.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 | package-lock.json 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | .env 16 | .DS_Store 17 | 18 | # Editor directories and files 19 | .vscode/* 20 | !.vscode/extensions.json 21 | .idea 22 | .DS_Store 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 OS-Builders 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 Pro 2 | 3 | Task Pro is an intuitive web application crafted to streamline project and task management. 4 | 5 | It empowers users of all proficiency levels to enhance their time management and organizational abilities with its user-friendly interface. 6 | 7 | Whether you opt for private usage or collaboration, Task Pro offers a comprehensive solution for creating, organizing, and sharing projects seamlessly. 8 | 9 | ## Technologies 10 | 11 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) ![React Router](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white) ![SASS](https://img.shields.io/badge/SASS-hotpink.svg?style=for-the-badge&logo=SASS&logoColor=white) ![Vite](https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white) ![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white) ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) ![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) 12 | 13 | ## Project Frames 14 | 15 | ![Project Frame](src/assets/projectFrame.png) 16 | 17 | ## Features 18 | 19 | ### User Sign-Up and Login 20 | 21 | - Easily create a user profile to personalize user experience. 22 | - Secure authentication ensures your private boards are only accessible to you. 23 | 24 | ![Authentication](src/assets/authPage.png) 25 | 26 | ### Board Management 27 | 28 | - Create, edit and delete boards with ease. 29 | - Instantly swap between boards to display their corresponding tasks. 30 | - All changes to boards and tasks are maintained on the frontend and backend. 31 | 32 | ![Dashboard](src/assets/Dashboard.png) 33 | 34 | ### Task Management 35 | 36 | - Create any number of new task cards with name, notes and status. 37 | - Edit and delete task cards as needed. 38 | - Move cards from one status column to another. 39 | 40 | ![taskCreation](src/assets/taskCreation.png) 41 | 42 | ## Front End 43 | 44 | - Developed using **React** for a responsive and dynamic user interface. 45 | - Utilizes **React Router** for smooth navigation between pages. 46 | - Stylish and customizable design with **SASS** for a modern look and organized styling. 47 | 48 | ## Back End 49 | 50 | - Powered by **Node.js** and **Express** for robust server-side functionality. 51 | - Data storage and retrieval are handled by **MongoDB**, ensuring data persistence and flexibility. 52 | 53 | ## Stretch Features 54 | 55 | In the future, we plan to introduce the following features: 56 | 57 | - Keep your project sets private for personal use or collaborate them with others. 58 | - Share sets can be accessed together with multiple method of invites, keeping team on the same page. 59 | - Enhance the Project-sharing system with comments for better communication. 60 | - Drag and drop to improve user experiences. 61 | - Light and Dark mode. 62 | - OTP/Email 2 step authentication. 63 | 64 | ## To Launch the Application 65 | 66 | **Step 1**. Clone repo to code editor 67 | 68 | **Step 2**. Run npm install to install all dependencies 69 | 70 | **Step 3**. Make sure node version is 18.17.1 or older, can use nvm to install the needed version. 71 | 72 | **Step 4**. Create env file and make sure to have `PORT = 3000` and `MONGO_URI = (Mongodb connection URI)` 73 | 74 | **Step 5**. start the project with `npm start` or `npm run dev` (for dev mode) 75 | 76 | ## Authors 77 | 78 | - Nam Ha: [Github](https://github.com/namos2502) 79 | 80 | - John Costello: [Github](https://github.com/johnlcos) 81 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Task Pro 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-pro", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite & nodemon server/server.ts --allowImportingTsExtensions --open", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@types/bcrypt": "^5.0.2", 14 | "bcrypt": "^5.1.1", 15 | "bcryptjs": "^2.4.3", 16 | "dotenv": "^16.3.1", 17 | "express": "^4.18.2", 18 | "mongodb": "^6.3.0", 19 | "mongoose": "^8.0.4", 20 | "nvm": "^0.0.4", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-router": "^6.21.1", 24 | "react-router-dom": "^6.21.1", 25 | "sass": "^1.69.7", 26 | "ts-node": "^10.9.2" 27 | }, 28 | "devDependencies": { 29 | "@types/bcryptjs": "^2.4.6", 30 | "@types/express": "^4.17.21", 31 | "@types/react": "^18.2.43", 32 | "@types/react-dom": "^18.2.17", 33 | "@typescript-eslint/eslint-plugin": "^6.14.0", 34 | "@typescript-eslint/parser": "^6.14.0", 35 | "@vitejs/plugin-react": "^4.2.1", 36 | "eslint": "^8.55.0", 37 | "eslint-plugin-react-hooks": "^4.6.0", 38 | "eslint-plugin-react-refresh": "^0.4.5", 39 | "nodemon": "^3.0.2", 40 | "typescript": "^5.3.3", 41 | "vite": "^5.0.8" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /server/controllers/boardsController.ts: -------------------------------------------------------------------------------- 1 | import User from "../models/userModel.ts"; 2 | import Board from "../models/boardModel.ts"; 3 | import { NextFunction, Request, Response } from "express"; 4 | import { BoardListItemState, BoardType } from "../../src/types.ts"; 5 | 6 | const boardsController = { 7 | getMyBoards: async (req: Request, res: Response, next: NextFunction) => { 8 | try { 9 | // find all boards from the user, push board names into an array and save on locals 10 | const user = await User.findOne({ _id: req.params.userId }).populate( 11 | "boards" 12 | ); 13 | if (!user) { 14 | return next({ 15 | log: `boardsController.getMyBoards ERROR: User cannot be found.`, 16 | status: 500, 17 | message: { err: "Cannot find user." }, 18 | }); 19 | } 20 | res.locals.boards = user.boards; 21 | return next(); 22 | } catch (err) { 23 | // pass error through to global error handler 24 | return next({ 25 | log: `boardssController.getMyBoards ERROR: ${err}`, 26 | status: 500, 27 | message: { err: "Error getting my boards" }, 28 | }); 29 | } 30 | }, 31 | getBoardNamesAndIds: async ( 32 | _req: Request, 33 | res: Response, 34 | next: NextFunction 35 | ) => { 36 | try { 37 | const namesAndIds: BoardListItemState[] = []; 38 | res.locals.boards.forEach((board: BoardType) => { 39 | namesAndIds.push({ 40 | name: board.name, 41 | id: board._id, 42 | }); 43 | }); 44 | res.locals.namesAndIds = namesAndIds; 45 | return next(); 46 | } catch (err) { 47 | // pass error through to global error handler 48 | return next({ 49 | log: `boardssController.getBoardNamesAndIds ERROR: ${err}`, 50 | status: 500, 51 | message: { err: "Error refining boards down to names and ids" }, 52 | }); 53 | } 54 | }, 55 | createBoard: async (req: Request, res: Response, next: NextFunction) => { 56 | try { 57 | const createdBoard = await Board.create({ 58 | name: req.body.boardName, 59 | backlog: [], 60 | inProgress: [], 61 | inReview: [], 62 | completed: [], 63 | boardOwner: req.body.userId, 64 | }); 65 | res.locals.createdBoard = createdBoard; 66 | return next(); 67 | } catch (err) { 68 | // pass error through to global error handler 69 | return next({ 70 | log: `boardssController.createBoard ERROR: ${err}`, 71 | status: 500, 72 | message: { err: "Error creating new board" }, 73 | }); 74 | } 75 | }, 76 | assignNewBoard: async (req: Request, res: Response, next: NextFunction) => { 77 | try { 78 | await User.updateOne( 79 | { _id: req.body.userId }, 80 | { $push: { boards: res.locals.createdBoard._id } } 81 | ); 82 | return next(); 83 | } catch (err) { 84 | // pass error through to global error handler 85 | return next({ 86 | log: `boardssController.assignNewBoard ERROR: ${err}`, 87 | status: 500, 88 | message: { err: "Error assigning board to user" }, 89 | }); 90 | } 91 | }, 92 | getCurrentBoard: async (req: Request, res: Response, next: NextFunction) => { 93 | try { 94 | // obtain the user document 95 | const user = await User.findOne({ _id: req.query.user }).populate( 96 | "boards" 97 | ); 98 | if (!user) { 99 | return next({ 100 | log: `boardsController.getCurrentBoard ERROR: User cannot be found.`, 101 | status: 500, 102 | message: { err: "Cannot find board." }, 103 | }); 104 | } 105 | // Find the board by ID 106 | const board = user.boards.find( 107 | (boardObj) => boardObj._id.toString() === req.query.board 108 | ); 109 | if (!board) { 110 | return next({ 111 | log: `boardsController.getCurrentBoard ERROR: Board cannot be found.`, 112 | status: 500, 113 | message: { err: "Cannot find board." }, 114 | }); 115 | } 116 | res.locals.board = board; 117 | return next(); 118 | } catch (err) { 119 | // pass error through to global error handler 120 | return next({ 121 | log: `boardssController.getCurrentBoard ERROR: ${err}`, 122 | status: 500, 123 | message: { err: "Error getting current board" }, 124 | }); 125 | } 126 | }, 127 | getBoardFromId: async (req: Request, res: Response, next: NextFunction) => { 128 | try { 129 | // obtain the user document 130 | const board = await Board.findOne({ _id: req.params.boardId }); 131 | res.locals.board = board; 132 | return next(); 133 | } catch (err) { 134 | // pass error through to global error handler 135 | return next({ 136 | log: `boardssController.getBoardFromId ERROR: ${err}`, 137 | status: 500, 138 | message: { err: "Error getting board" }, 139 | }); 140 | } 141 | }, 142 | deleteBoard: async (req: Request, _res: Response, next: NextFunction) => { 143 | try { 144 | await Board.findOneAndDelete({ 145 | _id: req.params.boardId, 146 | }); 147 | return next(); 148 | } catch (err) { 149 | // pass error through to global error handler 150 | return next({ 151 | log: `tasksController.deleteBoard ERROR: ${err}`, 152 | status: 500, 153 | message: { err: "Error deleting board" }, 154 | }); 155 | } 156 | }, 157 | editBoard: async (req: Request, res: Response, next: NextFunction) => { 158 | try { 159 | const editedBoard = await Board.findByIdAndUpdate( 160 | req.body.id, 161 | { 162 | name: req.body.name, 163 | }, 164 | { new: true } 165 | ); 166 | res.locals.board = editedBoard; 167 | return next(); 168 | } catch (err) { 169 | // pass error through to global error handler 170 | return next({ 171 | log: `tasksController.editBoard ERROR: ${err}`, 172 | status: 500, 173 | message: { err: "Error editing Board" }, 174 | }); 175 | } 176 | }, 177 | pullBoard: async (req: Request, res: Response, next: NextFunction) => { 178 | try { 179 | await User.updateOne( 180 | { _id: res.locals.board.boardOwner }, 181 | { $pull: { boards: req.params.boardId } } 182 | ); 183 | return next(); 184 | } catch (err) { 185 | // pass error through to global error handler 186 | return next({ 187 | log: `tasksController.pullTask ERROR: ${err}`, 188 | status: 500, 189 | message: { err: "Error pulling Task" }, 190 | }); 191 | } 192 | }, 193 | }; 194 | 195 | export default boardsController; 196 | -------------------------------------------------------------------------------- /server/controllers/tasksController.ts: -------------------------------------------------------------------------------- 1 | import Board from "../models/boardModel.ts"; 2 | import Card from "../models/cardModel.ts"; 3 | import { NextFunction, Request, Response } from "express"; 4 | 5 | const tasksController = { 6 | createTask: async (req: Request, res: Response, next: NextFunction) => { 7 | try { 8 | const createdTask = await Card.create({ 9 | name: req.body.taskname, 10 | status: req.body.status, 11 | notes: req.body.tasknotes, 12 | }); 13 | res.locals.task = createdTask; 14 | 15 | return next(); 16 | } catch (err) { 17 | // pass error through to global error handler 18 | return next({ 19 | log: `tasksController.createTask ERROR: ${err}`, 20 | status: 500, 21 | message: { err: "Error creating Task" }, 22 | }); 23 | } 24 | }, 25 | assignTask: async (req: Request, res: Response, next: NextFunction) => { 26 | try { 27 | const column = req.body.status; 28 | let updateQuery; 29 | if (column === "backlog") { 30 | updateQuery = { $push: { backlog: res.locals.task._id } }; 31 | } else if (column === "inProgress") { 32 | updateQuery = { 33 | $push: { inProgress: res.locals.task._id }, 34 | }; 35 | } else if (column === "inReview") { 36 | updateQuery = { 37 | $push: { inReview: res.locals.task._id }, 38 | }; 39 | } else { 40 | updateQuery = { 41 | $push: { completed: res.locals.task._id }, 42 | }; 43 | } 44 | await Board.updateOne({ _id: req.body.boardId }, updateQuery); 45 | return next(); 46 | } catch (err) { 47 | // pass error through to global error handler 48 | return next({ 49 | log: `tasksController.assignTask ERROR: ${err}`, 50 | status: 500, 51 | message: { err: "Error assigning task into board" }, 52 | }); 53 | } 54 | }, 55 | getTasks: async (_req: Request, res: Response, next: NextFunction) => { 56 | try { 57 | // populate the tasks 58 | res.locals.board = await res.locals.board.populate("backlog"); 59 | res.locals.board = await res.locals.board.populate("inProgress"); 60 | res.locals.board = await res.locals.board.populate("inReview"); 61 | res.locals.board = await res.locals.board.populate("completed"); 62 | return next(); 63 | } catch (err) { 64 | // pass error through to global error handler 65 | return next({ 66 | log: `tasksController.getTasks ERROR: ${err}`, 67 | status: 500, 68 | message: { err: "Error getting Tasks" }, 69 | }); 70 | } 71 | }, 72 | editTask: async (req: Request, res: Response, next: NextFunction) => { 73 | try { 74 | const taskEdits = req.body; 75 | const editedTask = await Card.findByIdAndUpdate( 76 | taskEdits.taskId, 77 | { 78 | name: taskEdits.taskname, 79 | status: taskEdits.status, 80 | notes: taskEdits.tasknotes, 81 | }, 82 | { new: true } 83 | ); 84 | res.locals.task = editedTask; 85 | 86 | return next(); 87 | } catch (err) { 88 | // pass error through to global error handler 89 | return next({ 90 | log: `tasksController.editTask ERROR: ${err}`, 91 | status: 500, 92 | message: { err: "Error editing Task" }, 93 | }); 94 | } 95 | }, 96 | pullTask: async (req: Request, res: Response, next: NextFunction) => { 97 | try { 98 | const column = req.body.startColumn; 99 | let updateQuery; 100 | if (column === "backlog") { 101 | updateQuery = { $pull: { backlog: res.locals.task._id } }; 102 | } else if (column === "inProgress") { 103 | updateQuery = { 104 | $pull: { inProgress: res.locals.task._id }, 105 | }; 106 | } else if (column === "inReview") { 107 | updateQuery = { 108 | $pull: { inReview: res.locals.task._id }, 109 | }; 110 | } else { 111 | updateQuery = { 112 | $pull: { completed: res.locals.task._id }, 113 | }; 114 | } 115 | await Board.updateOne({ _id: req.body.boardId }, updateQuery); 116 | return next(); 117 | } catch (err) { 118 | // pass error through to global error handler 119 | return next({ 120 | log: `tasksController.pullTask ERROR: ${err}`, 121 | status: 500, 122 | message: { err: "Error pulling Task" }, 123 | }); 124 | } 125 | }, 126 | deleteTask: async (req: Request, res: Response, next: NextFunction) => { 127 | try { 128 | const deletedTask = await Card.findOneAndDelete({ 129 | _id: req.params.taskId, 130 | }); 131 | res.locals.task = deletedTask; 132 | return next(); 133 | } catch (err) { 134 | // pass error through to global error handler 135 | return next({ 136 | log: `tasksController.deleteTask ERROR: ${err}`, 137 | status: 500, 138 | message: { err: "Error deleting Task" }, 139 | }); 140 | } 141 | }, 142 | clearTask: async (_req: Request, res: Response, next: NextFunction) => { 143 | try { 144 | await Card.deleteMany({ 145 | _id: { 146 | $in: [ 147 | ...res.locals.board.backlog, 148 | ...res.locals.board.inProgress, 149 | ...res.locals.board.inReview, 150 | ...res.locals.board.completed, 151 | ], 152 | }, 153 | }); 154 | return next(); 155 | } catch (err) { 156 | // pass error through to global error handler 157 | return next({ 158 | log: `tasksController.clearTask ERROR: ${err}`, 159 | status: 500, 160 | message: { err: "Error clearing Tasks" }, 161 | }); 162 | } 163 | }, 164 | }; 165 | 166 | export default tasksController; 167 | -------------------------------------------------------------------------------- /server/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/userModel.ts'; 2 | import bcrypt from 'bcryptjs'; 3 | import { NextFunction, Request, Response } from 'express'; 4 | 5 | const userController = { 6 | // middleware for creating a new user on signup 7 | createUser: async (req: Request, res: Response, next: NextFunction) => { 8 | // obtain username, password and email from the request body 9 | const { username, email, password } = req.body; 10 | try { 11 | // check if any input is missing 12 | if (!username || !password || !email) { 13 | return next({ 14 | log: 'userController.createUser error, missing input', 15 | status: 400, 16 | message: { err: 'Missing an input!' }, 17 | }); 18 | } 19 | //generate salt and encrypt with bcrypt function 20 | const salt = await bcrypt.genSalt(10); 21 | const hashedPassword = await bcrypt.hash(password, salt); 22 | // create the user in the DB 23 | const user = await User.create({ 24 | username: username, 25 | email: email, 26 | password: hashedPassword, 27 | }); 28 | // store the username on res.locals to send back to frontend 29 | res.locals.user = { id: user._id, name: user.username }; 30 | return next(); 31 | } catch (err) { 32 | // send any errors to global error handler 33 | // send any errors to global error handler 34 | return next({ 35 | log: `userController.createUser ERROR: ${err}`, 36 | status: 500, 37 | message: { err: 'Error occured creating user' }, 38 | }); 39 | } 40 | }, 41 | 42 | // middleware for verifying a user on login 43 | verifyUser: async (req: Request, res: Response, next: NextFunction) => { 44 | // obtain user name password from request body 45 | const { username, password } = req.body; 46 | 47 | // check for a missing input 48 | if (!username || !password) { 49 | return next({ 50 | log: 'Missing username or password in verifyUser', 51 | status: 400, 52 | message: { err: 'Username and Password required' }, 53 | }); 54 | } 55 | try { 56 | // search DB for the user based on the username 57 | const user = await User.findOne({ username }); 58 | // if now user is found error out 59 | if (!user) { 60 | return next({ 61 | log: `userController.verifyUser ERROR: no user with input username found in DB`, 62 | status: 400, 63 | message: { err: 'Invalid username or password' }, 64 | }); 65 | } else { 66 | // else a user is found, check passwords 67 | const resultPassword = await bcrypt.compare(password, user.password); 68 | // if passwords do not match error out 69 | if (!resultPassword) { 70 | return next({ 71 | log: `userController.verifyUser ERROR: input password does not match stored password`, 72 | status: 400, 73 | message: { err: 'Invalid username or password' }, 74 | }); 75 | } 76 | // passwords do match, store username in res.locals to send back to frontend 77 | res.locals.user = { id: user._id, name: user.username }; 78 | return next(); 79 | } 80 | } catch (err) { 81 | // send any errors to global error handler 82 | return next({ 83 | log: `usersController.createUser ERROR: ${err}`, 84 | status: 500, 85 | message: { err: 'Error occured creating user' }, 86 | }); 87 | } 88 | }, 89 | }; 90 | 91 | export default userController; 92 | -------------------------------------------------------------------------------- /server/models/boardModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { InferSchemaType } from 'mongoose'; 3 | 4 | const Schema = mongoose.Schema; 5 | 6 | const boardSchema = new Schema({ 7 | name: { type: String, required: true }, 8 | backlog: [{ type: Schema.Types.ObjectId, ref: 'Card' }], 9 | inProgress: [{ type: Schema.Types.ObjectId, ref: 'Card' }], 10 | inReview: [{ type: Schema.Types.ObjectId, ref: 'Card' }], 11 | completed: [{ type: Schema.Types.ObjectId, ref: 'Card' }], 12 | boardOwner: { type: Schema.Types.ObjectId, ref: 'User' }, 13 | }); 14 | 15 | type Board = InferSchemaType; 16 | 17 | export default mongoose.model('Board', boardSchema); 18 | -------------------------------------------------------------------------------- /server/models/cardModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { InferSchemaType } from "mongoose"; 3 | 4 | const Schema = mongoose.Schema; 5 | 6 | const cardSchema = new Schema({ 7 | name: { type: String, required: true }, 8 | status: { type: String, required: true }, 9 | notes: { type: String }, 10 | // tags: [{ type: String }], 11 | }); 12 | 13 | type Card = InferSchemaType; 14 | 15 | export default mongoose.model("Card", cardSchema); 16 | -------------------------------------------------------------------------------- /server/models/userModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { InferSchemaType } from 'mongoose'; 3 | 4 | const Schema = mongoose.Schema; 5 | 6 | const userSchema = new Schema({ 7 | username: { type: String, required: true, unique: true }, 8 | email: { type: String, required: true, unique: true }, 9 | password: { type: String, required: true }, 10 | boards: [{ type: Schema.Types.ObjectId, ref: 'Board' }], 11 | }); 12 | 13 | type User = InferSchemaType; 14 | 15 | export default mongoose.model('User', userSchema); 16 | -------------------------------------------------------------------------------- /server/routes/boardsRouter.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | // import controllers 6 | import boardsController from "../controllers/boardsController.ts"; 7 | import tasksController from "../controllers/tasksController.ts"; 8 | 9 | // define routes 10 | 11 | // route for getting board names 12 | router.get( 13 | "/myboards/:userId", 14 | boardsController.getMyBoards, 15 | boardsController.getBoardNamesAndIds, 16 | (_req: Request, res: Response) => { 17 | return res.status(200).json(res.locals.namesAndIds); 18 | } 19 | ); 20 | 21 | // route for getting all tasks associated with a board 22 | router.get( 23 | "/board", 24 | boardsController.getCurrentBoard, 25 | tasksController.getTasks, 26 | (_req: Request, res: Response) => { 27 | return res.status(200).json(res.locals.board); 28 | } 29 | ); 30 | 31 | // route for creating a new baord 32 | router.post( 33 | "/create", 34 | boardsController.createBoard, 35 | boardsController.assignNewBoard, 36 | (_req: Request, res: Response) => { 37 | return res.status(200).json(res.locals.createdBoard); //may need the board just created data to render into the main container 38 | } 39 | ); 40 | 41 | // route for deleting a boardd 42 | router.delete( 43 | "/delete/:boardId", 44 | boardsController.getBoardFromId, 45 | tasksController.clearTask, 46 | boardsController.deleteBoard, 47 | boardsController.pullBoard, 48 | (_req: Request, res: Response) => { 49 | return res.status(200).json(); 50 | } 51 | ); 52 | 53 | // route for editing a board 54 | router.put( 55 | "/edit", 56 | boardsController.editBoard, 57 | (_req: Request, res: Response) => { 58 | return res.status(200).json(res.locals.board); 59 | } 60 | ); 61 | 62 | export default router; 63 | -------------------------------------------------------------------------------- /server/routes/tasksRouter.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | // import controllers 6 | import tasksController from "../controllers/tasksController.ts"; 7 | 8 | // define routes 9 | 10 | // route for adding a new task card 11 | router.post( 12 | "/create", 13 | tasksController.createTask, 14 | tasksController.assignTask, 15 | (_req: Request, res: Response) => { 16 | return res.status(200).json(res.locals.task); 17 | } 18 | ); 19 | 20 | // route for editing a task card 21 | router.post( 22 | "/edit", 23 | tasksController.editTask, 24 | tasksController.pullTask, 25 | tasksController.assignTask, 26 | (_req: Request, res: Response) => { 27 | return res.status(200).json(res.locals.task); 28 | } 29 | ); 30 | 31 | // route for deleting a task card 32 | router.delete( 33 | "/delete/:taskId", 34 | tasksController.deleteTask, 35 | tasksController.pullTask, 36 | (_req: Request, res: Response) => { 37 | return res.status(200).json(); 38 | } 39 | ); 40 | 41 | export default router; 42 | -------------------------------------------------------------------------------- /server/routes/userRouter.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | // import controllers 6 | import userController from "../controllers/userController.ts"; 7 | 8 | // define routes 9 | 10 | // route for signup 11 | router.post( 12 | "/signup", 13 | userController.createUser, 14 | (_req: Request, res: Response) => { 15 | return res.status(200).json(res.locals.user); 16 | } 17 | ); 18 | 19 | // route for login 20 | router.post( 21 | "/login", 22 | userController.verifyUser, 23 | (_req: Request, res: Response) => { 24 | return res.status(200).json(res.locals.user); 25 | } 26 | ); 27 | 28 | export default router; 29 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import express, { NextFunction, Request, Response } from "express"; 3 | import { join, dirname } from "path"; 4 | import { fileURLToPath } from "url"; 5 | import mongoose from "mongoose"; 6 | import userRouter from "./routes/userRouter.ts"; 7 | import boardsRouter from "./routes/boardsRouter.ts"; 8 | import tasksRouter from "./routes/tasksRouter.ts"; 9 | 10 | // start the DB 11 | const mongoUri = process.env.MONGO_URI; 12 | if (!mongoUri) throw new Error("MONGO_URI environment variable undefinded"); 13 | mongoose 14 | .connect(mongoUri) 15 | .then(() => console.log("MongoDB Connected")) 16 | .catch((err) => console.log("Error connecting to DB: ", err)); 17 | 18 | // ES Modules work around 19 | const __filename = fileURLToPath(import.meta.url); 20 | const __dirname = dirname(__filename); 21 | 22 | const PORT = process.env.PORT; 23 | const app = express(); 24 | 25 | app.use(express.json()); 26 | app.use(express.urlencoded({ extended: true })); 27 | 28 | // serve static files 29 | app.use(express.static(join(__dirname, "../dist"))); 30 | 31 | // route handlers 32 | app.use("/user", userRouter); 33 | app.use("/boards", boardsRouter); 34 | app.use("/tasks", tasksRouter); 35 | 36 | // serve the built index.html 37 | app.use("/", (_req: Request, res: Response) => { 38 | return res.sendFile(join(__dirname, "../dist/index.html")); 39 | }); 40 | 41 | // unknown route handling 42 | app.use("*", (_req: Request, res: Response) => { 43 | return res.status(404).send("Page not found"); 44 | }); 45 | 46 | // global error handling 47 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 48 | app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => { 49 | const defaultErr = { 50 | //make sure to make type for default for global error handling 51 | log: "Express error handler caught unknown middleware error", 52 | status: 500, 53 | message: { err: "An error occurred" }, 54 | }; 55 | const errorObj = Object.assign({}, defaultErr, err); 56 | console.log(errorObj.log); 57 | return res.status(errorObj.status).json(errorObj.message); 58 | }); 59 | 60 | app.listen(PORT, () => console.log(`Listening on port: ${PORT}`)); 61 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from 'react-router'; 2 | import Authentication from './routes/Authentication.tsx'; 3 | import Dashboard from './routes/Dashboard.tsx'; 4 | import { useState } from 'react'; 5 | import { UserState } from './types.ts'; 6 | import './scss/app.scss'; 7 | 8 | function App() { 9 | // track the username in state 10 | const [user, setUser] = useState({ 11 | name: '', 12 | id: '', 13 | }); 14 | return ( 15 | 16 | } /> 17 | } /> 18 | 19 | ); 20 | } 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /src/assets/Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OS-Builders/Task-Pro/059efc3a337a4cff35f8a5d420bc0ea47ff180ff/src/assets/Dashboard.png -------------------------------------------------------------------------------- /src/assets/authPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OS-Builders/Task-Pro/059efc3a337a4cff35f8a5d420bc0ea47ff180ff/src/assets/authPage.png -------------------------------------------------------------------------------- /src/assets/edit-cover-1481-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | edit_cover [#1481] 6 | Created with Sketch. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/note-text-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | note-text 6 | Created with Sketch Beta. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/projectFrame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OS-Builders/Task-Pro/059efc3a337a4cff35f8a5d420bc0ea47ff180ff/src/assets/projectFrame.png -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/taskCreation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OS-Builders/Task-Pro/059efc3a337a4cff35f8a5d420bc0ea47ff180ff/src/assets/taskCreation.png -------------------------------------------------------------------------------- /src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { CardProps } from "../types"; 3 | import notesSvg from "../assets/note-text-svgrepo-com.svg"; 4 | import editSvg from "../assets/edit-cover-1481-svgrepo-com.svg"; 5 | import "../scss/taskCard.scss"; 6 | 7 | const Card = ({ info, setEditingTask }: CardProps) => { 8 | const [showNotes, setShowNotes] = useState(false); 9 | return ( 10 |
11 |

{info.name}

12 | {showNotes ?

{info.notes}

: null} 13 |
14 | 23 | 31 |
32 |
33 | ); 34 | }; 35 | 36 | export default Card; 37 | -------------------------------------------------------------------------------- /src/components/Column.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, ReactNode } from "react"; 2 | import { ColumnProps, TaskState } from "../types"; 3 | import NewTaskModal from "./NewTaskModal"; 4 | import Card from "./Card.tsx"; 5 | import EditTaskModal from "./EditTaskModal.tsx"; 6 | 7 | const Column = ({ 8 | name, 9 | create, 10 | currentBoard, 11 | boardState, 12 | setBoardState, 13 | }: ColumnProps) => { 14 | const [addingTask, setAddingTask] = useState(false); 15 | const [numTasks, setNumTasks] = useState(0); 16 | const [taskCards, setTaskCards] = useState([]); 17 | const [editingTask, setEditingTask] = useState(null); 18 | 19 | const handleNewTask = () => { 20 | setAddingTask(true); 21 | }; 22 | 23 | //effect for rendering cards 24 | useEffect(() => { 25 | const column = boardState[name]; 26 | setNumTasks(column.length); 27 | const cardsArray = column.map((task: TaskState) => { 28 | return ( 29 | 30 | ); 31 | }); 32 | setTaskCards(cardsArray); 33 | }, [boardState, name]); 34 | 35 | return ( 36 |
37 |

38 | {(name === "backlog" 39 | ? "Backlog" 40 | : name === "inProgress" 41 | ? "In Progress" 42 | : name === "inReview" 43 | ? "In Review" 44 | : "Completed") + ` (${numTasks})`} 45 |

46 |
47 | {create && ( 48 | 51 | )} 52 | {taskCards} 53 |
54 | {addingTask ? ( 55 | 60 | ) : null} 61 | {editingTask ? ( 62 | 69 | ) : null} 70 |
71 | ); 72 | }; 73 | export default Column; 74 | -------------------------------------------------------------------------------- /src/components/CreateBoardModal.tsx: -------------------------------------------------------------------------------- 1 | import { createPortal } from "react-dom"; 2 | import { CreateBoardModalProps } from "../types"; 3 | import { useState } from "react"; 4 | import "../scss/modal.scss"; 5 | 6 | const CreateBoardModal = ({ 7 | setCreatingBoard, 8 | setCurrentBoard, 9 | user, 10 | boardList, 11 | setBoardList, 12 | handleBoardSelect, 13 | selectedBoard, 14 | setSelectedBoard, 15 | }: CreateBoardModalProps) => { 16 | const [boardName, setBoardName] = useState(""); 17 | 18 | const handleInputChange = (e: React.ChangeEvent) => { 19 | const inputValue: string = e.target.value; 20 | setBoardName(inputValue.trim()); //edge case for whitespace 21 | }; 22 | 23 | const handleFormSubmit = async (e: React.FormEvent) => { 24 | e.preventDefault(); 25 | // send post request to /boards/create with formData in body 26 | const body = { 27 | boardName: boardName, 28 | userId: user.id, 29 | }; 30 | const response: Response = await fetch("/boards/create", { 31 | method: "POST", 32 | headers: { 33 | "Content-type": "application/json; charset=UTF-8", 34 | }, 35 | body: JSON.stringify(body), 36 | }); 37 | // receive board name and id from backend 38 | if (response.status === 200) { 39 | const responseData = await response.json(); 40 | const newBoardListItem = ( 41 | 52 | ); 53 | setBoardList([...boardList, newBoardListItem]); 54 | setCurrentBoard({ name: responseData.name, id: responseData._id }); 55 | setSelectedBoard(responseData._id); 56 | setCreatingBoard(false); 57 | } else { 58 | console.log("Failed To create board."); 59 | } 60 | }; 61 | 62 | const isButtonDisabled: boolean = boardName === ""; 63 | 64 | return createPortal( 65 |
66 |
67 |
68 |

New Board

69 | 77 |
78 | 85 | 94 |
95 |
96 |
97 |
, 98 | document.getElementById("portal") as Element 99 | ); 100 | }; 101 | 102 | export default CreateBoardModal; 103 | -------------------------------------------------------------------------------- /src/components/EditBoardModal.tsx: -------------------------------------------------------------------------------- 1 | import { createPortal } from "react-dom"; 2 | import { EditBoardModalProps } from "../types"; 3 | import { useState } from "react"; 4 | import "../scss/modal.scss"; 5 | 6 | const EditBoardModal = ({ 7 | setEditingBoard, 8 | setCurrentBoard, 9 | currentBoard, 10 | }: EditBoardModalProps) => { 11 | const [boardName, setBoardName] = useState(currentBoard.name); 12 | 13 | const handleInputChange = (e: React.ChangeEvent) => { 14 | const inputValue: string = e.target.value; 15 | setBoardName(inputValue); //edge case for whitespace 16 | }; 17 | 18 | const handleFormSubmit = async (e: React.FormEvent) => { 19 | e.preventDefault(); 20 | // send put request to /boards/edit with new boardName and id in body 21 | const body = { 22 | name: boardName, 23 | id: currentBoard.id, 24 | }; 25 | const response: Response = await fetch("/boards/edit", { 26 | method: "PUT", 27 | headers: { 28 | "Content-type": "application/json; charset=UTF-8", 29 | }, 30 | body: JSON.stringify(body), 31 | }); 32 | // receive board name and id from backend 33 | if (response.status === 200) { 34 | const responseData = await response.json(); 35 | setCurrentBoard({ name: responseData.name, id: responseData._id }); 36 | setEditingBoard(false); 37 | } else { 38 | console.log("Failed to edit board."); 39 | } 40 | }; 41 | 42 | const handleDeleteBoard = () => { 43 | const fetchDeleteBoard = async () => { 44 | const response: Response = await fetch( 45 | `/boards/delete/${currentBoard.id}`, 46 | { 47 | method: "DELETE", 48 | } 49 | ); 50 | if (response.status === 200) { 51 | setCurrentBoard({ 52 | name: "", 53 | id: "", 54 | }); 55 | setEditingBoard(false); 56 | } 57 | }; 58 | fetchDeleteBoard().catch(console.error); 59 | }; 60 | 61 | const isButtonDisabled: boolean = boardName === ""; 62 | 63 | return createPortal( 64 |
65 |
66 |
67 |

Edit Board

68 | 76 |
77 | 84 | 93 | 100 |
101 |
102 |
103 |
, 104 | document.getElementById("portal") as Element 105 | ); 106 | }; 107 | 108 | export default EditBoardModal; 109 | -------------------------------------------------------------------------------- /src/components/EditTaskModal.tsx: -------------------------------------------------------------------------------- 1 | import { createPortal } from "react-dom"; 2 | import { 3 | BoardState, 4 | EditTaskModalProps, 5 | TaskFormState, 6 | TaskState, 7 | } from "../types"; 8 | import { useState } from "react"; 9 | import "../scss/modal.scss"; 10 | 11 | const EditTaskModal = ({ 12 | setEditingTask, 13 | currentBoard, 14 | setBoardState, 15 | task, 16 | startColumn, 17 | }: EditTaskModalProps) => { 18 | const [formData, setFormData] = useState({ 19 | taskname: task.name, 20 | status: task.status, 21 | tasknotes: task.notes, 22 | }); 23 | 24 | const handleFormSubmit = (e: React.FormEvent) => { 25 | e.preventDefault; 26 | console.log("Edit Task Form Submitted: ", formData); 27 | // send POST request with the edited task, originla column and current board 28 | const fetchEditTask = async () => { 29 | const body = { 30 | ...formData, 31 | taskId: task._id, 32 | boardId: currentBoard.id, 33 | startColumn: startColumn, 34 | }; 35 | const response: Response = await fetch(`/tasks/edit`, { 36 | method: "POST", 37 | headers: { 38 | "Content-type": "application/json; charset=UTF-8", 39 | }, 40 | body: JSON.stringify(body), 41 | }); 42 | const editedTask: TaskState = await response.json(); 43 | if (response.status === 200) { 44 | // update the board state, removing from array if necessary 45 | setBoardState((prevState: BoardState) => { 46 | const column = [...prevState[startColumn]]; 47 | const idx = column.indexOf(task); 48 | // if changing columns, remove from startColumn and add to new column 49 | if (task.status !== editedTask.status) { 50 | column.splice(idx, 1); 51 | return { 52 | ...prevState, 53 | [editedTask.status]: [ 54 | ...prevState[editedTask.status], 55 | editedTask, 56 | ], 57 | [startColumn]: column, 58 | }; 59 | } 60 | // else update the existing column with new task name and notes 61 | else { 62 | column[idx] = editedTask; 63 | return { 64 | ...prevState, 65 | [startColumn]: column, 66 | }; 67 | } 68 | }); 69 | } 70 | }; 71 | fetchEditTask().catch(console.error); 72 | setEditingTask(null); 73 | }; 74 | 75 | const handleInputChange = ( 76 | e: 77 | | React.ChangeEvent 78 | | React.ChangeEvent 79 | ) => { 80 | const { name, value } = e.target; 81 | setFormData((prevData: TaskFormState) => ({ ...prevData, [name]: value })); 82 | }; 83 | 84 | const handleDeleteTask = () => { 85 | const fetchDeleteTask = async () => { 86 | const response: Response = await fetch(`/tasks/delete/${task._id}`, { 87 | method: "DELETE", 88 | }); 89 | if (response.status === 200) { 90 | setBoardState((prevState: BoardState) => { 91 | const column = [...prevState[startColumn]]; 92 | const idx = column.indexOf(task); 93 | column.splice(idx, 1); 94 | return { 95 | ...prevState, 96 | [startColumn]: column, 97 | }; 98 | }); 99 | } 100 | }; 101 | fetchDeleteTask().catch(console.error); 102 | setEditingTask(null); 103 | }; 104 | 105 | return createPortal( 106 |
107 |
108 |
109 |

Edit Task

110 | 113 | 121 | 124 |
125 |
126 | 134 | 137 |
138 | 139 |
140 | 148 | 151 |
152 | 153 |
154 | 162 | 165 |
166 | 167 |
168 | 176 | 179 |
180 |
181 | 184 |