├── .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 |         
12 |
13 | ## Project Frames
14 |
15 | 
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 | 
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 | 
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 | 
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 |
{
18 | setEditingTask(info);
19 | }}
20 | >
21 |
22 |
23 |
{
26 | setShowNotes(!showNotes);
27 | }}
28 | >
29 |
30 |
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 |
49 | Add New Task +
50 |
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 |
50 | {responseData.name}
51 |
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 | ,
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 | ,
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 | ,
215 | document.getElementById("portal") as Element
216 | );
217 | };
218 |
219 | export default EditTaskModal;
220 |
--------------------------------------------------------------------------------
/src/components/Login.tsx:
--------------------------------------------------------------------------------
1 | import { SignupProps, AuthFormState } from '../types';
2 | import { useState } from 'react';
3 | import { useNavigate } from 'react-router';
4 |
5 | const Login = ({ setUser, setLoggingIn }: SignupProps) => {
6 | const navigate = useNavigate();
7 | const [formData, setFormData] = useState({
8 | username: '',
9 | password: '',
10 | });
11 | // const [guestLogIn, setGuestLogIn] = useState(false);
12 | const [loginFail, setLoginFail] = useState(false);
13 |
14 | const handleInputChange = (e: React.ChangeEvent) => {
15 | const { name, value } = e.target;
16 | setFormData((prevData) => ({ ...prevData, [name]: value }));
17 | };
18 |
19 | const handleGuestLogin = () => {
20 | const guestCredentials = {
21 | username: 'guest',
22 | password: 'guest',
23 | };
24 | setFormData(guestCredentials);
25 | };
26 |
27 | const handleFormSubmit = async (e: React.FormEvent) => {
28 | e.preventDefault();
29 | // send post request to /user/login with formData in body
30 | const body: string = JSON.stringify(formData);
31 | const response: Response = await fetch('/user/login', {
32 | method: 'POST',
33 | headers: {
34 | 'Content-type': 'application/json; charset=UTF-8',
35 | },
36 | body: body,
37 | });
38 | // receive username from backend
39 | const user = await response.json();
40 | // if request success, save username to state and route to dashboard
41 | if (response.status === 200) {
42 | setUser(user);
43 | setLoginFail(false);
44 | return navigate('/dashboard');
45 | } else {
46 | setLoginFail(true);
47 | }
48 | };
49 |
50 | return (
51 |
95 | );
96 | };
97 | export default Login;
98 |
--------------------------------------------------------------------------------
/src/components/NewTaskModal.tsx:
--------------------------------------------------------------------------------
1 | import { createPortal } from "react-dom";
2 | import {
3 | BoardState,
4 | NewTaskModalProps,
5 | TaskFormState,
6 | TaskState,
7 | } from "../types";
8 | import { useState } from "react";
9 | import "../scss/modal.scss";
10 |
11 | const NewTaskModal = ({
12 | setAddingTask,
13 | currentBoard,
14 | setBoardState,
15 | }: NewTaskModalProps) => {
16 | const [formData, setFormData] = useState({
17 | taskname: "",
18 | status: "backlog",
19 | tasknotes: "",
20 | });
21 |
22 | const handleFormSubmit = (e: React.FormEvent) => {
23 | e.preventDefault;
24 | console.log("New Task Form Submitted: ", formData);
25 | // send POST request with the new task card, user and current board
26 | const fetchAddTask = async () => {
27 | if (currentBoard && currentBoard.name !== "") {
28 | const body = {
29 | ...formData,
30 | boardId: currentBoard.id,
31 | };
32 | const response: Response = await fetch(`/tasks/create`, {
33 | method: "POST",
34 | headers: {
35 | "Content-type": "application/json; charset=UTF-8",
36 | },
37 | body: JSON.stringify(body),
38 | });
39 | const task: TaskState = await response.json();
40 | if (response.status === 200) {
41 | console.log("newly created task: ", task);
42 | setBoardState((prevState: BoardState) => ({
43 | ...prevState,
44 | [task.status]: [...prevState[task.status], task],
45 | }));
46 | }
47 | }
48 | };
49 | fetchAddTask().catch(console.error);
50 | setAddingTask(false);
51 | };
52 |
53 | const handleInputChange = (
54 | e:
55 | | React.ChangeEvent
56 | | React.ChangeEvent
57 | ) => {
58 | const { name, value } = e.target;
59 | setFormData((prevData: TaskFormState) => ({ ...prevData, [name]: value }));
60 | };
61 |
62 | return createPortal(
63 |
64 |
65 |
66 | New Task
67 |
68 | Task Name:{" "}
69 |
70 |
78 |
79 | Task Status:{" "}
80 |
81 |
132 |
133 | Task Notes:{" "}
134 |
135 |
142 |
143 |
144 | Add Task
145 |
146 | {
150 | setAddingTask(false);
151 | }}
152 | >
153 | Cancel
154 |
155 |
156 |
157 |
158 |
,
159 | document.getElementById("portal") as Element
160 | );
161 | };
162 |
163 | export default NewTaskModal;
164 |
--------------------------------------------------------------------------------
/src/components/Settings.tsx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/components/Signup.tsx:
--------------------------------------------------------------------------------
1 | import { SignupProps, AuthFormState } from "../types";
2 | import { useState, useEffect } from "react";
3 | import { useNavigate } from "react-router";
4 |
5 | const Signup = ({ setUser, setLoggingIn }: SignupProps) => {
6 | const navigate = useNavigate();
7 | // save the signup info into state
8 | const [formData, setFormData] = useState({
9 | username: "",
10 | email: "",
11 | password: "",
12 | confirmPassword: "",
13 | });
14 | const [passwordsMatch, setPasswordsMatch] = useState(false);
15 |
16 | // update form data as user input changes
17 | const handleInputChange = (e: React.ChangeEvent) => {
18 | const { name, value } = e.target;
19 | setFormData((prevData: AuthFormState) => ({ ...prevData, [name]: value }));
20 | };
21 |
22 | // sign user in and navigate to dashboard
23 | const handleFormSubmit = async (e: React.FormEvent) => {
24 | e.preventDefault();
25 | // send post request to /user/signup with formData in body
26 | const body: string = JSON.stringify(formData);
27 | const response: Response = await fetch("/user/signup", {
28 | method: "POST",
29 | headers: {
30 | "Content-type": "application/json; charset=UTF-8",
31 | },
32 | body: body,
33 | });
34 | // receive username from backend
35 | const user = await response.json();
36 | // if request success, save username to state and route to dashboard
37 | if (response.status === 200) {
38 | setUser(user);
39 | return navigate("/dashboard");
40 | }
41 | };
42 |
43 | useEffect(() => {
44 | if (formData.password === formData.confirmPassword) setPasswordsMatch(true);
45 | else setPasswordsMatch(false);
46 | }, [formData]);
47 |
48 | return (
49 |
124 | );
125 | };
126 | export default Signup;
127 |
--------------------------------------------------------------------------------
/src/containers/ColumnContainer.tsx:
--------------------------------------------------------------------------------
1 | import Column from "../components/Column";
2 | import "../scss/columnContainer.scss";
3 | import { ColumnContainerProps, BoardState } from "../types";
4 |
5 | const ColumnContainer = ({
6 | user,
7 | currentBoard,
8 | boardState,
9 | setBoardState,
10 | }: ColumnContainerProps) => {
11 | const columns = [];
12 | const columnNames: Array = [
13 | "backlog",
14 | "inProgress",
15 | "inReview",
16 | "completed",
17 | ];
18 | for (let i = 0; i < 4; i++) {
19 | columns.push(
20 |
29 | );
30 | }
31 | return {columns}
;
32 | };
33 |
34 | export default ColumnContainer;
35 |
--------------------------------------------------------------------------------
/src/containers/LeftContainer.tsx:
--------------------------------------------------------------------------------
1 | import { BoardListItemState, LeftContainerProps } from '../types';
2 | import { ReactNode, useEffect, useState } from 'react';
3 | import CreateBoardModal from '../components/CreateBoardModal';
4 | import '../scss/leftContainer.scss';
5 | import { useNavigate } from 'react-router';
6 |
7 | const LeftContainer = ({
8 | user,
9 | setCurrentBoard,
10 | currentBoard,
11 | }: LeftContainerProps) => {
12 | const [creatingBoard, setCreatingBoard] = useState(false);
13 | const [boardList, setBoardList] = useState([]);
14 | const [selectedBoard, setSelectedBoard] = useState(null);
15 | const navigate = useNavigate();
16 | // make a request for all board names, return an array containing strings of the board names
17 | useEffect(() => {
18 | const fetchBoardList = async () => {
19 | const reponse: Response = await fetch(`/boards/myboards/${user.id}`);
20 | const list = await reponse.json();
21 | const boardSelectors = list.map((board: BoardListItemState) => (
22 |
31 | {board.name}
32 |
33 | ));
34 | setBoardList(boardSelectors);
35 | };
36 | fetchBoardList().catch(console.error);
37 | }, [currentBoard]);
38 |
39 | const handleBoardSelect = (e: React.MouseEvent) => {
40 | e.preventDefault();
41 | setCurrentBoard({
42 | name: e.currentTarget.name,
43 | id: e.currentTarget.id,
44 | });
45 | setSelectedBoard(e.currentTarget.id);
46 | };
47 |
48 | const handleCreateBoard = () => {
49 | setCreatingBoard(true);
50 | };
51 |
52 | const handleLogOut = (e: React.MouseEvent) => {
53 | e.preventDefault();
54 | // localStorage.removeItem('authToken');
55 | navigate('/');
56 | };
57 |
58 | return (
59 |
60 |
Task Pro
61 |
{user.name}'s Boards
62 |
63 | Create New Board +
64 |
65 |
68 | {creatingBoard ? (
69 |
79 | ) : null}
80 |
81 |
82 | Log Out
83 |
84 |
85 |
86 | );
87 | };
88 |
89 | export default LeftContainer;
90 |
--------------------------------------------------------------------------------
/src/containers/MainContainer.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import '../scss/mainContainer.scss';
3 | import { BoardState, MainContainerProps } from '../types';
4 | import ColumnContainer from './ColumnContainer.tsx';
5 | import editSvg from '../assets/edit-cover-1481-svgrepo-com.svg';
6 | import EditBoardModal from '../components/EditBoardModal.tsx';
7 |
8 | const MainContainer = ({
9 | user,
10 | currentBoard,
11 | setCurrentBoard,
12 | }: MainContainerProps) => {
13 | // set the board state as empty arrays, will be populated with card ids after fetch
14 | const [boardState, setBoardState] = useState({
15 | backlog: [],
16 | inProgress: [],
17 | inReview: [],
18 | completed: [],
19 | });
20 | const [editingBoard, setEditingBoard] = useState(false);
21 |
22 | // effect for fetching the current board info whenever currentBoard changes
23 | useEffect(() => {
24 | const fetchBoard = async () => {
25 | // fetch the currentBoard if a board has been selected
26 | if (currentBoard && currentBoard.name !== '') {
27 | const reponse: Response = await fetch(
28 | `/boards/board?board=${currentBoard.id}&user=${user.id}`
29 | );
30 | const board = await reponse.json();
31 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
32 | const { boardOwner, name, _id, __v, ...columns } = board;
33 | await setBoardState({ ...columns });
34 | }
35 | };
36 | fetchBoard().catch(console.error);
37 | }, [currentBoard]);
38 |
39 | if (!currentBoard?.name) {
40 | return (
41 |
42 |
43 | Select existing board or create a new one to get started!
44 |
45 |
46 | );
47 | }
48 |
49 | return (
50 |
51 |
52 |
{currentBoard.name}
53 |
setEditingBoard(true)}
56 | >
57 |
58 |
59 |
60 |
66 | {editingBoard ? (
67 |
72 | ) : null}
73 |
74 | );
75 | };
76 |
77 | export default MainContainer;
78 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import App from './App.tsx';
5 | import './scss/app.scss';
6 |
7 | ReactDOM.createRoot(document.getElementById('root')!).render(
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/routes/Authentication.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import Login from "../components/Login.tsx";
3 | import Signup from "../components/Signup.tsx";
4 | import { AuthProps } from "../types.ts";
5 | import "../scss/authContainer.scss";
6 | import "../scss/authWrapper.scss";
7 |
8 | const Authentication = ({ setUser }: AuthProps) => {
9 | // use state to swap between logging in and signing in
10 | const [loggingIn, setLoggingIn] = useState(true);
11 | return (
12 |
13 |
Task Pro
14 | {loggingIn ? (
15 |
16 | ) : (
17 |
18 | )}
19 |
20 | );
21 | };
22 | export default Authentication;
23 |
--------------------------------------------------------------------------------
/src/routes/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import LeftContainer from "../containers/LeftContainer";
2 | import MainContainer from "../containers/MainContainer";
3 | import { DashboardProps, CurrentBoardState } from "../types";
4 | import { useState } from "react";
5 | import "../scss/dashBoard.scss";
6 |
7 | const Dashboard = ({ user }: DashboardProps) => {
8 | const [currentBoard, setCurrentBoard] = useState({
9 | name: "",
10 | id: "",
11 | });
12 | return (
13 |
14 |
19 |
24 |
25 | );
26 | };
27 |
28 | export default Dashboard;
29 |
--------------------------------------------------------------------------------
/src/scss/app.scss:
--------------------------------------------------------------------------------
1 | html {
2 | height: 100vh;
3 | }
4 |
5 | body {
6 | background-color: rgb(32, 32, 31);
7 | margin: 0;
8 | height: 100%;
9 | }
10 |
11 | #root {
12 | height: 100%;
13 | }
14 |
15 | .authContainer {
16 | display: flex;
17 | flex-direction: column;
18 | justify-content: center;
19 | text-align: center;
20 | position: relative;
21 | }
22 |
--------------------------------------------------------------------------------
/src/scss/authContainer.scss:
--------------------------------------------------------------------------------
1 | .title {
2 | font-size: 32px;
3 | color: goldenrod;
4 | font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande',
5 | 'Lucida Sans', Arial, sans-serif;
6 | }
7 |
8 | .title-wrapper {
9 | font-family: ui-monospace;
10 | color: white;
11 | background-color: #484848;
12 | display: flex;
13 | flex-direction: column;
14 | align-items: center;
15 | justify-content: center;
16 | width: 300px; /* Adjust width as needed */
17 | position: absolute;
18 | top: 40%;
19 | left: 50%;
20 | transform: translate(-50%, -50%);
21 | padding: 20px;
22 | border: 2px solid black;
23 | border-radius: 8px;
24 | box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.1);
25 | }
26 |
--------------------------------------------------------------------------------
/src/scss/authWrapper.scss:
--------------------------------------------------------------------------------
1 | .auth-form {
2 | width: 300px;
3 | padding: 20px;
4 | font-weight: bold;
5 | font-size: large;
6 | font-family: monospace;
7 | display: flex;
8 | flex-direction: column; /* Align items vertically */
9 | }
10 |
11 | .auth-input {
12 | margin-bottom: 10px;
13 | padding: 8px;
14 | width: 100%;
15 | box-sizing: border-box;
16 | border: 2px solid black;
17 | border-radius: 5px;
18 | }
19 | .auth-label {
20 | margin-bottom: 10px; /* Add margin between labels and inputs */
21 | }
22 | .auth-confirm {
23 | color: red;
24 | margin-top: 5px;
25 | }
26 |
27 | .auth-submit {
28 | align-self: center;
29 | background-color: #61db65;
30 | color: black;
31 | padding: 10px;
32 | border: 1px solid black;
33 | border-radius: 4px;
34 | width: 100px;
35 | cursor: pointer;
36 | transition: 0.3s ease-in-out;
37 | &:hover {
38 | color: white;
39 | background-color: #3b8a3e;
40 | }
41 | }
42 |
43 | .auth-switch {
44 | align-self: center;
45 | margin-top: 10px;
46 | background-color: #2ea1ff;
47 | color: black;
48 | border: 1px solid black;
49 | padding: 10px;
50 | border-radius: 4px;
51 | cursor: pointer;
52 | transition: 0.3s ease-in-out;
53 | &:hover {
54 | color: white;
55 | background-color: rgb(24, 97, 157);
56 | }
57 | }
58 |
59 | .auth-guest {
60 | align-self: center;
61 | margin-top: 10px;
62 | background-color: rgb(248, 178, 13);
63 | color: black;
64 | border: 1px solid black;
65 | padding: 10px;
66 | border-radius: 4px;
67 | cursor: pointer;
68 | transition: 0.3s ease-in-out;
69 | &:hover {
70 | color: white;
71 | background-color: rgb(248, 178, 13);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/scss/columnContainer.scss:
--------------------------------------------------------------------------------
1 | .column-container {
2 | display: flex;
3 | justify-content: space-between;
4 | align-items: center;
5 | height: 100%;
6 | width: 100%;
7 |
8 | .column {
9 | width: 25%;
10 | height: 100%;
11 | background-color: rgb(70, 70, 70);
12 | margin: 0px 10px;
13 | border-radius: 15px;
14 | box-shadow: 0 60px 30px -60px rgba(0, 0, 0, 0.45) inset,
15 | -60px 0 30px -60px rgba(0, 0, 0, 0.45) inset;
16 |
17 | .column-title {
18 | font-size: 24px;
19 | font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
20 | "Lucida Sans", Arial, sans-serif;
21 | }
22 |
23 | .new-task-btn {
24 | width: 90%;
25 | padding: 10px;
26 | cursor: pointer;
27 | border-radius: 10px;
28 | border: none;
29 | font-size: 18px;
30 | font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
31 | "Lucida Sans", Arial, sans-serif;
32 | box-shadow: 6.1px 12.2px 12.2px hsl(0deg 0% 0% / 0.31);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/scss/dashBoard.scss:
--------------------------------------------------------------------------------
1 | .main-page {
2 | color: white;
3 | display: flex;
4 | height: 100%;
5 | width: 100%;
6 | }
7 |
--------------------------------------------------------------------------------
/src/scss/leftContainer.scss:
--------------------------------------------------------------------------------
1 | .left-container {
2 | width: 300px;
3 | height: 100%;
4 | background-color: rgb(22, 22, 22);
5 | margin: 0;
6 | display: flex;
7 | flex-direction: column;
8 | justify-content: space-between;
9 | text-align: center;
10 |
11 | .heading {
12 | font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande',
13 | 'Lucida Sans', Arial, sans-serif;
14 | }
15 | footer {
16 | margin-top: auto;
17 | height: 60px;
18 | }
19 | .settings-button {
20 | border: none;
21 | background-color: rgb(22, 22, 22);
22 | color: white;
23 | font-size: x-large;
24 | padding: 10px;
25 | height: 100%;
26 | width: 100%;
27 | letter-spacing: 5px;
28 | transition: all 0.3s;
29 |
30 | &:hover {
31 | background-color: rgb(54, 53, 53);
32 | letter-spacing: 15px;
33 | color: white(255, 255, 255, 0.7);
34 | }
35 | }
36 | }
37 | .boards-info {
38 | display: flex;
39 | flex-direction: column;
40 | justify-content: space-between;
41 | align-items: center;
42 | max-height: 90%;
43 | overflow-y: auto;
44 | margin-top: 10px;
45 | }
46 | .board-selector {
47 | display: flex;
48 | flex-direction: column;
49 | align-items: center;
50 | color: white;
51 | background-color: rgb(22, 22, 22);
52 | padding: 10px;
53 | border: 2px solid rgb(22, 22, 22);
54 | border-radius: 4px;
55 | font-weight: bold;
56 | font-size: large;
57 | width: 200px;
58 | margin-top: 10px;
59 | margin-bottom: 10px;
60 | transition: all 0.2s ease-in-out;
61 |
62 | &:hover {
63 | border: 2px solid #c3ff0e;
64 | }
65 | }
66 | .selected {
67 | border: 2px solid #c3ff0e;
68 | transform: scale(1.07);
69 | text-transform: uppercase;
70 | box-shadow: 0 0 10px rgba(195, 255, 14, 0.5);
71 | }
72 |
73 | //styling for Create board Button
74 | .new-board-btn {
75 | align-self: center;
76 | font-size: large;
77 | width: 200px;
78 | height: 50px;
79 | border: none;
80 | outline: none;
81 | color: #fff;
82 | background: rgb(15, 15, 15);
83 | cursor: pointer;
84 | position: relative;
85 | z-index: 0;
86 | border-radius: 5px;
87 |
88 | &:before {
89 | content: '';
90 | background: linear-gradient(
91 | 45deg,
92 | #ff0000,
93 | #ff7300,
94 | #fffb00,
95 | #48ff00,
96 | #00ffd5,
97 | #002bff,
98 | #7a00ff,
99 | #ff00c8,
100 | #ff0000
101 | );
102 | position: absolute;
103 | top: -2px;
104 | left: -2px;
105 | background-size: 400%;
106 | z-index: -1;
107 | filter: blur(5px);
108 | width: calc(100% + 4px);
109 | height: calc(100% + 4px);
110 | animation: glowing 20s linear infinite; //animation apply here
111 | opacity: 0;
112 | transition: opacity 0.4s ease-in-out;
113 | border-radius: 10px;
114 | }
115 | // &:hover {
116 | // background-color: rgb(54, 53, 53);
117 | // }
118 | &:after {
119 | z-index: -1;
120 | content: '';
121 | position: absolute;
122 | width: 100%;
123 | height: 100%;
124 | background: rgb(15, 15, 15);
125 | left: 0;
126 | top: 0;
127 | border-radius: 10px;
128 | }
129 | &:hover:before {
130 | opacity: 1;
131 | }
132 | }
133 |
134 | //keyframe is for animated
135 | @keyframes glowing {
136 | 0% {
137 | background-position: 0 0;
138 | }
139 | 50% {
140 | background-position: 400% 0;
141 | }
142 | 100% {
143 | background-position: 0 0;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/scss/mainContainer.scss:
--------------------------------------------------------------------------------
1 | .main-container {
2 | display: flex;
3 | flex-direction: column;
4 | width: 80%;
5 | text-align: center;
6 | height: 100%;
7 |
8 | .main-header {
9 | display: flex;
10 | align-items: center;
11 | justify-content: space-between;
12 | margin: 10px;
13 |
14 | .edit-board-btn {
15 | padding: 5px 10px;
16 | border: none;
17 | border-radius: 10px;
18 | background-color: lightgrey;
19 | cursor: pointer;
20 |
21 | .edit-svg {
22 | width: 20px;
23 | height: auto;
24 | margin: 0;
25 | }
26 | }
27 |
28 | .board-title {
29 | margin: 0 auto;
30 | font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
31 | "Lucida Sans", Arial, sans-serif;
32 | font-size: 32px;
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/scss/modal.scss:
--------------------------------------------------------------------------------
1 | .modal-overlay {
2 | color: white;
3 | position: fixed;
4 | top: 0;
5 | left: 0;
6 | right: 0;
7 | bottom: 0;
8 | background-color: rgba(0, 0, 0, 0.7);
9 | z-index: 100;
10 |
11 | .modal {
12 | position: fixed;
13 | top: 50%;
14 | left: 50%;
15 | transform: translate(-50%, -50%);
16 | background-color: rgb(48, 48, 48);
17 | padding: 30px;
18 | width: 35%;
19 | border-radius: 20px;
20 | box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px,
21 | rgba(0, 0, 0, 0.3) 0px 8px 16px -8px;
22 |
23 | .modal-form {
24 | display: flex;
25 | flex-direction: column;
26 | gap: 20px;
27 |
28 | .modal-title {
29 | margin: 0;
30 | text-align: center;
31 | font-size: 32px;
32 | color: goldenrod;
33 | font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
34 | "Lucida Sans", Arial, sans-serif;
35 | }
36 |
37 | .modal-input {
38 | margin-bottom: 10px;
39 | padding: 8px;
40 | width: 100%;
41 | box-sizing: border-box;
42 | font-size: 18px;
43 | font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
44 | "Lucida Sans", Arial, sans-serif;
45 | border-radius: 10px;
46 | }
47 |
48 | .modal-radio {
49 | display: flex;
50 | justify-content: space-around;
51 | }
52 |
53 | .modal-label {
54 | font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
55 | "Lucida Sans", Arial, sans-serif;
56 | }
57 |
58 | .modal-option {
59 | font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
60 | "Lucida Sans", Arial, sans-serif;
61 | }
62 |
63 | .text {
64 | height: 150px;
65 | }
66 |
67 | .modal-btns {
68 | display: flex;
69 | gap: 20px;
70 |
71 | button {
72 | font-size: 18px;
73 | font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
74 | "Lucida Sans", Arial, sans-serif;
75 | padding: 10px 20px;
76 | border: none;
77 | border-radius: 5px;
78 | width: 100px;
79 | cursor: pointer;
80 | transition: all 0.3s ease-in-out;
81 | &:disabled {
82 | background-color: gray;
83 | }
84 | &:hover {
85 | transform: scale(1.1);
86 | }
87 | }
88 |
89 | .modal-submit {
90 | background-color: #c3ff0e;
91 | }
92 |
93 | .modal-cancel {
94 | background-color: goldenrod;
95 | }
96 |
97 | .modal-delete {
98 | background-color: rgb(206, 61, 61);
99 | }
100 | }
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/scss/taskCard.scss:
--------------------------------------------------------------------------------
1 | .task-card {
2 | background-color: #a0d308;
3 | padding: 10px 5px 5px;
4 | margin: 15px;
5 | border-radius: 10px;
6 | display: flex;
7 | flex-direction: column;
8 | gap: 10px;
9 | box-shadow: 6.1px 12.2px 12.2px hsl(0deg 0% 0% / 0.31);
10 |
11 | .task-name {
12 | color: black;
13 | margin: 0;
14 | font-size: 18px;
15 | font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
16 | "Lucida Sans", Arial, sans-serif;
17 | }
18 |
19 | .task-notes {
20 | color: black;
21 | margin: 0;
22 | font-size: 14px;
23 | font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
24 | "Lucida Sans", Arial, sans-serif;
25 | }
26 |
27 | .task-btns-container {
28 | display: flex;
29 | justify-content: space-between;
30 |
31 | .task-btn {
32 | cursor: pointer;
33 | border: none;
34 | background: none;
35 | align-self: self-end;
36 |
37 | .task-svg {
38 | width: 20px;
39 | height: auto;
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | // PROP TYPES
2 | export type SignupProps = {
3 | setLoggingIn: React.Dispatch>;
4 | setUser: React.Dispatch>;
5 | };
6 |
7 | export type AuthProps = {
8 | setUser: React.Dispatch>;
9 | };
10 |
11 | export type DashboardProps = {
12 | user: UserState;
13 | };
14 |
15 | export type LeftContainerProps = {
16 | user: UserState;
17 | setCurrentBoard: React.Dispatch>;
18 | currentBoard: CurrentBoardState;
19 | };
20 |
21 | export type MainContainerProps = {
22 | user: UserState;
23 | currentBoard: CurrentBoardState;
24 | setCurrentBoard: React.Dispatch>;
25 | };
26 |
27 | export type CreateBoardModalProps = {
28 | setCreatingBoard: React.Dispatch>;
29 | setCurrentBoard: React.Dispatch>;
30 | boardList: React.ReactNode[];
31 | setBoardList: React.Dispatch>;
32 | user: UserState;
33 | handleBoardSelect: (e: React.MouseEvent) => void;
34 | selectedBoard: string | null;
35 | setSelectedBoard: React.Dispatch>;
36 | };
37 |
38 | export type EditBoardModalProps = {
39 | setEditingBoard: React.Dispatch>;
40 | setCurrentBoard: React.Dispatch>;
41 | currentBoard: CurrentBoardState;
42 | };
43 |
44 | export type NewTaskModalProps = {
45 | setAddingTask: React.Dispatch>;
46 | currentBoard: CurrentBoardState;
47 | setBoardState: React.Dispatch>;
48 | };
49 |
50 | export type EditTaskModalProps = {
51 | setEditingTask: React.Dispatch>;
52 | currentBoard: CurrentBoardState;
53 | setBoardState: React.Dispatch>;
54 | task: TaskState;
55 | startColumn: "backlog" | "inProgress" | "inReview" | "completed";
56 | };
57 |
58 | export type ColumnProps = {
59 | name: keyof BoardState;
60 | create: boolean;
61 | user: UserState;
62 | currentBoard: CurrentBoardState;
63 | boardState: BoardState;
64 | setBoardState: React.Dispatch>;
65 | };
66 |
67 | export type ColumnContainerProps = {
68 | user: UserState;
69 | currentBoard: CurrentBoardState;
70 | boardState: BoardState;
71 | setBoardState: React.Dispatch>;
72 | };
73 |
74 | export type CardProps = {
75 | info: TaskState;
76 | setEditingTask: React.Dispatch>;
77 | };
78 | // STATE TYPES
79 | export interface UserState {
80 | name: string;
81 | id: string;
82 | }
83 |
84 | export interface AuthFormState {
85 | username: string;
86 | email?: string;
87 | password: string;
88 | confirmPassword?: string;
89 | }
90 |
91 | export interface CurrentBoardState {
92 | name: string;
93 | id: string;
94 | }
95 |
96 | export interface BoardListItemState {
97 | name: string;
98 | id: string;
99 | }
100 |
101 | export interface TaskFormState {
102 | taskname: string;
103 | status: string;
104 | tasknotes: string;
105 | }
106 |
107 | export interface BoardState {
108 | backlog: TaskState[];
109 | inProgress: TaskState[];
110 | inReview: TaskState[];
111 | completed: TaskState[];
112 | }
113 |
114 | export interface TaskState {
115 | name: string;
116 | notes: string;
117 | status: "backlog" | "inProgress" | "inReview" | "completed";
118 | __v: number;
119 | _id: string;
120 | }
121 |
122 | // TYPES
123 | export type BoardType = {
124 | name: string;
125 | backlog: TaskState[];
126 | inProgress: TaskState[];
127 | inReview: TaskState[];
128 | completed: TaskState[];
129 | __v: number;
130 | _id: string;
131 | };
132 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true
23 | },
24 | "ts-node": {
25 | "esm": true,
26 | "compiler": "typescript"
27 | },
28 | "include": ["./**/*"],
29 | "references": [{ "path": "./tsconfig.node.json" }]
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 | import "dotenv/config";
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [react()],
8 | server: {
9 | proxy: {
10 | "/user": {
11 | target:
12 | `http://localhost:${process.env.PORT}`,
13 | changeOrigin: true,
14 | secure: false,
15 | },
16 | "/boards": {
17 | target:
18 | `http://localhost:${process.env.PORT}`,
19 | changeOrigin: true,
20 | secure: false,
21 | },
22 | "/tasks": {
23 | target:
24 | `http://localhost:${process.env.PORT}`,
25 | changeOrigin: true,
26 | secure: false,
27 | },
28 | },
29 | port: 5173,
30 | },
31 | });
32 |
--------------------------------------------------------------------------------