├── server ├── procfile ├── error.js ├── middleware │ ├── auth.js │ └── verifyToken.js ├── models │ ├── Notifications.js │ ├── Tasks.js │ ├── Works.js │ ├── User.js │ ├── Teams.js │ └── Project.js ├── package.json ├── routes │ ├── auth.js │ ├── teams.js │ ├── user.js │ └── project.js ├── index.js └── controllers │ ├── works.js │ └── user.js ├── client ├── src │ ├── data │ │ ├── types.js │ │ └── data.js │ ├── Images │ │ ├── Header.png │ │ ├── AddProject.gif │ │ ├── google.svg │ │ └── Logo.svg │ ├── pages │ │ ├── Home │ │ │ ├── components │ │ │ │ ├── About.jsx │ │ │ │ ├── HeroBgAnimation │ │ │ │ │ └── HeroBgAnimationStyle.js │ │ │ │ ├── Faq.jsx │ │ │ │ ├── Testimonials.jsx │ │ │ │ ├── TeamMember.jsx │ │ │ │ ├── Navbar.jsx │ │ │ │ ├── Team.jsx │ │ │ │ ├── Hero.jsx │ │ │ │ ├── Footer.jsx │ │ │ │ ├── Features.jsx │ │ │ │ └── Benifits.jsx │ │ │ └── Home.jsx │ │ ├── Community.jsx │ │ ├── Chats.jsx │ │ └── Projects.jsx │ ├── index.css │ ├── redux │ │ ├── snackbarSlice.js │ │ ├── store.js │ │ └── userSlice.js │ ├── index.js │ ├── components │ │ ├── ToastMessage.jsx │ │ ├── ToolsCard.jsx │ │ ├── DropWrapper.jsx │ │ ├── IdeaCard.jsx │ │ ├── ProjectInvite.jsx │ │ ├── MemberCard.jsx │ │ ├── TeamInvite.jsx │ │ ├── ImageSelector.jsx │ │ ├── NotificationDialog.jsx │ │ ├── AccountDialog.jsx │ │ ├── ProjectStatCard.jsx │ │ ├── TaskCard.jsx │ │ ├── Card.jsx │ │ ├── DeletePopup.jsx │ │ ├── ChatContact.jsx │ │ ├── WorkCards.jsx │ │ ├── Menu.jsx │ │ ├── ChatContainer.jsx │ │ ├── Navbar.jsx │ │ ├── InviteMembers.jsx │ │ └── WorkDetails.jsx │ ├── utils │ │ └── Theme.js │ ├── logo.svg │ ├── App.js │ └── api │ │ └── index.js ├── netlify.toml ├── public │ ├── favicon.ico │ ├── index.html │ └── Logo.svg ├── package.json └── README.md ├── .gitignore ├── README.md └── LICENSE /server/procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /client/src/data/types.js: -------------------------------------------------------------------------------- 1 | const ITEM_TYPE = "ITEM"; 2 | 3 | export default ITEM_TYPE; -------------------------------------------------------------------------------- /client/netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/index.html" 4 | status = 200 -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rishavchanda/Project-Management-App/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/Images/Header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rishavchanda/Project-Management-App/HEAD/client/src/Images/Header.png -------------------------------------------------------------------------------- /client/src/Images/AddProject.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rishavchanda/Project-Management-App/HEAD/client/src/Images/AddProject.gif -------------------------------------------------------------------------------- /client/src/pages/Home/components/About.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const About = () => { 4 | return ( 5 |
About
6 | ) 7 | } 8 | 9 | export default About -------------------------------------------------------------------------------- /server/error.js: -------------------------------------------------------------------------------- 1 | export const createError = (status, message)=>{ 2 | const err = new Error() 3 | err.status= status 4 | err.message= message 5 | return err 6 | } -------------------------------------------------------------------------------- /client/src/pages/Community.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Community = () => { 4 | return ( 5 |
Comming Soon...
6 | ) 7 | } 8 | 9 | export default Community -------------------------------------------------------------------------------- /client/src/pages/Home/components/HeroBgAnimation/HeroBgAnimationStyle.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | 4 | export const Div = styled.div` 5 | width: 500px; 6 | height: 500px; 7 | ` -------------------------------------------------------------------------------- /server/middleware/auth.js: -------------------------------------------------------------------------------- 1 | 2 | export function localVariables(req, res, next) { 3 | res.app.locals = { 4 | OTP: null, 5 | resetSession: false, 6 | CODE: null 7 | }; 8 | next(); 9 | } -------------------------------------------------------------------------------- /server/models/Notifications.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const Notifications = new mongoose.Schema({ 4 | message: { 5 | type: String, 6 | required: true, 7 | }, 8 | link: { 9 | type: String, 10 | }, 11 | type: { type: String, required: true }, 12 | }, 13 | { timestamps: true } 14 | ); 15 | 16 | export default mongoose.model("Notifications", Notifications); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | server/node_modules 5 | client/node_modules 6 | client/.env 7 | server/.env 8 | /.pnp 9 | .pnp.js 10 | 11 | # testing 12 | /coverage 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .env 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@100;300;400;500;600&display=swap'); 2 | html{ 3 | scroll-behavior: smooth; 4 | } 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | font-family: 'Montserrat', sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | /* width */ 15 | ::-webkit-scrollbar { 16 | width: 2px; 17 | } 18 | /* Track */ 19 | ::-webkit-scrollbar-track { 20 | 21 | } 22 | 23 | /* Handle */ 24 | ::-webkit-scrollbar-thumb { 25 | background: #888; 26 | border-radius: 6px; 27 | height: 50px; 28 | } -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js", 9 | "dev": "nodemon index.js" 10 | }, 11 | "author": "rishav", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt": "^5.1.0", 15 | "cookie-parser": "^1.4.6", 16 | "cors": "^2.8.5", 17 | "dotenv": "^16.0.3", 18 | "express": "^4.18.2", 19 | "jsonwebtoken": "^8.5.1", 20 | "mongoose": "^6.7.0", 21 | "morgan": "^1.10.0", 22 | "nodemailer": "^6.8.0", 23 | "nodemon": "^2.0.20", 24 | "otp-generator": "^4.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vexa Project 2 | Welcome to Vexa - a project management app designed to help you manage your projects more efficiently. 3 | 4 | ## Features 5 | - Project management 6 | - Team collaboration 7 | - Community building 8 | - Time tracking 9 | 10 | ## Benefits 11 | - Increased productivity 12 | - Improved communication 13 | - Better project outcomes 14 | - Increased collaboration 15 | - Networking opportunities 16 | 17 | 18 | ## Technologies Used 19 | - React Js 20 | - NodeJs 21 | - Express Js 22 | - Mongo Db 23 | - Styled Components 24 | - Material-UI 25 | - Nodemailer 26 | 27 | Installation 28 | 29 | ![image](https://user-images.githubusercontent.com/64485885/234916413-96296f13-fe4b-4cc4-b215-e72bd7c27928.png) 30 | -------------------------------------------------------------------------------- /client/src/redux/snackbarSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | open: false, 5 | message: "", 6 | severity: "success", 7 | }; 8 | 9 | const snackbar = createSlice({ 10 | name: 'snackbar', 11 | initialState, 12 | reducers: { 13 | openSnackbar: (state, action) => { 14 | state.open = true; 15 | state.message = action.payload.message; 16 | state.severity = action.payload.severity; 17 | }, 18 | closeSnackbar: (state) => { 19 | state.open = false; 20 | } 21 | } 22 | }); 23 | 24 | export const { openSnackbar, closeSnackbar } = snackbar.actions; 25 | 26 | export default snackbar.reducer; 27 | -------------------------------------------------------------------------------- /server/middleware/verifyToken.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import { createError } from "../error.js"; 3 | 4 | export const verifyToken = async (req, res, next) => { 5 | try { 6 | if (!req.headers.authorization) return next(createError(401, "You are not authenticated!")); 7 | // Get the token from the header 8 | const token = req.headers.authorization.split(" ")[1]; 9 | // Check if token exists 10 | if (!token) return next(createError(401, "You are not authenticated!")); 11 | 12 | const decode = await jwt.verify(token, process.env.JWT); 13 | req.user = decode; 14 | next(); 15 | } catch (error) { 16 | console.log(error) 17 | res.status(402).json({ error: error.message }) 18 | } 19 | }; -------------------------------------------------------------------------------- /server/models/Tasks.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const TasksSchema = new mongoose.Schema({ 4 | projectId: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | ref: "Projects", 7 | required: true, 8 | unique: false, 9 | }, 10 | workId: { type: String, unique: false }, 11 | task: { type: String, required: true }, 12 | start_date: { type: String, required: true, default: "" }, 13 | end_date: { type: String, required: true, default: "" }, 14 | members: { 15 | type: [mongoose.Schema.Types.ObjectId], 16 | ref: "User", 17 | default: [], 18 | }, 19 | status: { type: String, default: "Working" }, 20 | }, 21 | { timestamps: true } 22 | ); 23 | 24 | export default mongoose.model("Tasks", TasksSchema); -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { store, persistor } from './redux/store'; 6 | import { Provider } from 'react-redux'; 7 | import { PersistGate } from 'redux-persist/integration/react'; 8 | import { CookiesProvider } from "react-cookie"; 9 | import { GoogleOAuthProvider } from '@react-oauth/google'; 10 | 11 | const root = ReactDOM.createRoot(document.getElementById('root')); 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | 24 | -------------------------------------------------------------------------------- /client/src/components/ToastMessage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Alert from "@mui/material/Alert"; 3 | import Snackbar from "@mui/material/Snackbar"; 4 | import { useState } from "react"; 5 | import { closeSnackbar } from "../redux/snackbarSlice"; 6 | import { useSelector } from "react-redux"; 7 | import { useDispatch } from "react-redux"; 8 | 9 | const ToastMessage = ({ 10 | message, 11 | severity, 12 | open, 13 | }) => { 14 | const dispatch = useDispatch(); 15 | return ( 16 | dispatch(closeSnackbar())} 20 | > 21 | dispatch(closeSnackbar())} 23 | severity={severity} 24 | sx={{ width: "100%" }} 25 | > 26 | {message} 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default ToastMessage; 33 | -------------------------------------------------------------------------------- /client/src/components/ToolsCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from "styled-components"; 3 | 4 | 5 | const Container = styled.div` 6 | padding: 8px 16px; 7 | font-weight: 500; 8 | cursor: pointer; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | gap: 6px; 13 | border: 1.2px solid ${({ theme }) => theme.soft2 + "99"}; 14 | color: ${({ theme }) => theme.soft2+"99"}; 15 | border-radius: 12px 16 | `; 17 | 18 | const Img = styled.img` 19 | width: 16px; 20 | height: 16px; 21 | `; 22 | 23 | const Text = styled.div` 24 | font-size: 12px; 25 | font-weight: 500; 26 | color: ${({ theme }) => theme.soft2}; 27 | `; 28 | 29 | const ToolsCard = ({tool}) => { 30 | const openWebsite = () => { 31 | window.open(tool.link, "_blank"); 32 | } 33 | return ( 34 | 35 | 36 | {tool.name} 37 | 38 | ) 39 | } 40 | 41 | export default ToolsCard -------------------------------------------------------------------------------- /server/routes/auth.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { signup,signin, logout, googleAuthSignIn, generateOTP, verifyOTP, createResetSession,findUserByEmail, resetPassword } from "../controllers/auth.js"; 3 | import { verifyToken } from "../middleware/verifyToken.js"; 4 | import { localVariables } from "../middleware/auth.js"; 5 | 6 | const router = express.Router(); 7 | 8 | //create a user 9 | router.post("/signup", signup); 10 | //signin 11 | router.post("/signin", signin); 12 | //logout 13 | router.post("/logout", logout); 14 | //google signin 15 | router.post("/google", googleAuthSignIn); 16 | //find user by email 17 | router.get("/findbyemail", findUserByEmail); 18 | //generate opt 19 | router.get("/generateotp",localVariables, generateOTP); 20 | //verify opt 21 | router.get("/verifyotp", verifyOTP); 22 | //create reset session 23 | router.get("/createResetSession", createResetSession); 24 | //forget password 25 | router.put("/forgetpassword", resetPassword); 26 | 27 | 28 | 29 | 30 | export default router; -------------------------------------------------------------------------------- /client/src/components/DropWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDrop } from "react-dnd"; 3 | import ITEM_TYPE from "../data/types"; 4 | import { statuses } from "../data/data"; 5 | 6 | const DropWrapper = ({ onDrop, children, status }) => { 7 | const [{ isOver }, drop] = useDrop({ 8 | accept: ITEM_TYPE, 9 | canDrop: (item, monitor) => { 10 | const itemIndex = statuses.findIndex(si => si.status === item.status); 11 | const statusIndex = statuses.findIndex(si => si.status === status); 12 | return [itemIndex + 1, itemIndex - 1, itemIndex].includes(statusIndex); 13 | }, 14 | drop: (item, monitor) => { 15 | onDrop(item, monitor, status); 16 | }, 17 | collect: monitor => ({ 18 | isOver: monitor.isOver() 19 | }) 20 | }); 21 | 22 | return ( 23 |
24 | {React.cloneElement(children, { isOver })} 25 |
26 | ) 27 | }; 28 | 29 | export default DropWrapper; -------------------------------------------------------------------------------- /client/src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore, combineReducers } from "@reduxjs/toolkit"; 2 | import userReducer from "./userSlice"; 3 | import snackbarReducer from "./snackbarSlice"; 4 | import { 5 | persistStore, 6 | persistReducer, 7 | FLUSH, 8 | REHYDRATE, 9 | PAUSE, 10 | PERSIST, 11 | PURGE, 12 | REGISTER, 13 | } from "redux-persist"; 14 | import storage from "redux-persist/lib/storage"; 15 | import { PersistGate } from "redux-persist/integration/react"; 16 | 17 | const persistConfig = { 18 | key: "root", 19 | version: 1, 20 | storage, 21 | }; 22 | 23 | const rootReducer = combineReducers({ user: userReducer, snackbar: snackbarReducer }); 24 | 25 | const persistedReducer = persistReducer(persistConfig, rootReducer); 26 | 27 | export const store = configureStore({ 28 | reducer: persistedReducer, 29 | middleware: (getDefaultMiddleware) => 30 | getDefaultMiddleware({ 31 | serializableCheck: { 32 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], 33 | }, 34 | }), 35 | }); 36 | 37 | export const persistor = persistStore(store) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rishav Chanda 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 | -------------------------------------------------------------------------------- /client/src/components/IdeaCard.jsx: -------------------------------------------------------------------------------- 1 | import { Delete } from "@mui/icons-material"; 2 | import { IconButton } from "@mui/material"; 3 | import React from "react"; 4 | import styled from "styled-components"; 5 | 6 | const Container = styled.div` 7 | display: flex; 8 | align-items: center; 9 | border-radius: 10px; 10 | gap: 12px; 11 | justify-content: space-between; 12 | `; 13 | 14 | const Span = styled.div` 15 | font-size: 13px; 16 | font-weight: 600; 17 | margin-top: 1px; 18 | color: ${({ theme }) => theme.textSoft + "99"}; 19 | `; 20 | 21 | const Text = styled.div` 22 | display: flex; 23 | gap: 8px; 24 | font-size: 13px; 25 | color: ${({ theme }) => theme.textSoft + "99"}; 26 | `; 27 | 28 | const IcoButton = styled(IconButton)` 29 | color: ${({ theme }) => theme.textSoft + "99"} !important; 30 | `; 31 | 32 | const IdeaCard = ({ idea, no }) => { 33 | return ( 34 | 35 | 36 | {no + 1}) 37 | {idea} 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default IdeaCard; 47 | -------------------------------------------------------------------------------- /client/src/Images/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /server/models/Works.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const WorksSchema = new mongoose.Schema({ 4 | projectId: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | ref: "Projects", 7 | required: true, 8 | unique: false, 9 | }, 10 | creatorId: { 11 | type: mongoose.Schema.Types.ObjectId, 12 | ref: "User", 13 | required: true, 14 | unique: false, 15 | }, 16 | title: { 17 | type: String, 18 | required: true, 19 | unique: false, 20 | }, 21 | desc: { 22 | type: String, 23 | required: true, 24 | unique: false, 25 | }, 26 | priority: { 27 | type: String, 28 | required: true, 29 | default: "Low", 30 | }, 31 | tags: { 32 | type: [String], 33 | default: [], 34 | }, 35 | status: { 36 | type: String, 37 | required: true, 38 | default: "Working", 39 | }, 40 | tasks: { 41 | type: [mongoose.Schema.Types.ObjectId], 42 | ref: "Tasks", 43 | default: [], 44 | } 45 | }, 46 | { timestamps: true } 47 | ); 48 | 49 | export default mongoose.model("Works", WorksSchema); -------------------------------------------------------------------------------- /server/routes/teams.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { addTeam, getTeam, deleteTeam, updateTeam, addTeamProject, inviteTeamMember, verifyInvitationTeam, getTeamMembers, removeMember, updateMembers } from "../controllers/teams.js"; 3 | import { verifyToken } from "../middleware/verifyToken.js"; 4 | import { localVariables } from "../middleware/auth.js"; 5 | 6 | const router = express.Router(); 7 | 8 | //create a Team 9 | router.post("/",verifyToken, addTeam); 10 | //get all Teams 11 | router.get("/:id",verifyToken, getTeam) 12 | //delete a Team 13 | router.delete("/:id", verifyToken, deleteTeam) 14 | //update a Team 15 | router.patch("/:id", verifyToken, updateTeam) 16 | //update a team member 17 | router.patch("/member/:id", verifyToken, updateMembers) 18 | //remove a team member 19 | router.patch("/member/remove/:id", verifyToken, removeMember) 20 | //add a team project 21 | router.post("/addProject/:id", verifyToken, addTeamProject) 22 | //invite a team member 23 | router.post("/invite/:id", verifyToken,localVariables, inviteTeamMember) 24 | //verify a invite 25 | router.get("/invite/:code",verifyInvitationTeam) 26 | //get team members 27 | router.get("/members/:id", verifyToken, getTeamMembers) 28 | 29 | 30 | export default router; -------------------------------------------------------------------------------- /server/routes/user.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | update, 4 | deleteUser, 5 | getUser, 6 | subscribe, 7 | unsubscribe, 8 | getUserProjects, 9 | getUserTeams, 10 | findUser, 11 | findUserByEmail, 12 | getNotifications, 13 | getWorks, 14 | getTasks 15 | } from "../controllers/user.js"; 16 | import { verifyToken } from "../middleware/verifyToken.js"; 17 | 18 | const router = express.Router(); 19 | 20 | //update user 21 | router.put("/:id", verifyToken, update); 22 | 23 | //delete user 24 | router.delete("/:id", verifyToken, deleteUser); 25 | 26 | //get a user 27 | router.get("/find/:id",verifyToken, findUser); 28 | router.get("/find",verifyToken, getUser); 29 | 30 | //get user projects 31 | router.get("/projects", verifyToken, getUserProjects); 32 | 33 | //get user teams 34 | router.get("/teams", verifyToken, getUserTeams); 35 | 36 | //search a user 37 | router.get("/search/:email",verifyToken, findUserByEmail); 38 | 39 | //get notifications of a user 40 | router.get("/notifications", verifyToken, getNotifications); 41 | 42 | //get works of a user 43 | router.get("/works", verifyToken, getWorks); 44 | 45 | //get tasks of a user 46 | router.get("/tasks", verifyToken, getTasks); 47 | 48 | 49 | export default router; -------------------------------------------------------------------------------- /client/src/utils/Theme.js: -------------------------------------------------------------------------------- 1 | export const darkTheme = { 2 | bg:"#15171E", 3 | bgLighter:"#1C1E27", 4 | itemHover: "#3A3C45", 5 | itemText:"#DADEDF", 6 | primary: "#854CE6", 7 | bgDark:"#15171E", 8 | text:"#F2F3F4", 9 | textSoft:"#C1C7C9", 10 | soft:"#373D3F", 11 | soft2:"#8C979A", 12 | card: "#1C1E27", 13 | lightAdd:"99", 14 | yellow: "#fccf03", 15 | green: "#03fc45", 16 | pink: "#fc036b", 17 | recieve_message: "#1a1826", 18 | chat_background: "#100f17", 19 | send_message: "#4f34ad", 20 | contact_background: "#151a21", 21 | 22 | /*Colors 23 | Swatch 1 24 | #030405 25 | Swatch 2 26 | #585759 27 | Swatch 3 28 | #03840c 29 | Swatch 4 30 | #acb0b3 31 | Swatch 5 32 | #185c1a 33 | Swatch 6 34 | #40b957*/ 35 | } 36 | 37 | export const lightTheme = { 38 | bg:"#F8F9FD", 39 | bgLighter:"#FFFFFF", 40 | itemHover: "#F7F5F4", 41 | itemText:"#555F61", 42 | primary: "#854CE6", 43 | bgDark:"white", 44 | text:"#2F2F2F", 45 | textSoft:"#606060", 46 | soft:"#C4C4C4", 47 | soft2:"#757575", 48 | card: "#FFFFFF", 49 | lightAdd:"", 50 | recieve_message: "#ffffff", 51 | chat_background: "#f5f5fc", 52 | send_message: "#4f34ad", 53 | send_message_text_color: "#ffffff", 54 | contact_background: "#ffffff", 55 | 56 | } -------------------------------------------------------------------------------- /server/routes/project.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { addProject, deleteProject, getProject, updateProject,removeMember, inviteProjectMember, verifyInvitation, getProjectMembers, addWork, getWorks, updateMembers } from "../controllers/project.js"; 3 | import { verifyToken } from "../middleware/verifyToken.js"; 4 | import { localVariables } from "../middleware/auth.js"; 5 | 6 | const router = express.Router(); 7 | 8 | //create a project 9 | router.post("/", verifyToken, addProject); 10 | //get all projects 11 | router.get("/:id", verifyToken, getProject) 12 | //delete a project 13 | router.delete("/:id", verifyToken, deleteProject) 14 | //update a project 15 | router.patch("/:id", verifyToken, updateProject) 16 | //update a project member 17 | router.patch("/member/:id", verifyToken, updateMembers) 18 | //remove a project member 19 | router.patch("/member/remove/:id", verifyToken, removeMember) 20 | 21 | //invite a project 22 | router.post("/invite/:id", verifyToken, localVariables, inviteProjectMember) 23 | //verify a invite 24 | router.get("/invite/:code", verifyInvitation) 25 | //get members 26 | router.get("/members/:id",verifyToken, getProjectMembers) 27 | 28 | //works 29 | // add works to a project 30 | router.post("/works/:id", verifyToken, addWork) 31 | //get all works of a project 32 | router.get("/works/:id", verifyToken, getWorks) 33 | 34 | 35 | export default router; -------------------------------------------------------------------------------- /client/src/redux/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { useNavigate } from 'react-router-dom' 3 | 4 | const initialState = { 5 | currentUser: null, 6 | loading: false, 7 | error: false, 8 | }; 9 | 10 | export const userSlice = createSlice({ 11 | name: "user", 12 | initialState, 13 | reducers: { 14 | loginStart: (state) => { 15 | state.loading = true; 16 | }, 17 | loginSuccess: (state, action) => { 18 | state.loading = false; 19 | state.currentUser = action.payload.user; 20 | localStorage.setItem('token', action.payload.token); 21 | }, 22 | loginFailure: (state) => { 23 | state.loading = false; 24 | state.error = true; 25 | }, 26 | logout: (state) => { 27 | state.currentUser = null; 28 | state.loading = false; 29 | state.error = false; 30 | localStorage.removeItem('token'); 31 | }, 32 | verified: (state, action) => { 33 | if(state.currentUser){ 34 | state.currentUser.verified = action.payload; 35 | } 36 | }, 37 | subscription: (state, action) => { 38 | if (state.currentUser.subscribedUsers.includes(action.payload)) { 39 | state.currentUser.subscribedUsers.splice( 40 | state.currentUser.subscribedUsers.findIndex( 41 | (channelId) => channelId === action.payload 42 | ), 43 | 1 44 | ); 45 | } else { 46 | state.currentUser.subscribedUsers.push(action.payload); 47 | } 48 | }, 49 | }, 50 | }); 51 | 52 | export const { loginStart, loginSuccess, loginFailure, logout, subscription,verified } = 53 | userSlice.actions; 54 | 55 | export default userSlice.reducer; -------------------------------------------------------------------------------- /client/src/pages/Home/components/Faq.jsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const FaqContainer = styled.section` 4 | background-color: #060B27; 5 | color: #fff; 6 | padding: 80px 0; 7 | `; 8 | 9 | const FaqTitle = styled.h2` 10 | text-align: center; 11 | font-size: 36px; 12 | margin-bottom: 40px; 13 | `; 14 | 15 | const FaqList = styled.ul` 16 | list-style: none; 17 | margin: 0; 18 | padding: 0; 19 | `; 20 | 21 | const FaqItem = styled.li` 22 | margin-bottom: 20px; 23 | `; 24 | 25 | const FaqQuestion = styled.h4` 26 | font-size: 24px; 27 | margin-bottom: 10px; 28 | `; 29 | 30 | const FaqAnswer = styled.p` 31 | font-size: 18px; 32 | `; 33 | 34 | const Faq = () => { 35 | const faqData = [ 36 | { 37 | question: "Is my data secure?", 38 | answer: 39 | "Yes, we take security very seriously and use the latest encryption technology to protect your data.", 40 | }, 41 | { 42 | question: "How much does the app cost?", 43 | answer: 44 | "Our app has a variety of pricing plans to fit any budget, starting at just $9.99/month.", 45 | }, 46 | { 47 | question: "What kind of support is available?", 48 | answer: 49 | "We offer 24/7 support via email and live chat to ensure you get the help you need.", 50 | }, 51 | ]; 52 | 53 | return ( 54 | 55 | Frequently Asked Questions 56 | 57 | {faqData.map((faq, index) => ( 58 | 59 | {faq.question} 60 | {faq.answer} 61 | 62 | ))} 63 | 64 | 65 | ); 66 | }; 67 | 68 | export default Faq; 69 | -------------------------------------------------------------------------------- /server/models/User.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import jwt from "jsonwebtoken"; 3 | 4 | const UserSchema = new mongoose.Schema({ 5 | name: { 6 | type: String, 7 | required: true, 8 | unique: false, 9 | }, 10 | email: { 11 | type: String, 12 | required: true, 13 | unique: true, 14 | }, 15 | password: { 16 | type: String, 17 | default: "", 18 | }, 19 | img: { 20 | type: String, 21 | default: "", 22 | }, 23 | googleSignIn:{ 24 | type: Boolean, 25 | required: true, 26 | default: false, 27 | }, 28 | projects: { 29 | type: [mongoose.Schema.Types.ObjectId], 30 | ref: "Project", 31 | default: [], 32 | }, 33 | teams: { 34 | type: [mongoose.Schema.Types.ObjectId], 35 | ref: "Teams", 36 | default: [], 37 | }, 38 | notifications: { 39 | type: [mongoose.Schema.Types.ObjectId], 40 | ref: "Notifications", 41 | default: [], 42 | }, 43 | works: { 44 | type: [mongoose.Schema.Types.ObjectId], 45 | ref: "Works", 46 | default: [], 47 | }, 48 | tasks: { 49 | type: [mongoose.Schema.Types.ObjectId], 50 | ref: "Tasks", 51 | default: [], 52 | } 53 | }, 54 | { timestamps: true } 55 | ); 56 | 57 | UserSchema.methods.generateVerificationToken = function () { 58 | const user = this; 59 | const verificationToken = jwt.sign( 60 | { ID: user._id }, 61 | process.env.USER_VERIFICATION_TOKEN_SECRET, 62 | { expiresIn: "7d" } 63 | ); 64 | return verificationToken; 65 | }; 66 | 67 | export default mongoose.model("User", UserSchema); -------------------------------------------------------------------------------- /server/models/Teams.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Mongoose } from "mongoose"; 2 | 3 | const TeamSchema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | required: true, 7 | unique: false, 8 | }, 9 | desc: { 10 | type: String, 11 | required: true, 12 | unique: false, 13 | }, 14 | img: { 15 | type: String, 16 | default: "", 17 | unique: false, 18 | }, 19 | tools: { 20 | type: [{ 21 | _id: false, 22 | link: { 23 | type: String, 24 | required: true, 25 | }, 26 | name: { 27 | type: String, 28 | required: true, 29 | }, 30 | icon: { 31 | type: String, 32 | require: true, 33 | } 34 | }], 35 | default: [], 36 | }, 37 | members: { 38 | type: [{ 39 | _id: false, 40 | id: { 41 | type: mongoose.Schema.Types.ObjectId, 42 | ref: "User", 43 | required: true, 44 | }, 45 | role: { 46 | type: String, 47 | required: true, 48 | }, 49 | access: { 50 | type: String, 51 | require: true, 52 | default: "View Only", 53 | unique: false, 54 | } 55 | }], 56 | required: true, 57 | default: [], 58 | }, 59 | projects: { 60 | type: [mongoose.Schema.Types.ObjectId], 61 | default: [], 62 | ref: "Project", 63 | unique: true 64 | } 65 | }, 66 | { timestamps: true } 67 | ); 68 | 69 | export default mongoose.model("Teams", TeamSchema); -------------------------------------------------------------------------------- /client/src/pages/Home/components/Testimonials.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const TestimonialsContainer = styled.div` 5 | background-color: #060B27; 6 | padding: 50px 0; 7 | `; 8 | 9 | const TestimonialCard = styled.div` 10 | background-color: #1C1E27; 11 | border-radius: 20px; 12 | box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.2); 13 | padding: 30px; 14 | text-align: center; 15 | margin: 0 20px; 16 | width: 300px; 17 | `; 18 | 19 | const TestimonialText = styled.p` 20 | font-size: 18px; 21 | line-height: 1.5; 22 | color: #fff; 23 | `; 24 | 25 | const TestimonialAuthor = styled.p` 26 | font-size: 16px; 27 | font-weight: bold; 28 | margin-top: 20px; 29 | color: #854CE6; 30 | `; 31 | 32 | const Testimonials = () => { 33 | const testimonialsData = [{ id: 1, text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed auctor justo quis felis malesuada, a feugiat elit tristique.', author: 'John Doe', jobTitle: 'CEO' }, { id: 2, text: 'Donec laoreet elit in malesuada tempus. Aliquam pretium blandit commodo. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.', author: 'Jane Smith', jobTitle: 'Marketing Manager' }, { id: 3, text: 'Fusce eget turpis eget nulla fringilla auctor. Praesent eget mi nec quam dictum consequat nec eu nibh.', author: 'David Lee', jobTitle: 'Software Engineer' }]; 34 | 35 | return ( 36 | 37 |

Testimonials

38 |
39 | {testimonialsData.map(testimonial => ( 40 | 41 | "{testimonial.text}" 42 | {testimonial.author}, {testimonial.jobTitle} 43 | 44 | ))} 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default Testimonials; 51 | -------------------------------------------------------------------------------- /server/models/Project.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const ProjectSchema = new mongoose.Schema({ 4 | title: { 5 | type: String, 6 | required: true, 7 | unique: false, 8 | }, 9 | desc: { 10 | type: String, 11 | required: true, 12 | unique: false, 13 | }, 14 | img: { 15 | type: String, 16 | default: "", 17 | unique: false, 18 | }, 19 | tags: { 20 | type: [String], 21 | default: [], 22 | }, 23 | status: { 24 | type: String, 25 | required: true, 26 | default: "Working", 27 | }, 28 | works: { 29 | type: [String], 30 | default: [] 31 | }, 32 | tools: { 33 | type: [{ 34 | _id: false, 35 | link: { 36 | type: String, 37 | required: true, 38 | }, 39 | name: { 40 | type: String, 41 | required: true, 42 | }, 43 | icon: { 44 | type: String, 45 | require: true, 46 | } 47 | }], 48 | default: [], 49 | }, 50 | members: { 51 | type: [{ 52 | _id: false, 53 | id: { 54 | type: mongoose.Schema.Types.ObjectId, 55 | ref: "User", 56 | required: true, 57 | }, 58 | role: { 59 | type: String, 60 | required: true, 61 | }, 62 | access: { 63 | type: String, 64 | require: true, 65 | default: "View Only", 66 | unique: false, 67 | } 68 | }], 69 | required: true, 70 | default: [], 71 | }, 72 | }, 73 | { timestamps: true } 74 | ); 75 | 76 | export default mongoose.model("Project", ProjectSchema); -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import dotenv from 'dotenv'; 3 | import mongoose from 'mongoose'; 4 | import authRoutes from './routes/auth.js'; 5 | import userRoutes from './routes/user.js'; 6 | import projectRoutes from './routes/project.js'; 7 | import teamRoutes from './routes/teams.js'; 8 | import cookieParser from "cookie-parser"; 9 | import cors from 'cors'; 10 | import morgan from 'morgan'; 11 | const app = express(); 12 | dotenv.config(); 13 | 14 | /** Middlewares */ 15 | app.use(express.json()); 16 | const corsConfig = { 17 | credentials: true, 18 | origin: true, 19 | }; 20 | app.use(cors(corsConfig)); 21 | app.use(morgan('tiny')); 22 | app.disable('x-powered-by'); 23 | 24 | const port = process.env.PORT || 8700; 25 | 26 | const connect = () => { 27 | mongoose.set('strictQuery', true); 28 | mongoose.connect(process.env.MONGO_URL).then(() => { 29 | console.log('MongoDB connected'); 30 | }).catch((err) => { 31 | console.log(err); 32 | }); 33 | }; 34 | 35 | 36 | app.use(express.json()) 37 | // app.enable('trust proxy'); // optional, not needed for secure cookies 38 | // app.use(express.session({ 39 | // secret : '123456', 40 | // key : 'sid', 41 | // proxy : true, // add this when behind a reverse proxy, if you need secure cookies 42 | // cookie : { 43 | // secure : true, 44 | // maxAge: 5184000000 // 2 months 45 | // } 46 | // })); 47 | app.use("/api/auth", authRoutes) 48 | app.use("/api/users", userRoutes) 49 | app.use("/api/project", projectRoutes) 50 | app.use("/api/team", teamRoutes) 51 | app.use((err, req, res, next)=>{ 52 | const status = err.status || 500; 53 | const message = err.message || "Something went wrong"; 54 | return res.status(status).json({ 55 | success: false, 56 | status, 57 | message 58 | }) 59 | }) 60 | 61 | app.listen(port,()=>{ 62 | console.log("Connected") 63 | connect(); 64 | }) 65 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.10.4", 7 | "@emotion/styled": "^11.10.4", 8 | "@fortawesome/fontawesome-free": "^6.4.0", 9 | "@mui/icons-material": "^5.10.6", 10 | "@mui/material": "^5.10.8", 11 | "@mui/x-date-pickers": "^5.0.13", 12 | "@mui/x-date-pickers-pro": "^5.0.13", 13 | "@react-oauth/google": "^0.9.0", 14 | "@reduxjs/toolkit": "^1.9.0", 15 | "@testing-library/jest-dom": "^5.16.5", 16 | "@testing-library/react": "^13.4.0", 17 | "@testing-library/user-event": "^13.5.0", 18 | "axios": "^1.1.3", 19 | "dotenv": "^10.0.0", 20 | "firebase": "^9.13.0", 21 | "gh-pages": "^4.0.0", 22 | "jwt-decode": "^3.1.2", 23 | "react": "^18.2.0", 24 | "react-cookie": "^4.1.1", 25 | "react-dnd": "^16.0.1", 26 | "react-dnd-html5-backend": "^16.0.1", 27 | "react-dom": "^18.2.0", 28 | "react-file-image-to-base64": "^1.0.1", 29 | "react-otp-input": "^3.0.0", 30 | "react-redux": "^8.0.5", 31 | "react-responsive-masonry": "^2.1.7", 32 | "react-router-dom": "^6.4.2", 33 | "react-scripts": "5.0.1", 34 | "redux": "^4.2.0", 35 | "redux-persist": "^6.0.0", 36 | "styled-components": "^5.3.6", 37 | "timeago.js": "^4.0.2", 38 | "validator": "^13.7.0", 39 | "web-vitals": "^2.1.4" 40 | }, 41 | "scripts": { 42 | "predeploy": "npm run build", 43 | "deploy": "gh-pages -d build", 44 | "start": "react-scripts start", 45 | "build": "react-scripts build", 46 | "test": "react-scripts test", 47 | "eject": "react-scripts eject" 48 | }, 49 | "eslintConfig": { 50 | "extends": [ 51 | "react-app", 52 | "react-app/jest" 53 | ] 54 | }, 55 | "browserslist": { 56 | "production": [ 57 | ">0.2%", 58 | "not dead", 59 | "not op_mini all" 60 | ], 61 | "development": [ 62 | "last 1 chrome version", 63 | "last 1 firefox version", 64 | "last 1 safari version" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /client/src/components/ProjectInvite.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useDispatch } from 'react-redux'; 3 | import { 4 | useParams, 5 | useLocation 6 | } from "react-router-dom"; 7 | import { useNavigate } from 'react-router-dom' 8 | import { useSelector } from "react-redux"; 9 | import { verifyProjectInvite } from '../api'; 10 | import { openSnackbar } from "../redux/snackbarSlice"; 11 | import styled from 'styled-components'; 12 | import { CircularProgress } from '@mui/material'; 13 | 14 | const Joining = styled.div` 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | height: 100vh; 19 | font-size: 2rem; 20 | `; 21 | 22 | const ProjectInvite = () => { 23 | const navigate = useNavigate(); 24 | const dispatch = useDispatch(); 25 | function useQuery() { 26 | const { search } = useLocation(); 27 | 28 | return React.useMemo(() => new URLSearchParams(search), [search]); 29 | } 30 | let query = useQuery(); 31 | 32 | const { code } = useParams(); 33 | const projectid = query.get("projectid"); 34 | const userid = query.get("userid"); 35 | const access = query.get("access"); 36 | const role = query.get("role"); 37 | 38 | 39 | const { currentUser } = useSelector((state) => state.user); 40 | useEffect(() => { 41 | verifyProjectInvite(code, projectid, userid, access, role).then((res) => { 42 | console.log(res); 43 | if (res.status === 200) { 44 | dispatch(openSnackbar({ message: res.data.Message, type: "success" })); 45 | //navigate to project page 46 | if (currentUser) 47 | navigate(`/projects/${projectid}`); 48 | else 49 | navigate(`/`); 50 | 51 | } 52 | else { 53 | navigate(`/`); 54 | } 55 | 56 | } 57 | ).catch((err) => { 58 | console.log(err); 59 | navigate(`/`); 60 | } 61 | ) 62 | }, [projectid, userid, access, role]); 63 | return ( 64 | 65 | 66 | 67 | ) 68 | } 69 | 70 | export default ProjectInvite -------------------------------------------------------------------------------- /client/src/components/MemberCard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { tagColors } from "../data/data"; 4 | import { Avatar } from "@mui/material"; 5 | 6 | const Container = styled.div` 7 | padding: 6px 4px; 8 | text-align: left; 9 | margin: 1px 0px; 10 | font-weight: 500; 11 | cursor: pointer; 12 | display: flex; 13 | align-items: center; 14 | justify-content: space-between; 15 | `; 16 | 17 | const Wrapper = styled.div` 18 | display: flex; 19 | align-items: center; 20 | gap: 12px; 21 | `; 22 | 23 | const Details = styled.div` 24 | gap: 4px; 25 | `; 26 | 27 | const Name = styled.div` 28 | font-size: 13px; 29 | font-weight: 500; 30 | color: ${({ theme }) => theme.textSoft}; 31 | `; 32 | 33 | const EmailId = styled.div` 34 | font-size: 10px; 35 | font-weight: 400; 36 | color: ${({ theme }) => theme.textSoft + "99"}; 37 | `; 38 | 39 | const Role = styled.div` 40 | font-size: 10px; 41 | font-weight: 500; 42 | padding: 4px 8px; 43 | border-radius: 12px; 44 | color: ${({ tagColor, theme }) => tagColor + theme.lightAdd}; 45 | background-color: ${({ tagColor, theme }) => tagColor + "10"}; 46 | `; 47 | 48 | const Access = styled.div` 49 | font-size: 10px; 50 | font-weight: 500; 51 | color: ${({ theme }) => theme.soft2}; 52 | padding: 4px 8px; 53 | border-radius: 12px; 54 | background-color: ${({ theme }) => theme.soft2 + "33"}; 55 | `; 56 | 57 | const MemberCard = ({ member }) => { 58 | return ( 59 | 60 | 61 | {member.id.name.charAt(0)} 62 |
63 | {member.id.name} 64 | {member.id.email} 65 |
66 | 69 | {member.role} 70 | 71 |
72 | {member.access} 73 |
74 | ); 75 | }; 76 | 77 | export default MemberCard; 78 | -------------------------------------------------------------------------------- /client/src/pages/Home/components/TeamMember.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const TeamMemberContainer = styled.div` 5 | width: 350px; 6 | display: flex; 7 | flex-direction: column; 8 | background-color: hsl(250, 24%, 9%); 9 | box-shadow: RGB(252, 3, 111, 0.15) 0px 4px 24px; 10 | border: 1px solid RGB(252, 3, 111,30%); 11 | padding: 24px 24px; 12 | border-radius: 12px; 13 | gap: 8px; 14 | cursor: pointer; 15 | @media (max-width: 925px) { 16 | width: 300px; 17 | } 18 | `; 19 | 20 | const Header = styled.div` 21 | display: flex; 22 | flex-direction: row; 23 | align-items: center; 24 | gap: 12px; 25 | `; 26 | 27 | const TeamMemberData = styled.div` 28 | display: flex; 29 | flex-direction: column; 30 | align-items: start; 31 | justify-content: start; 32 | gap: 4px; 33 | `; 34 | 35 | const TeamMemberPhoto = styled.img` 36 | width: 60px; 37 | height: 60px; 38 | border-radius: 50%; 39 | border: 2px solid #282D45; 40 | object-fit: cover; 41 | `; 42 | 43 | const TeamMemberName = styled.div` 44 | font-size: 18px; 45 | font-weight: bold; 46 | color: ${({ theme }) => theme.text}; 47 | `; 48 | 49 | const TeamMemberTitle = styled.div` 50 | font-size: 14px; 51 | font-weight: 500; 52 | color: ${({ theme }) => theme.soft2 + '99'}; 53 | `; 54 | 55 | const TeamMemberBio = styled.p` 56 | font-size: 16px; 57 | line-height: 1.5; 58 | color: ${({ theme }) => theme.soft2}; 59 | `; 60 | 61 | const TeamMember = ({ photo, name, title, bio }) => { 62 | return ( 63 | 64 |
65 | 66 | 67 | {name} 68 | {title} 69 | 70 |
71 | {bio} 72 |
73 | ); 74 | }; 75 | 76 | export default TeamMember; 77 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 29 | VEXA 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /client/src/components/TeamInvite.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useDispatch } from 'react-redux'; 3 | import { 4 | useParams, 5 | useLocation 6 | } from "react-router-dom"; 7 | import { useNavigate } from 'react-router-dom' 8 | import { useSelector } from "react-redux"; 9 | import { verifyTeamInvite } from '../api'; 10 | import { openSnackbar } from "../redux/snackbarSlice"; 11 | import styled from 'styled-components'; 12 | import { CircularProgress } from '@mui/material'; 13 | 14 | const Joining = styled.div` 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | height: 100vh; 19 | font-size: 2rem; 20 | `; 21 | 22 | const TeamInvite = () => { 23 | 24 | const dispatch = useDispatch(); 25 | const navigate = useNavigate(); 26 | function useQuery() { 27 | const { search } = useLocation(); 28 | 29 | return React.useMemo(() => new URLSearchParams(search), [search]); 30 | } 31 | 32 | let query = useQuery(); 33 | 34 | const { code } = useParams(); 35 | const teamid = query.get("teamid"); 36 | const userid = query.get("userid"); 37 | const access = query.get("access"); 38 | const role = query.get("role"); 39 | 40 | const { currentUser } = useSelector((state) => state.user); 41 | 42 | useEffect(() => { 43 | verifyTeamInvite(code, teamid, userid, access, role).then((res) => { 44 | console.log(res); 45 | if (res.status === 200) { 46 | dispatch(openSnackbar({ message: res.data.Message, type: "success" })); 47 | //navigate to project page 48 | if (currentUser) 49 | navigate(`/teams/${teamid}`); 50 | else 51 | navigate(`/`); 52 | } 53 | else { 54 | navigate(`/`); 55 | } 56 | } 57 | ).catch((err) => { 58 | console.log(err); 59 | navigate(`/`); 60 | } 61 | ) 62 | }, [teamid, userid, access, role]); 63 | 64 | return ( 65 | 66 | 67 | 68 | ) 69 | } 70 | 71 | export default TeamInvite -------------------------------------------------------------------------------- /client/src/pages/Home/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined"; 4 | 5 | const Container = styled.div` 6 | width: 90%; 7 | max-width: 1320px; 8 | height: 60px; 9 | margin: 12px 14px; 10 | display: flex; 11 | align-items: center; 12 | justify-content: space-between; 13 | @media (max-width: 768px) { 14 | padding: 0px 20px !important; 15 | } 16 | `; 17 | const Logo = styled.h1` 18 | font-weight: 600; 19 | font-size: 20px; 20 | color: ${({ theme }) => theme.primary}; 21 | `; 22 | 23 | const Menu = styled.ul` 24 | display: flex; 25 | align-items: center; 26 | gap: 20px; 27 | list-style: none; 28 | @media (max-width: 768px) { 29 | display: none; 30 | } 31 | `; 32 | 33 | const MenuItem = styled.a` 34 | font-size: 16px; 35 | text-decoration: none; 36 | font-weight: 500; 37 | color: ${({ theme }) => theme.text}; 38 | cursor: pointer; 39 | transition: all 0.3s ease; 40 | &:hover { 41 | color: ${({ theme }) => theme.primary}; 42 | } 43 | `; 44 | 45 | 46 | 47 | const Button = styled.button` 48 | padding: 5px 18px; 49 | background-color: transparent; 50 | border: 1px solid ${({ theme }) => theme.primary}; 51 | color: ${({ theme }) => theme.primary}; 52 | border-radius: 3px; 53 | font-weight: 500; 54 | cursor: pointer; 55 | display: flex; 56 | align-items: center; 57 | gap: 5px; 58 | font-size: 15px; 59 | border-radius: 100px; 60 | transition: all 0.3s ease; 61 | &:hover { 62 | background-color: ${({ theme }) => theme.primary}; 63 | color: ${({ theme }) => theme.text}; 64 | } 65 | `; 66 | const Navbar = ({ setSignInOpen }) => { 67 | 68 | return ( 69 | 70 | VEXA 71 | 72 | Home 73 | Features 74 | Benifits 75 | Team 76 | 77 | 80 | 81 | 82 | ) 83 | } 84 | 85 | export default Navbar -------------------------------------------------------------------------------- /client/src/components/ImageSelector.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components'; 3 | import { useState, useEffect } from 'react'; 4 | import ReactImageFileToBase64 from "react-file-image-to-base64"; 5 | import CloudUploadIcon from '@mui/icons-material/CloudUpload'; 6 | 7 | const Container = styled.div` 8 | height: 120px; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | gap: 6px; 13 | align-items: center; 14 | border: 2px dashed ${({ theme }) => theme.soft2+ "80"}}; 15 | border-radius: 12px; 16 | color: ${({ theme }) => theme.soft2+ "80"}; 17 | margin: 30px 20px 0px 20px; 18 | `; 19 | 20 | const Typo = styled.div` 21 | font-size: 14px; 22 | font-weight: 600; 23 | `; 24 | 25 | const TextBtn = styled.div` 26 | font-size: 14px; 27 | font-weight: 600; 28 | color: ${({ theme }) => theme.primary}; 29 | cursor: pointer; 30 | `; 31 | 32 | const Img = styled.img` 33 | height: 120px !important; 34 | width: 100%; 35 | object-fit: cover; 36 | border-radius: 12px; 37 | `; 38 | 39 | const ImageSelector = ({ inputs, setInputs }) => { 40 | const handleOnCompleted = files => { 41 | console.log(files[0].base64_file); 42 | 43 | setInputs((prev) => { 44 | return { ...prev, img: files[0].base64_file }; 45 | }); 46 | console.log(inputs); 47 | }; 48 | 49 | const CustomisedButton = ({ triggerInput }) => { 50 | return ( 51 | 52 | Browse Image 53 | 54 | ); 55 | }; 56 | return ( 57 | 58 | {inputs.img!=="" ? : <> 59 | 60 | Drag & Drop Image here 61 |
62 | or 63 | 68 |
69 | } 70 |
71 | ) 72 | } 73 | 74 | export default ImageSelector -------------------------------------------------------------------------------- /client/src/pages/Home/components/Team.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import TeamMember from "./TeamMember"; 4 | 5 | const TeamWrapper = styled.div`padding: 40px 0; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | padding: 40px 0px 100px 0px; 10 | background-image: linear-gradient(90deg, rgba(0,70,209,0.03) 0%, rgba(57,14,61,0.1) 100%); 11 | clip-path: polygon(0 0, 100% 0, 100% 100%,50% 95%, 0 100%); 12 | `; 13 | 14 | const Title = styled.div` 15 | font-size: 52px; 16 | font-weight: 800; 17 | text-align: center; 18 | @media (max-width: 768px) { 19 | margin-top: 12px; 20 | font-size: 36px; 21 | } 22 | color: ${({ theme }) => theme.text}; 23 | `; 24 | 25 | const Description = styled.p` 26 | font-size: 20px; 27 | line-height: 1.5; 28 | font-weight:600px; 29 | width: 100%; 30 | max-width: 700px; 31 | text-align: center; 32 | color: hsl(246, 6%, 65%); 33 | margin-bottom: 80px; 34 | @media (max-width: 768px) { 35 | width: 90%; 36 | font-size: 16px; 37 | margin-bottom: 60px; 38 | } 39 | `; 40 | 41 | 42 | const TeamContainer = styled.div` 43 | display: flex; 44 | justify-content: center; 45 | flex-wrap: wrap; 46 | width: 100%; 47 | gap: 20px; 48 | max-width: 1200px; 49 | @media (max-width: 768px) { 50 | flex-direction: column; 51 | align-items: center; 52 | } 53 | `; 54 | 55 | const Team = () => { 56 | const member = [{ 57 | photo: "https://avatars.githubusercontent.com/u/64485885?v=4", 58 | name: "Rishav Chanda", 59 | title: "Full-Stack Developer", 60 | bio: "I have expertise in full-stack web development, Android app development, and MERN stack development. I am knowledgeable in various programming languages, frameworks, and technologies and strive to create high-quality, user-friendly applications." 61 | }] 62 | return ( 63 | 64 | Meet the crew 65 | We're a small, remote team working on interesting problems at the edge of compute. 66 | 67 | {member.map((member, index) => ( 68 | 69 | ))} 70 | 71 | 72 | 73 | ); 74 | }; 75 | 76 | export default Team; 77 | -------------------------------------------------------------------------------- /client/src/pages/Home/components/Hero.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import DemoImage from "../../../Images/AddProject.gif" 4 | import HeaderImage from "../../../Images/Header.png" 5 | 6 | const Container = styled.div` 7 | height: 80vh; 8 | margin: 6px 14px; 9 | max-width: 1320px; 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | gap: 80px; 14 | padding: 20px; 15 | @media (max-width: 768px) { 16 | flex-direction: column; 17 | gap: 20px; 18 | padding: 20px 12px 30px 12px!important; 19 | height: 70vh; 20 | } 21 | ` 22 | const Left = styled.div` 23 | flex: 1; 24 | display: flex; 25 | flex-direction: column; 26 | gap: 12px; 27 | ` 28 | 29 | const TitleTag = styled.div` 30 | font-size: 58px; 31 | @media (max-width: 768px) { 32 | font-size: 40px; 33 | } 34 | font-weight: 800; 35 | color: ${({ theme }) => theme.text}; 36 | ` 37 | const DescriptiveTag = styled.p` 38 | font-size: 17px; 39 | @media (max-width: 768px) { 40 | font-size: 15px; 41 | } 42 | font-weight: 300; 43 | margin-bottom: 32px; 44 | line-height: 1.5; 45 | color: ${({ theme }) => theme.soft2}; 46 | ` 47 | const Button = styled.button` 48 | width: 50%; 49 | padding: 16px 20px; 50 | font-size: 20px; 51 | font-weight: 600; 52 | background: linear-gradient(76.35deg, #801AE6 15.89%, #A21AE6 89.75%); 53 | color: ${({ theme }) => theme.text}; 54 | border: none; 55 | border-radius: 10px; 56 | cursor: pointer; 57 | @media (max-width: 1250px) { 58 | width: 80%; 59 | } 60 | @media (max-width: 600px) { 61 | width: 100%; 62 | font-size: 16px; 63 | } 64 | ` 65 | const Image = styled.img` 66 | width: 500px; 67 | height: 500px; 68 | flex: 0.8; 69 | display: flex; 70 | object-fit: scale-down; 71 | border-radius: 10px; 72 | @media (max-width: 1000px) { 73 | display: none; 74 | } 75 | ` 76 | 77 | const Hero = ({ setSignInOpen }) => { 78 | return ( 79 | 80 | 81 | Power Your Projects with Our App. 82 | Take control of your projects and stay on top of your goals with our intuitive project management app. Say goodbye to chaos and hello to streamlined efficiency. Try it now and experience the difference. 83 | 84 | 85 | 86 | 87 | ) 88 | } 89 | 90 | export default Hero -------------------------------------------------------------------------------- /client/src/components/NotificationDialog.jsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Popover } from "@mui/material"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | import { useDispatch } from "react-redux"; 5 | import { logout } from "../redux/userSlice"; 6 | 7 | const Wrapper = styled.div` 8 | width: 100%; 9 | min-width: 300px; 10 | max-width: 400px; 11 | height: 500px; 12 | display: flex; 13 | flex-direction: column; 14 | position: relative; 15 | padding: 6px 2px; 16 | background-color: ${({ theme }) => theme.card}; 17 | `; 18 | 19 | const Heading = styled.div` 20 | font-size: 22px; 21 | font-weight: 500; 22 | color: ${({ theme }) => theme.text}; 23 | margin: 4px 0px 12px 12px; 24 | `; 25 | 26 | const Item = styled.div` 27 | display: flex; 28 | gap: 10px; 29 | padding: 4px 12px 0px 12px; 30 | `; 31 | 32 | const Details = styled.div` 33 | width: 100%; 34 | display: flex; 35 | flex-direction: column; 36 | gap: 4px; 37 | padding: 0px 0px 0px 0px; 38 | `; 39 | 40 | const Title = styled.div` 41 | font-size: 14px; 42 | font-weight: 500; 43 | color: ${({ theme }) => theme.textSoft}; 44 | `; 45 | 46 | const Desc = styled.div` 47 | font-size: 12px; 48 | font-weight: 400; 49 | color: ${({ theme }) => theme.textSoft + "99"}; 50 | `; 51 | 52 | const Hr = styled.hr` 53 | background-color: ${({ theme }) => theme.soft + "99"}; 54 | border: none; 55 | width: 100%; 56 | height: 1px; 57 | margin-top: 4px; 58 | `; 59 | 60 | const NotificationDialog = ({ 61 | open, 62 | id, 63 | anchorEl, 64 | handleClose, 65 | currentUser, 66 | notification, 67 | }) => { 68 | return ( 69 | 80 | 81 | Notifications 82 | 83 | {notification.map((item) => ( 84 | 85 | 89 | {currentUser.name.charAt(0)} 90 | 91 |
92 | {item.type} invitation 93 | {item.message} 94 |
95 |
96 |
97 | ))} 98 | 99 |
100 |
101 | ); 102 | }; 103 | 104 | export default NotificationDialog; 105 | -------------------------------------------------------------------------------- /client/src/pages/Home/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import FacebookIcon from '@mui/icons-material/Facebook'; 3 | import TwitterIcon from '@mui/icons-material/Twitter'; 4 | import LinkedInIcon from '@mui/icons-material/LinkedIn'; 5 | import InstagramIcon from '@mui/icons-material/Instagram'; 6 | 7 | const FooterContainer = styled.footer` 8 | width: 100%; 9 | max-width: 1200px; 10 | display: flex; 11 | flex-direction: column; 12 | gap: 14px; 13 | align-items: center; 14 | padding: 1rem; 15 | color: ${({ theme }) => theme.text}; 16 | `; 17 | 18 | const Logo = styled.h1` 19 | font-weight: 600; 20 | font-size: 20px; 21 | color: ${({ theme }) => theme.primary}; 22 | `; 23 | 24 | const Nav = styled.nav` 25 | width: 100%; 26 | max-width: 800px; 27 | margin-top: 0.5rem; 28 | display: flex; 29 | flex-direction: row; 30 | gap: 2rem; 31 | justify-content: center; 32 | @media (max-width: 768px) { 33 | flex-wrap: wrap; 34 | gap: 1rem; 35 | justify-content: center; 36 | text-align: center; 37 | font-size: 12px; 38 | } 39 | `; 40 | 41 | const NavLink = styled.a` 42 | color: ${({ theme }) => theme.text}; 43 | text-decoration: none; 44 | font-size: 1.2rem; 45 | transition: color 0.2s ease-in-out; 46 | &:hover { 47 | color: ${({ theme }) => theme.primary}; 48 | } 49 | `; 50 | 51 | const SocialMediaIcons = styled.div` 52 | display: flex; 53 | margin-top: 1rem; 54 | `; 55 | 56 | const SocialMediaIcon = styled.a` 57 | display: inline-block; 58 | margin: 0 1rem; 59 | font-size: 1.5rem; 60 | color: ${({ theme }) => theme.text}; 61 | transition: color 0.2s ease-in-out; 62 | &:hover { 63 | color: ${({ theme }) => theme.primary}; 64 | } 65 | `; 66 | 67 | const Copyright = styled.p` 68 | margin-top: 1.5rem; 69 | font-size: 0.9rem; 70 | color: ${({ theme }) => theme.soft2}; 71 | text-align: center; 72 | `; 73 | 74 | function Footer() { 75 | return ( 76 | 77 | VEXA 78 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | © 2023 Vexa. All rights reserved. 92 | 93 | 94 | ); 95 | } 96 | 97 | export default Footer; -------------------------------------------------------------------------------- /client/src/pages/Chats.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import ChatContainer from '../components/ChatContainer' 3 | import ChatContact from '../components/ChatContact' 4 | import styled from 'styled-components' 5 | 6 | const Container = styled.div` 7 | display: flex; 8 | flex-direction: row; 9 | height: 100%; 10 | width: 100%; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | height: 100%; 15 | background-color: ${({ theme }) => theme.bg}; 16 | ` 17 | 18 | const Wrapper = styled.div` 19 | display: flex; 20 | flex-direction: row; 21 | height: 85vh; 22 | width: 100%; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | margin: 12px 0px; 27 | @media (max-width: 800px) { 28 | height: 82vh; 29 | border-radius: 0px; 30 | height: 87vh; 31 | } 32 | ` 33 | 34 | const ChatsContact = styled.div` 35 | margin: 12px 0px; 36 | display: flex; 37 | flex-direction: column; 38 | width: 100%; 39 | max-width: 360px; 40 | height: 100%; 41 | background-color: ${({ theme }) => theme.card}; 42 | border-right: 1px solid ${({ theme }) => theme.soft}; 43 | @media (max-width: 800px) {border-right: 1px solid ${({ theme }) => theme.soft}; 44 | border-right: none; 45 | border-radius: 0px 0px 0px 0px; 46 | } 47 | border-radius: 10px 0px 0px 10px; 48 | ` 49 | 50 | const ChatsContainer = styled.div` 51 | 52 | margin: 12px 0px; 53 | display: flex; 54 | max-width: 800px; 55 | width: 100%; 56 | height: 100%; 57 | flex-direction: column; 58 | background-color: ${({ theme }) => theme.card}; 59 | border-radius: 0px 10px 10px 0px; 60 | ` 61 | 62 | const Chats = () => { 63 | //get the window size and hide the chat container for mobile and dislay it for desktop 64 | const [width, setWidth] = React.useState(window.innerWidth) 65 | const breakpoint = 768 66 | 67 | useEffect(() => { 68 | const handleWindowResize = () => setWidth(window.innerWidth) 69 | window.addEventListener("resize", handleWindowResize) 70 | return () => window.removeEventListener("resize", handleWindowResize) 71 | }, []) 72 | 73 | const [showChat, setShowChat] = React.useState(false) 74 | 75 | return ( 76 | 77 | 78 | {width < breakpoint ? 79 | (showChat ? 80 | 81 | : 82 | ) 83 | : ( 84 | <> 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | )} 93 | 94 | 95 | ) 96 | } 97 | 98 | export default Chats -------------------------------------------------------------------------------- /client/src/pages/Home/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import Navbar from './components/Navbar' 4 | import Hero from './components/Hero' 5 | import Footer from './components/Footer' 6 | import Features from './components/Features' 7 | import Testimonials from './components/Testimonials' 8 | import Team from './components/Team' 9 | import Benefits from './components/Benifits' 10 | import About from './components/About' 11 | import SignUp from '../../components/SignUp' 12 | import SignIn from '../../components/SignIn' 13 | import Faq from './components/Faq' 14 | 15 | const Body = styled.div` 16 | background: #13111C; 17 | display: flex; 18 | justify-content: center; 19 | overflow-x: hidden; 20 | ` 21 | 22 | const Container = styled.div` 23 | width: 100%; 24 | background-Image: linear-gradient(38.73deg, rgba(204, 0, 187, 0.25) 0%, rgba(201, 32, 184, 0) 50%), linear-gradient(141.27deg, rgba(0, 70, 209, 0) 50%, rgba(0, 70, 209, 0.25) 100%); 25 | display: flex; 26 | flex-direction: column; 27 | align-items: center; 28 | justify-content: center; 29 | ` 30 | 31 | const Top = styled.div` 32 | width: 100%; 33 | display: flex; 34 | padding-bottom: 50px; 35 | flex-direction: column; 36 | align-items: center; 37 | background: linear-gradient(38.73deg, rgba(204, 0, 187, 0.15) 0%, rgba(201, 32, 184, 0) 50%), linear-gradient(141.27deg, rgba(0, 70, 209, 0) 50%, rgba(0, 70, 209, 0.15) 100%); 38 | clip-path: polygon(0 0, 100% 0, 100% 100%,50% 95%, 0 100%); 39 | @media (max-width: 768px) { 40 | clip-path: polygon(0 0, 100% 0, 100% 100%,50% 98%, 0 100%); 41 | padding-bottom: 0px; 42 | } 43 | `; 44 | const Content = styled.div` 45 | width: 100%; 46 | height: 100%; 47 | background: #13111C; 48 | display: flex; 49 | flex-direction: column; 50 | ` 51 | 52 | const Home = () => { 53 | const [SignInOpen, setSignInOpen] = React.useState(false); 54 | const [SignUpOpen, setSignUpOpen] = React.useState(false); 55 | 56 | return ( 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | {/* */} 66 | 67 | {/* */} 68 | 69 |
70 |
71 |
72 |
73 | {SignUpOpen && ( 74 | 75 | )} 76 | {SignInOpen && ( 77 | 78 | )} 79 |
80 | 81 | ) 82 | } 83 | 84 | export default Home -------------------------------------------------------------------------------- /client/src/components/AccountDialog.jsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Popover } from "@mui/material"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | import { useDispatch } from "react-redux"; 5 | import { logout } from "../redux/userSlice"; 6 | import {useNavigate} from 'react-router-dom'; 7 | 8 | const Wrapper = styled.div` 9 | min-width: 200px; 10 | display: flex; 11 | flex-direction: column; 12 | position: relative; 13 | gap: 12px; 14 | padding: 6px 2px; 15 | background-color: ${({ theme }) => theme.card}; 16 | `; 17 | 18 | const Account = styled.div` 19 | display: flex; 20 | align-items: center; 21 | gap: 16px; 22 | padding: 16px 16px 6px 16px; 23 | `; 24 | 25 | const Details = styled.div` 26 | display: flex; 27 | flex-direction: column; 28 | gap: 4px; 29 | padding: 0px 16px 0px 0px; 30 | `; 31 | 32 | const Name = styled.div` 33 | font-size: 17px; 34 | font-weight: 600; 35 | color: ${({ theme }) => theme.textSoft}; 36 | `; 37 | 38 | const Email = styled.div` 39 | font-size: 12px; 40 | font-weight: 400; 41 | color: ${({ theme }) => theme.textSoft + "99"}; 42 | `; 43 | 44 | const Hr = styled.hr` 45 | background-color: ${({ theme }) => theme.soft + "99"}; 46 | border: none; 47 | width: 100%; 48 | height: 1px; 49 | margin: 0; 50 | `; 51 | 52 | const Logout = styled.div` 53 | padding: 0px 0px 12px 0px; 54 | display: flex; 55 | align-items: center; 56 | justify-content: center;; 57 | font-size: 16px; 58 | font-weight: 500; 59 | color: ${({ theme }) => theme.textSoft}; 60 | cursor: pointer; 61 | `; 62 | 63 | const OutlinedBox = styled.div` 64 | border-radius: 6px; 65 | padding: 4px 16px; 66 | border: 1px solid ${({ theme }) => theme.soft2+"99"}; 67 | color: ${({ theme }) => theme.soft2+"99"}; 68 | font-size: 12px; 69 | &:hover { 70 | background-color: ${({ theme }) => theme.soft2 + "33"}; 71 | } 72 | `; 73 | 74 | const AccountDialog = ({ open, id, anchorEl, handleClose, currentUser }) => { 75 | const dispatch = useDispatch() 76 | const navigate = useNavigate() 77 | const logoutUser = () => { 78 | dispatch(logout()) 79 | navigate('/'); 80 | } 81 | return ( 82 | 93 | 94 | 95 | {currentUser.name.charAt(0)} 99 |
100 | {currentUser.name} 101 | {currentUser.email} 102 |
103 |
104 |
105 | 106 | Logout 107 | 108 |
109 |
110 | ); 111 | }; 112 | 113 | export default AccountDialog; 114 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /client/src/components/ProjectStatCard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import LinearProgress from "@mui/material/LinearProgress"; 4 | 5 | const Container = styled.div` 6 | padding: 12px 16px; 7 | border-radius: 12px; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: space-between; 11 | gap: 6px; 12 | background-color: ${({ theme }) => theme.card}; 13 | &:hover { 14 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); 15 | } 16 | `; 17 | 18 | const Middle = styled.div` 19 | display: flex; 20 | flex-direction: column; 21 | gap: 6px; 22 | `; 23 | 24 | const Flex = styled.div` 25 | display: flex; 26 | flex-direction: row; 27 | justify-content: space-between; 28 | gap: 30px; 29 | `; 30 | 31 | const Title = styled.div` 32 | font-size: 14px; 33 | font-weight: 500; 34 | color: ${({ theme }) => theme.textSoft}; 35 | `; 36 | 37 | const Desc = styled.div` 38 | font-size: 11px; 39 | font-weight: 500; 40 | color: ${({ theme }) => theme.textSoft + "99"}; 41 | `; 42 | 43 | const Tags = styled.div` 44 | display: flex; 45 | flex-wrap: wrap; 46 | flex-direction: row; 47 | gap: 4px; 48 | margin-top: 4px; 49 | `; 50 | 51 | const Tag = styled.div` 52 | padding: 4px 10px; 53 | border-radius: 8px; 54 | color: ${({ tagColor, theme }) => tagColor + theme.lightAdd}; 55 | background-color: ${({ tagColor, theme }) => tagColor + "10"}; 56 | font-size: 10px; 57 | font-weight: 500; 58 | `; 59 | 60 | const TimeLeft = styled.div` 61 | font-size: 10px; 62 | font-weight: 500; 63 | color: #20B2AA; 64 | padding: 4px 8px; 65 | border-radius: 12px; 66 | background-color: #20B2AA10}; 67 | width: fit-content; 68 | height: fit-content; 69 | `; 70 | 71 | const Progress = styled.div` 72 | margin: 4px 0px 4px 0px; 73 | `; 74 | 75 | const Text = styled.div` 76 | display: flex; 77 | align-items: center; 78 | font-size: 11px; 79 | font-weight: 400; 80 | color: ${({ theme }) => theme.soft2}; 81 | margin: 0px 0px 6px 0px; 82 | line-height: 1.5; 83 | overflow: hidden; 84 | gap: 8px; 85 | `; 86 | 87 | const Span = styled.span` 88 | font-size: 12px; 89 | font-weight: 600; 90 | color: ${({ theme }) => theme.soft2}; 91 | line-height: 1.5; 92 | `; 93 | 94 | const AvatarGroup = styled.div` 95 | display: flex; 96 | align-items: center; 97 | `; 98 | 99 | const Avatar = styled.img` 100 | width: 28px; 101 | height: 28px; 102 | border-radius: 50%; 103 | margin-right: -12px; 104 | border: 3px solid ${({ theme }) => theme.bgLighter}; 105 | `; 106 | 107 | const Hr = styled.hr` 108 | margin: 2px 0px; 109 | border: 0.5px solid ${({ theme }) => theme.soft + "99"}; 110 | `; 111 | 112 | const ProjectStatCard = () => { 113 | return ( 114 | 115 | Project Card 116 | Lorem ipsum dolor sit amet consectetur adipisicing eli jbj 117 | 118 | React 119 | Tag 2 120 | 121 | 122 | 123 | Task Done :2/10 124 | 125 | 131 | 132 |
133 | 134 | 135 | 136 | 137 | 138 | 12 days left 139 | 140 |
141 | ); 142 | }; 143 | 144 | export default ProjectStatCard; 145 | -------------------------------------------------------------------------------- /client/src/components/TaskCard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Avatar } from "@mui/material"; 4 | 5 | const Card = styled.div` 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | padding: 6px 10px; 10 | gap: 8px; 11 | border-left: 1.8px solid ${({ theme }) => theme.soft + "99"}; 12 | border-right: 1.8px solid ${({ theme }) => theme.soft + "99"}; 13 | border-bottom: 1.8px solid ${({ theme }) => theme.soft + "99"}; 14 | background-color: ${({ theme }) => theme.card}; 15 | color: ${({ theme }) => theme.text}; 16 | cursor: pointer; 17 | &:hover { 18 | transition: all 0.2s ease-in-out; 19 | background-color: ${({ theme }) => theme.bgDark + "40"}; 20 | } 21 | 22 | ${({ completed, theme }) => 23 | completed === "Completed" && 24 | ` 25 | background-color: ${theme.soft + "30"}; 26 | `} 27 | `; 28 | 29 | const No = styled.div` 30 | width: 4%; 31 | font-size: 12px; 32 | text-overflow: ellipsis; 33 | font-weight: 500; 34 | color: ${({ theme }) => theme.soft2}; 35 | display: -webkit-box; 36 | -webkit-line-clamp: 5; /* number of lines to show */ 37 | line-clamp: 5; 38 | -webkit-box-orient: vertical; 39 | 40 | ${({ completed, theme }) => 41 | completed === "Completed" && 42 | ` 43 | text-decoration: line-through; 44 | `} 45 | `; 46 | 47 | const Task = styled.div` 48 | width: 50%; 49 | font-size: 12px; 50 | font-weight: 500; 51 | color: ${({ theme }) => theme.soft2}; 52 | display: -webkit-box; 53 | -webkit-line-clamp: 5; /* number of lines to show */ 54 | line-clamp: 5; 55 | -webkit-box-orient: vertical; 56 | padding: 6px; 57 | 58 | ${({ completed, theme }) => 59 | completed === "Completed" && 60 | ` 61 | text-decoration: line-through; 62 | `} 63 | `; 64 | 65 | const Date = styled.div` 66 | font-size: 12px; 67 | font-weight: 500; 68 | text-align: center; 69 | text-overflow: ellipsis; 70 | width: 14%; 71 | color: ${({ theme }) => theme.soft2}; 72 | ${({ enddate, theme }) => 73 | enddate && 74 | ` 75 | color: ${theme.pink}; 76 | `} 77 | display: -webkit-box; 78 | -webkit-line-clamp: 5; /* number of lines to show */ 79 | line-clamp: 5; 80 | -webkit-box-orient: vertical; 81 | 82 | ${({ completed, theme }) => 83 | completed === "Completed" && 84 | ` 85 | text-decoration: line-through; 86 | `} 87 | `; 88 | 89 | const Status = styled.div` 90 | font-size: 12px; 91 | font-weight: 500; 92 | text-align: center; 93 | width: 10%; 94 | color: ${({ theme }) => theme.yellow}; 95 | padding: 4px 8px; 96 | background: ${({ theme }) => theme.yellow + "10"}; 97 | border-radius: 8px; 98 | 99 | ${({ completed, theme }) => 100 | completed === "Completed" && 101 | ` 102 | color: ${theme.green}; 103 | background: ${theme.green + "10"}; 104 | `} 105 | `; 106 | 107 | const Members = styled.div` 108 | display: flex; 109 | flex-direction: row; 110 | align-items: center; 111 | `; 112 | 113 | const TaskCard = ({item,index,members}) => { 114 | return ( 115 | 116 | {index + 1}. 117 | {item.task} 118 | 119 | {item.start_date.split("-").reverse().join("-")} 120 | 121 | 122 | {item.end_date.split("-").reverse().join("-")} 123 | 124 | {item.status} 125 | 131 | {item.members.slice(0, 5).map((member) => ( 132 | 141 | {member.name.charAt(0)} 142 | 143 | ))} 144 | 145 | {item.members.length > 5 && ( 146 | 154 | +{members.length - 9} 155 | 156 | )} 157 | 158 | 159 | ); 160 | }; 161 | 162 | export default TaskCard; 163 | -------------------------------------------------------------------------------- /client/public/Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/src/Images/Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/src/pages/Projects.jsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import React from "react"; 3 | import { useState, useEffect } from "react"; 4 | import styled from "styled-components"; 5 | import Item from "../components/Card"; 6 | import { statuses, data, tagColors } from "../data/data"; 7 | import { useDispatch } from "react-redux"; 8 | import { openSnackbar } from "../redux/snackbarSlice"; 9 | import { useSelector } from "react-redux"; 10 | import Skeleton from "@mui/material/Skeleton"; 11 | import { useCookies } from "react-cookie"; 12 | import { getProjects } from "../api/index"; 13 | import AddNewProject from "../components/AddNewProject"; 14 | import { CircularProgress } from "@mui/material"; 15 | 16 | const Container = styled.div` 17 | width: 100%; 18 | `; 19 | 20 | const Column = styled.div` 21 | display: flex; 22 | flex-direction: row; 23 | @media screen and (max-width: 480px) { 24 | flex-direction: column; 25 | } 26 | justify-content: space-between; 27 | margin: 12px 0px; 28 | `; 29 | const ItemWrapper = styled.div` 30 | width: 100%; 31 | height: 100%; 32 | @media screen and (max-width: 480px) { 33 | width: 97%; 34 | } 35 | padding: 4px; 36 | text-align: left; 37 | margin: 2px; 38 | font-size: 16px; 39 | font-weight: 500; 40 | color: ${({ theme }) => theme.text}; 41 | `; 42 | 43 | const Span = styled.span` 44 | color: ${({ theme }) => theme.soft2}; 45 | font-weight: 400; 46 | margin-left: 8px; 47 | `; 48 | 49 | const Wrapper = styled.div` 50 | padding: 12px 6px; 51 | `; 52 | 53 | const OutlinedBox = styled.div` 54 | min-height: 44px; 55 | border-radius: 8px; 56 | border: 1px solid ${({ theme }) => theme.soft2}; 57 | color: ${({ theme }) => theme.soft2}; 58 | ${({ googleButton, theme }) => 59 | googleButton && 60 | ` 61 | user-select: none; 62 | gap: 16px;`} 63 | ${({ button, theme }) => 64 | button && 65 | ` 66 | user-select: none; 67 | border: none; 68 | font-weight: 600; 69 | font-size: 16px; 70 | background: ${theme.card}; `} 71 | ${({ activeButton, theme }) => 72 | activeButton && 73 | ` 74 | user-select: none; 75 | border: none; 76 | background: ${theme.primary}; 77 | color: white;`} 78 | margin-top: 8px; 79 | font-weight: 600; 80 | font-size: 16px; 81 | display: flex; 82 | justify-content: center; 83 | align-items: center; 84 | padding: 0px 14px; 85 | &:hover { 86 | transition: all 0.6s ease-in-out; 87 | background: ${({ theme }) => theme.soft}; 88 | color: white; 89 | } 90 | `; 91 | 92 | const Projects = ({newProject,setNewProject}) => { 93 | const dispatch = useDispatch(); 94 | const [data, setData] = useState([]); 95 | const [loading, setLoading] = useState(true); 96 | const { currentUser } = useSelector((state) => state.user); 97 | 98 | 99 | const token = localStorage.getItem("token"); 100 | console.log(token) 101 | const getprojects = async () => { 102 | await getProjects(token) 103 | .then((res) => { 104 | setData(res.data); 105 | setLoading(false); 106 | }) 107 | .catch((err) => { 108 | setLoading(false); 109 | dispatch( 110 | openSnackbar({ 111 | message: err.response.data.message, 112 | severity: "error", 113 | }) 114 | ); 115 | }); 116 | }; 117 | 118 | useEffect(() => { 119 | getprojects(); 120 | window.scrollTo(0, 0); 121 | }, [newProject, currentUser]); 122 | 123 | 124 | return ( 125 | 126 | {loading ? ( 127 |
128 | 129 |
130 | ) : ( 131 | 132 | {statuses.map((s, index) => { 133 | return ( 134 | 135 | {s.icon} {s.status} 136 | 137 | ({data.filter((item) => item.status == s.status).length}) 138 | 139 | 140 | {s.status === "Working" && ( 141 | setNewProject(true)}> 142 | New Project 143 | 144 | )} 145 | {data 146 | .filter((item) => item.status == s.status) 147 | .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) 148 | .map((item, idx) => ( 149 | 156 | ))} 157 | 158 | 159 | ); 160 | })} 161 | 162 | )} 163 |
164 | ); 165 | }; 166 | 167 | export default Projects; 168 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import logo from './logo.svg'; 2 | import { ThemeProvider } from "styled-components"; 3 | import { useState } from "react"; 4 | import { darkTheme, lightTheme } from "./utils/Theme"; 5 | import { 6 | BrowserRouter, 7 | Routes, 8 | Route, 9 | } from "react-router-dom" 10 | import Menu from './components/Menu'; 11 | import Navbar from './components/Navbar'; 12 | import styled from 'styled-components'; 13 | import Dashboard from './pages/Dashboard'; 14 | import Works from './pages/Works'; 15 | import Projects from './pages/Projects'; 16 | import { DndProvider } from "react-dnd"; 17 | import { HTML5Backend } from "react-dnd-html5-backend"; 18 | import ProjectDetails from './pages/ProjectDetails'; 19 | import Teams from './pages/Teams'; 20 | import ToastMessage from './components/ToastMessage'; 21 | import Community from './pages/Community'; 22 | import { useSelector } from "react-redux"; 23 | import AddNewTeam from './components/AddNewTeam'; 24 | import { useEffect } from 'react'; 25 | import { getUsers } from './api'; 26 | import { useDispatch } from 'react-redux'; 27 | import Home from './pages/Home/Home'; 28 | import Chats from './pages/Chats'; 29 | import ProjectInvite from './components/ProjectInvite'; 30 | import TeamInvite from './components/TeamInvite'; 31 | import AddNewProject from './components/AddNewProject'; 32 | 33 | const Container = styled.div` 34 | height: 100vh; 35 | display: flex; 36 | background-color: ${({ theme }) => theme.bg}; 37 | color: ${({ theme }) => theme.text}; 38 | overflow-x: hidden; 39 | `; 40 | 41 | const Main = styled.div` 42 | flex: 7; 43 | `; 44 | const Wrapper = styled.div` 45 | padding: 0% 1%; 46 | overflow-y: scroll !important; 47 | `; 48 | 49 | function App() { 50 | const [darkMode, setDarkMode] = useState(true); 51 | const [menuOpen, setMenuOpen] = useState(true); 52 | const [newTeam, setNewTeam] = useState(false); 53 | const [newProject, setNewProject] = useState(false); 54 | const { open, message, severity } = useSelector((state) => state.snackbar); 55 | const [loading, setLoading] = useState(false); 56 | 57 | 58 | const { currentUser } = useSelector(state => state.user); 59 | 60 | 61 | //set the menuOpen state to false if the screen size is less than 768px 62 | useEffect(() => { 63 | const resize = () => { 64 | if (window.innerWidth < 1110) { 65 | setMenuOpen(false); 66 | } else { 67 | setMenuOpen(true); 68 | } 69 | } 70 | resize(); 71 | window.addEventListener("resize", resize); 72 | return () => window.removeEventListener("resize", resize); 73 | }, []); 74 | 75 | return ( 76 | 77 | 78 | 79 | 80 | {currentUser ? 81 | 82 | {loading ?
Loading...
: <> 83 | {menuOpen && } 84 |
85 | 86 | 87 | {newTeam && } 88 | {newProject && } 89 | 90 | 91 | } /> 92 | } /> 93 | 94 | } /> 95 | 96 | 97 | } /> 98 | 99 | 100 | } /> 101 | 102 | 103 | } /> 104 | 105 | 106 | } /> 107 | } /> 108 | } /> 109 | Not Found} /> 110 | 111 | 112 | 113 |
114 | } 115 | 116 | : 118 | 119 | 120 | 121 | } /> 123 | 124 | } /> 125 | 126 | 127 | } /> 128 | 129 | 130 | 131 | } 132 | {open && } 133 | 134 | 135 | 136 | 137 | ); 138 | } 139 | 140 | export default App; 141 | -------------------------------------------------------------------------------- /client/src/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import jwt_decode from 'jwt-decode'; 3 | //https://vexa-server.herokuapp.com/api 4 | //http://localhost:8800/api/ 5 | //https://dull-blue-dolphin-tutu.cyclic.app 6 | //https://project-management-app-production-3d51.up.railway.app/api/ 7 | //http://localhost:8700/api/ 8 | //https://vexa.onrender.com/ 9 | const API = axios.create({ baseURL: `https://vexa.onrender.com/api/` }); 10 | 11 | 12 | 13 | //auth 14 | export const signIn = async ({ email, password }) => await API.post('/auth/signin', { email, password }); 15 | export const signUp = async ({ 16 | name, 17 | email, 18 | password, 19 | }) => await API.post('/auth/signup', { 20 | name, 21 | email, 22 | password, 23 | }); 24 | export const googleSignIn = async ({ 25 | name, 26 | email, 27 | img, 28 | }) => await API.post('/auth/google', { 29 | name, 30 | email, 31 | img, 32 | },{ withCredentials: true }); 33 | export const findUserByEmail = async (email) => await API.get(`/auth/findbyemail?email=${email}`); 34 | export const generateOtp = async (email,name,reason) => await API.get(`/auth/generateotp?email=${email}&name=${name}&reason=${reason}`); 35 | export const verifyOtp = async (otp) => await API.get(`/auth/verifyotp?code=${otp}`); 36 | export const resetPassword = async (email,password) => await API.put(`/auth/forgetpassword`,{email,password}); 37 | 38 | //user api 39 | export const getUsers = async (token) => await API.get('/users/find', { headers: { "Authorization" : `Bearer ${token}` }},{ 40 | withCredentials: true 41 | }); 42 | export const searchUsers = async (search,token) => await API.get(`users/search/${search}`,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 43 | export const notifications = async (token) => await API.get('/users/notifications',{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 44 | export const getProjects = async (token) => await API.get(`/users/projects`, { headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 45 | export const userWorks = async (token) => await API.get('/users/works',{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 46 | export const userTasks = async (token) => await API.get('/users/tasks',{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 47 | 48 | //projects api 49 | export const createProject = async (project,token) => await API.post('project/', project,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 50 | export const getProjectDetails = async (id,token) => await API.get(`/project/${id}`,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 51 | export const inviteProjectMembers = async (id, members,token) => await API.post(`/project/invite/${id}`, members,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 52 | export const addWorks = async (id, works,token) => await API.post(`/project/works/${id}`, works,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 53 | export const getWorks = async (id,token) => await API.get(`/project/works/${id}`,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 54 | export const verifyProjectInvite = async (code,projectid,userid,access,role) => await API.get(`/project/invite/${code}?projectid=${projectid}&userid=${userid}&access=${access}&role=${role}`,{ withCredentials: true }); 55 | export const updateProject = async (id, project,token) => await API.patch(`/project/${id}`, project,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 56 | export const deleteProject = async (id,token) => await API.delete(`/project/${id}`,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 57 | export const updateMembers = async (id, members,token) => await API.patch(`/project/member/${id}`, members,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 58 | export const removeMembers = async (id, members,token) => await API.patch(`/project/member/remove/${id}`, members,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 59 | 60 | 61 | //teams api 62 | export const createTeam = async (team,token) => await API.post('team/', team,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 63 | export const getTeams = async (id,token) => await API.get(`/team/${id}`,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 64 | export const inviteTeamMembers = async (id, members,token) => await API.post(`/team/invite/${id}`, members,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 65 | export const addTeamProject = async (id, project,token) => await API.post(`/team/addProject/${id}`, project,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 66 | export const verifyTeamInvite = async (code,teamid,userid,access,role) => await API.get(`/team/invite/${code}?teamid=${teamid}&userid=${userid}&access=${access}&role=${role}`,{ withCredentials: true }); 67 | export const updateTeam = async (id, team,token) => await API.patch(`/team/${id}`, team,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 68 | export const deleteTeam = async (id,token) => await API.delete(`/team/${id}`,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 69 | export const updateTeamMembers = async (id, members,token) => await API.patch(`/team/member/${id}`, members,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 70 | export const removeTeamMembers = async (id, members,token) => await API.patch(`/team/member/remove/${id}`, members,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true }); 71 | -------------------------------------------------------------------------------- /client/src/components/Card.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Fragment, useState, useRef } from "react"; 3 | import styled from "styled-components"; 4 | import { MoreHoriz, TimelapseRounded } from "@mui/icons-material"; 5 | import { LinearProgress } from "@mui/material"; 6 | import { useDrag, useDrop } from "react-dnd"; 7 | import ITEM_TYPE from "../data/types"; 8 | import { tagColors } from "../data/data"; 9 | import { Link } from "react-router-dom"; 10 | import axios from "axios"; 11 | import Avatar from "@mui/material/Avatar"; 12 | import {format} from 'timeago.js'; 13 | 14 | const Container = styled.div` 15 | padding: 14px 14px; 16 | text-align: left; 17 | margin: 12px 0px 8px 0px; 18 | font-size: 14px; 19 | font-weight: 500; 20 | border-radius: 10px; 21 | background-color: ${({ theme }) => theme.card}; 22 | color: ${({ theme }) => theme.text}; 23 | cursor: pointer; 24 | box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.09); 25 | &:hover { 26 | transition: all 0.6s ease-in-out; 27 | box-shadow: 0 0 18px 0 rgba(0, 0, 0, 0.5); 28 | } 29 | `; 30 | 31 | const Image = styled.img` 32 | height: 120px; 33 | width: 100%; 34 | object-fit: cover; 35 | border-radius: 10px; 36 | margin-top: 1px; 37 | margin-bottom: 8px; 38 | `; 39 | 40 | const Top = styled.div` 41 | display: flex; 42 | justify-content: space-between; 43 | align-items: center; 44 | `; 45 | 46 | const Title = styled.div` 47 | font-size: 15px; 48 | font-weight: 500; 49 | color: ${({ theme }) => theme.textSoft}; 50 | margin-top: 6px; 51 | flex: 7; 52 | line-height: 1.5; 53 | overflow: hidden; 54 | text-overflow: ellipsis; 55 | display: -webkit-box; 56 | -webkit-line-clamp: 2; /* number of lines to show */ 57 | line-clamp: 2; 58 | -webkit-box-orient: vertical; 59 | `; 60 | 61 | const Desc = styled.div` 62 | font-size: 12px; 63 | font-weight: 400; 64 | color: ${({ theme }) => theme.soft2}; 65 | margin-top: 8px; 66 | line-height: 1.5; 67 | overflow: hidden; 68 | text-overflow: ellipsis; 69 | display: -webkit-box; 70 | -webkit-line-clamp: 5; /* number of lines to show */ 71 | line-clamp: 5; 72 | -webkit-box-orient: vertical; 73 | `; 74 | 75 | 76 | const Tags = styled.div` 77 | display: flex; 78 | flex-wrap: wrap; 79 | flex-direction: row; 80 | gap: 6px; 81 | margin-top: 8px; 82 | `; 83 | 84 | const Tag = styled.div` 85 | padding: 4px 10px; 86 | border-radius: 8px; 87 | color: ${({ tagColor,theme }) => tagColor + theme.lightAdd}; 88 | background-color: ${({ tagColor, theme }) => tagColor + "10"}; 89 | font-size: 10px; 90 | font-weight: 500; 91 | `; 92 | 93 | const Bottom = styled.div` 94 | display: flex; 95 | justify-content: space-between; 96 | align-items: center; 97 | margin: 10px 0px; 98 | `; 99 | 100 | const Time = styled.div` 101 | display: flex; 102 | align-items: center; 103 | gap: 8px; 104 | font-size: 12px; 105 | font-weight: 500; 106 | color: ${({ theme }) => theme.soft2 + "99"}; 107 | `; 108 | 109 | const AvatarGroup = styled.div` 110 | display: flex; 111 | align-items: center; 112 | margin-right: 12px; 113 | `; 114 | 115 | 116 | const Card = ({ tagColor, item, index, status }) => { 117 | const ref = useRef(null); 118 | 119 | /*const [, drop] = useDrop({ 120 | accept: ITEM_TYPE, 121 | hover(item, monitor) { 122 | if (!ref.current) { 123 | return 124 | } 125 | const dragIndex = item.index; 126 | const hoverIndex = index; 127 | 128 | if (dragIndex === hoverIndex) { 129 | return 130 | } 131 | 132 | const hoveredRect = ref.current.getBoundingClientRect(); 133 | const hoverMiddleY = (hoveredRect.bottom - hoveredRect.top) / 2; 134 | const mousePosition = monitor.getClientOffset(); 135 | const hoverClientY = mousePosition.y - hoveredRect.top; 136 | 137 | if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { 138 | return; 139 | } 140 | 141 | if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { 142 | return; 143 | } 144 | moveItem(dragIndex, hoverIndex); 145 | item.index = hoverIndex; 146 | }, 147 | }); 148 | 149 | const [{ isDragging }, drag] = useDrag({ 150 | item: { type: ITEM_TYPE, ...item, index }, 151 | collect: monitor => ({ 152 | isDragging: monitor.isDragging() 153 | }) 154 | });*/ 155 | 156 | //drag(drop(ref)); 157 | 158 | return ( 159 | 160 | 161 | 162 | {item.img && } 163 | 164 | {item.title} 165 | 166 | {item.desc} 167 | 168 | {item.tags.map((tag) => ( 169 | 174 | {tag} 175 | 176 | ))} 177 | 178 | 179 | 182 | 183 | {item.members.map((member) => ( 184 | {member.id.name.charAt(0)} 185 | ))} 186 | 187 | 188 | 189 | 190 | 191 | ); 192 | }; 193 | 194 | export default Card; 195 | -------------------------------------------------------------------------------- /client/src/components/DeletePopup.jsx: -------------------------------------------------------------------------------- 1 | import { CloseRounded } from '@mui/icons-material'; 2 | import { CircularProgress, Modal } from '@mui/material'; 3 | import React, { useEffect } from 'react' 4 | import { useNavigate } from 'react-router-dom'; 5 | import styled from 'styled-components' 6 | import { deleteProject, deleteTeam } from '../api'; 7 | import { useDispatch } from 'react-redux'; 8 | import { openSnackbar } from '../redux/snackbarSlice'; 9 | 10 | 11 | const Container = styled.div` 12 | width: 100%; 13 | height: 100%; 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | background-color: #000000a7; 18 | display: flex; 19 | justify-content: center; 20 | `; 21 | 22 | const Wrapper = styled.div` 23 | width: 430px; 24 | height: min-content; 25 | border-radius: 16px; 26 | background-color: ${({ theme }) => theme.bgLighter}; 27 | color: ${({ theme }) => theme.text}; 28 | padding: 10px; 29 | display: flex; 30 | margin-top: 80px; 31 | flex-direction: column; 32 | position: relative; 33 | @media (max-width: 768px) { 34 | width: 100%; 35 | } 36 | `; 37 | 38 | const Heading = styled.div` 39 | font-size: 20px; 40 | font-weight: 500; 41 | color: ${({ theme }) => theme.text}; 42 | margin: 12px 12px 0 12px; 43 | `; 44 | 45 | const Text = styled.div` 46 | font-size: 14px; 47 | font-weight: 400; 48 | color: ${({ theme }) => theme.soft2}; 49 | margin: 12px ; 50 | line-height: 1.5; 51 | `; 52 | 53 | const Input = styled.input` 54 | border: none; 55 | font-size: 14px; 56 | padding: 14px 20px; 57 | margin: 12px; 58 | border-radius: 12px; 59 | color: ${({ theme }) => theme.textSoft}; 60 | background-color: ${({ theme }) => theme.bgDark}; 61 | &:focus { 62 | outline: 1px solid ${({ theme }) => theme.primary}; 63 | } 64 | `; 65 | 66 | const Button = styled.button` 67 | border: none; 68 | font-size: 14px; 69 | padding: 14px 20px; 70 | margin: 0px 12px 12px 12px; 71 | font-size: 14px; 72 | border-radius: 12px; 73 | font-weight: 600; 74 | color: ${({ theme }) => theme.text}; 75 | background: ${({ theme }) => theme.primary}; 76 | cursor: pointer; 77 | ${({ disabled, theme }) => 78 | disabled && 79 | ` 80 | background: ${theme.soft}; 81 | color: ${theme.soft2}; 82 | cursor: not-allowed; 83 | ` 84 | } 85 | `; 86 | 87 | 88 | const DeletePopup = ({ openDelete, setOpenDelete }) => { 89 | 90 | const [name, setName] = React.useState(''); 91 | const [loading, setLoading] = React.useState(false); 92 | const [disabled, setDisabled] = React.useState(true); 93 | const navigate = useNavigate(); 94 | const dispatch = useDispatch(); 95 | 96 | useEffect(() => { 97 | if (name === openDelete.name) { 98 | setDisabled(false); 99 | } else { 100 | setDisabled(true); 101 | } 102 | }, [name, openDelete.name]); 103 | 104 | const handleDelete = () => { 105 | setLoading(true); 106 | setDisabled(true); 107 | if (openDelete.type === "Project") { 108 | DeleteProject(); 109 | } else if (openDelete.type === "Team") { 110 | DeleteTeam(); 111 | } else if (openDelete.type === "Work") { 112 | deleteWork(); 113 | } 114 | 115 | } 116 | 117 | const DeleteProject = async () => { 118 | await deleteProject(openDelete.id, openDelete.token) 119 | .then((res) => { 120 | console.log(res); 121 | dispatch(openSnackbar 122 | ({ 123 | message: "Project deleted successfully", 124 | type: "success", 125 | })); 126 | 127 | handleDeleteSuccess("/projects"); 128 | }) 129 | .catch((err) => { 130 | dispatch(openSnackbar 131 | ({ 132 | message: err.message, 133 | type: "error", 134 | })); 135 | }) 136 | } 137 | 138 | const DeleteTeam = async () => { 139 | await deleteTeam(openDelete.id, openDelete.token) 140 | .then((res) => { 141 | console.log(res); 142 | dispatch(openSnackbar 143 | ({ 144 | message: "Team deleted successfully", 145 | type: "success", 146 | })); 147 | 148 | handleDeleteSuccess("/"); 149 | } 150 | ).catch((err) => { 151 | dispatch(openSnackbar 152 | ({ 153 | message: err.message, 154 | type: "error", 155 | })); 156 | } 157 | ) 158 | } 159 | 160 | const deleteWork = () => { 161 | } 162 | 163 | 164 | const handleDeleteSuccess = (link) => { 165 | setLoading(false); 166 | setOpenDelete({ ...openDelete, state: false }); 167 | navigate(`${link}`); 168 | } 169 | 170 | 171 | 172 | return ( 173 | setOpenDelete({ ...openDelete, state: false })}> 174 | 175 | 176 | setOpenDelete({ ...openDelete, state: false })} 185 | /> 186 | Delete {openDelete.type} 187 | Are you sure you want to delete this {openDelete.type} {openDelete.name}.
This will permanently delete {openDelete.name} {openDelete.type}'s comments, tools, tasks, workflow runs, and remove all collaborator associations.
188 | setName(e.target.value)} /> 189 | 193 |
194 |
195 |
196 | ) 197 | } 198 | 199 | export default DeletePopup -------------------------------------------------------------------------------- /client/src/pages/Home/components/Features.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import HeroBgAnimation from '../components/HeroBgAnimation' 3 | import Groups3Icon from '@mui/icons-material/Groups3'; 4 | import TimelineIcon from '@mui/icons-material/Timeline'; 5 | import ElectricBoltIcon from '@mui/icons-material/ElectricBolt'; 6 | import PublicIcon from '@mui/icons-material/Public'; 7 | 8 | const FeaturesWrapper = styled.section` 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | background-color: #13111C; 13 | padding-bottom: 200px; 14 | margin-top: -80px; 15 | background: linear-gradient(343.07deg, rgba(23, 92, 230, 0.02) 2.71%, rgba(23, 92, 230, 0.0) 64.83%); 16 | clip-path: polygon(0 0, 100% 0, 100% 100%,50% 95%, 0 100%); 17 | @media (max-width: 768px) { 18 | padding-bottom: 100px; 19 | margin-top: -40px; 20 | clip-path: polygon(0 0, 100% 0, 100% 100%,50% 98%, 0 100%); 21 | } 22 | `; 23 | 24 | const Number = styled.div` 25 | width: 70px; 26 | height: 70px; 27 | font-size: 36px; 28 | font-weight: 800; 29 | color: #306EE8; 30 | text-align: center; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | border-radius: 50%; 35 | border: 6px solid #306EE8; 36 | background-color: #306EE816; 37 | margin-bottom: 20px; 38 | @media (max-width: 768px) { 39 | 40 | width: 50px; 41 | height: 50px; 42 | font-size: 32px; 43 | } 44 | `; 45 | 46 | const FeaturesTitle = styled.div` 47 | font-size: 52px; 48 | text-align: center; 49 | font-weight: 800; 50 | margin-top: 20px; 51 | color: #306EE8; 52 | @media (max-width: 768px) { 53 | margin-top: 12px; 54 | font-size: 36px; 55 | } 56 | `; 57 | 58 | 59 | const FeatureDescription = styled.p` 60 | font-size: 20px; 61 | line-height: 1.5; 62 | font-weight:600px; 63 | width: 100%; 64 | max-width: 700px; 65 | text-align: center; 66 | color: hsl(246, 6%, 65%); 67 | margin-bottom: 80px; 68 | @media (max-width: 768px) { 69 | width: 100%; 70 | font-size: 16px; 71 | margin-bottom: 60px; 72 | } 73 | `; 74 | 75 | const Content = styled.div` 76 | position: relative; 77 | `; 78 | 79 | const FeaturesContainer = styled.div` 80 | position: relative; 81 | z-index: 1; 82 | grid-template-columns: repeat(2, 1fr); 83 | display: grid; 84 | grid-column-gap: 60px; 85 | grid-row-gap: 60px; 86 | @media (max-width: 768px) { 87 | grid-template-columns: repeat(1, 1fr); 88 | grid-column-gap: 30px; 89 | grid-row-gap: 30px; 90 | 91 | } 92 | `; 93 | 94 | const FeatureCard = styled.div` 95 | width: 350px; 96 | height: 190px; 97 | position: relative; 98 | background-color: hsl(250, 24%, 9%); 99 | border: 0.1px solid #306EE8; 100 | border-radius: 16px; 101 | padding: 24px 42px; 102 | box-shadow: rgba(23, 92, 230, 0.15) 0px 4px 24px; 103 | transition: transform 0.2s ease-in-out; 104 | display: flex; 105 | &:hover { 106 | transform: translateY(-10px); 107 | } 108 | @media (max-width: 925px) { 109 | width: 300px; 110 | } 111 | 112 | @media (max-width: 728px) 113 | { 114 | padding: 20px 20px; 115 | } 116 | 117 | `; 118 | 119 | const FeatureIcon = styled.div` 120 | width: 80px; 121 | height: 80px; 122 | color: #306EE8; 123 | position: absolute; 124 | bottom: 0px; 125 | right: 0px; 126 | flex-shrink: 0; 127 | border-top-right-radius: 40%; 128 | border-top-left-radius: 60%; 129 | border-bottom-left-radius: 40%; 130 | border-bottom-right-radius: 16px; 131 | border: 2px solid hsl(220, 80%, 75%,30%); 132 | display: flex; 133 | justify-content: center; 134 | align-items: center; 135 | @media (max-width: 925px) { 136 | width: 80px; 137 | height: 80px; 138 | } 139 | `; 140 | 141 | const FeatureTitle = styled.div` 142 | font-size: 20px; 143 | color: hsl(220, 80%, 75%); 144 | margin-bottom: 10px; 145 | margin-top: 16px; 146 | font-weight: 600; 147 | `; 148 | 149 | const FeatureCardDescription = styled.div` 150 | font-size: 16px; 151 | line-height: 1.5; 152 | color: hsl(246, 6%, 65%); 153 | `; 154 | 155 | const BgImage = styled.div` 156 | position: absolute; 157 | top: 50%; 158 | left: 50%; 159 | transform: translate(-50%, -50%); 160 | @media (max-width: 768px) { 161 | display: none; 162 | } 163 | `; 164 | 165 | const featuresData = [{ icon: , title: 'Project Management', description: 'Effortlessly manage your personal projects and assign tasks to team members while keeping track of progress.', }, 166 | { icon: , title: 'Team Collaboration', description: 'Collaborate with your team members in real-time, assign tasks, and keep track of your team’s progress.', }, 167 | { icon: , title: 'Community Building', description: 'Connect with members of similar interests, build communities, and grow your network.', }, 168 | { icon: , title: 'Time Tracking', description: 'Track your time and improve your productivity by setting goals and keeping track of your progress.', },]; 169 | 170 | const Features = () => { 171 | return ( 172 | 173 | 1 174 | Key Features 175 | Discover how our app simplifies project management and makes collaboration effortless. 176 | 177 | 178 | {featuresData.map((feature, index) => ( 179 | 180 |
181 | {feature.title} 182 | {feature.description} 183 |
184 | 185 | {feature.icon} 186 | 187 |
188 | ))} 189 |
190 | 191 | 192 | 193 | 194 |
195 |
196 | ); 197 | }; 198 | 199 | export default Features; -------------------------------------------------------------------------------- /client/src/pages/Home/components/Benifits.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import TrendingUpIcon from '@mui/icons-material/TrendingUp'; 3 | import ForumIcon from '@mui/icons-material/Forum'; 4 | import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; 5 | import HeroBgAnimation from '../components/HeroBgAnimation' 6 | import Diversity3Icon from '@mui/icons-material/Diversity3'; 7 | 8 | const FeaturesWrapper = styled.section` 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | background-color: #181622; 13 | padding-bottom: 150px; 14 | margin-top: -90px; 15 | @media (max-width: 768px) { 16 | padding-bottom: 100px; 17 | margin-top: -50px; 18 | } 19 | background: linear-gradient(343.07deg, rgba(132, 59, 206, 0.06) 5.71%, rgba(132, 59, 206, 0) 64.83%); 20 | `; 21 | 22 | 23 | const Number = styled.div` 24 | width: 70px; 25 | height: 70px; 26 | font-size: 36px; 27 | font-weight: 800; 28 | color: #854CE6; 29 | text-align: center; 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | border-radius: 50%; 34 | background-color: #854CE616; 35 | border: 6px solid #854CE6; 36 | margin-bottom: 20px; 37 | @media (max-width: 768px) { 38 | 39 | width: 50px; 40 | height: 50px; 41 | font-size: 32px; 42 | } 43 | `; 44 | 45 | 46 | const FeaturesTitle = styled.div` 47 | font-size: 52px; 48 | text-align: center; 49 | font-weight: 800; 50 | margin-top: 20px; 51 | color: #854CE6; 52 | @media (max-width: 768px) { 53 | margin-top: 12px; 54 | font-size: 36px; 55 | } 56 | `; 57 | 58 | 59 | const FeatureDescription = styled.p` 60 | font-size: 20px; 61 | line-height: 1.5; 62 | font-weight:600px; 63 | width: 100%; 64 | max-width: 700px; 65 | text-align: center; 66 | color: hsl(246, 6%, 65%); 67 | margin-bottom: 80px; 68 | @media (max-width: 768px) { 69 | width: 90%; 70 | font-size: 16px; 71 | margin-bottom: 60px; 72 | } 73 | `; 74 | 75 | const Content = styled.div` 76 | position: relative; 77 | `; 78 | 79 | 80 | const FeaturesContainer = styled.div` 81 | position: relative; 82 | z-index: 1; 83 | grid-template-columns: repeat(2, 1fr); 84 | display: grid; 85 | grid-column-gap: 60px; 86 | grid-row-gap: 60px; 87 | @media (max-width: 768px) { 88 | grid-template-columns: repeat(1, 1fr); 89 | grid-column-gap: 30px; 90 | grid-row-gap: 30px; 91 | 92 | } 93 | `; 94 | 95 | const FeatureCard = styled.div` 96 | width: 350px; 97 | height: 190px; 98 | position: relative; 99 | background-color: hsl(250, 24%, 9%); 100 | border: 0.1px solid #854CE6; 101 | border-radius: 16px; 102 | padding: 24px 42px; 103 | box-shadow: rgba(23, 92, 230, 0.15) 0px 4px 24px; 104 | transition: transform 0.2s ease-in-out; 105 | display: flex; 106 | &:hover { 107 | transform: translateY(-10px); 108 | } 109 | @media (max-width: 925px) { 110 | width: 300px; 111 | } 112 | 113 | @media (max-width: 728px) 114 | { 115 | padding: 20px 20px; 116 | } 117 | 118 | `; 119 | 120 | const FeatureIcon = styled.div` 121 | width: 80px; 122 | height: 80px; 123 | color: #854CE6; 124 | position: absolute; 125 | bottom: 0px; 126 | right: 0px; 127 | flex-shrink: 0; 128 | border-top-right-radius: 40%; 129 | border-top-left-radius: 60%; 130 | border-bottom-left-radius: 40%; 131 | border-bottom-right-radius: 16px; 132 | border: 2px solid hsl(220, 80%, 75%,30%); 133 | display: flex; 134 | justify-content: center; 135 | align-items: center; 136 | @media (max-width: 925px) { 137 | width: 80px; 138 | height: 80px; 139 | } 140 | `; 141 | 142 | const FeatureTitle = styled.div` 143 | font-size: 20px; 144 | color: #854CE6; 145 | margin-bottom: 10px; 146 | margin-top: 16px; 147 | font-weight: 600; 148 | `; 149 | 150 | const FeatureCardDescription = styled.div` 151 | font-size: 16px; 152 | line-height: 1.5; 153 | color: hsl(246, 6%, 65%); 154 | `; 155 | 156 | 157 | const BgImage = styled.div` 158 | position: absolute; 159 | top: 50%; 160 | left: 50%; 161 | transform: translate(-50%, -50%); 162 | @media (max-width: 768px) { 163 | display: none; 164 | } 165 | `; 166 | 167 | const featuresData = [{ icon: , title: 'Increased Productivity', description: 'Effortlessly manage your personal projects and assign tasks to team members while keeping track of progress.', }, 168 | { icon: , title: 'Improved Communication', description: 'Keep everyone on the same page and reduce misunderstandings with clear communication.', }, 169 | { icon: , title: 'Better Project Outcomes', description: 'Make informed decisions and track progress to ensure successful project outcomes.', }, 170 | {icon: , title: 'Networking Opportunities', description: 'Connect and collaborate with other developers and professionals in your industry to expand your network and build valuable relationships.'}]; 171 | 172 | const Benefits = () => { 173 | 174 | return ( 175 | 176 | 2 177 | Benefits 178 | Discover the many benefits of using our app to manage your personal and team projects. 179 | 180 | {/* */} 181 | 182 | {featuresData.map((feature, index) => ( 183 | 184 |
185 | {feature.title} 186 | {feature.description} 187 |
188 | 189 | {feature.icon} 190 | 191 |
192 | ))} 193 |
194 | 195 | 196 | 197 | 198 |
199 |
200 | ); 201 | }; 202 | 203 | export default Benefits; 204 | -------------------------------------------------------------------------------- /client/src/components/ChatContact.jsx: -------------------------------------------------------------------------------- 1 | import { Search } from '@mui/icons-material' 2 | import { Avatar } from '@mui/material' 3 | import React from 'react' 4 | import styled from 'styled-components' 5 | 6 | const Continer = styled.div` 7 | display: flex; 8 | flex-direction: column; 9 | width: 100%; 10 | height: 100%; 11 | background-color: ${({ theme }) => theme.card}; 12 | ` 13 | 14 | const TopBar = styled.div` 15 | height: 70px; 16 | border-bottom: 1px solid ${({ theme }) => theme.soft}; 17 | display: flex; 18 | align-items: center; 19 | padding: 0 16px; 20 | @media (max-width: 800px) { 21 | height: 60px; 22 | } 23 | ` 24 | const Contacts = styled.div` 25 | flex: 1; 26 | overflow-y: scroll; 27 | background-color: ${({ theme }) => theme.contact_background}; 28 | @media (max-width: 800px) { 29 | padding: 20px 0; 30 | } 31 | border-bottom-left-radius: 10px; 32 | ` 33 | 34 | 35 | const Profile = styled.div` 36 | display: flex; 37 | flex-direction: column; 38 | margin-left: 16px; 39 | gap: 4px; 40 | ` 41 | const Name = styled.span` 42 | font-weight: 500; 43 | font-size: 16px; 44 | color: ${({ theme }) => theme.text}; 45 | ` 46 | 47 | const SearchBar = styled.div` 48 | display: flex; 49 | align-items: center; 50 | padding: 0 16px; 51 | height: 56px; 52 | border-bottom: 1px solid ${({ theme }) => theme.soft}; 53 | color: ${({ theme }) => theme.soft2}; 54 | @media (max-width: 800px) { 55 | height: 46px; 56 | } 57 | ` 58 | const SearchInput = styled.input` 59 | border: none; 60 | outline: none; 61 | background-color: transparent; 62 | font-size: 16px; 63 | color: ${({ theme }) => theme.text}; 64 | margin-left: 16px; 65 | flex: 1; 66 | &::placeholder { 67 | color: ${({ theme }) => theme.soft2}; 68 | } 69 | 70 | ` 71 | 72 | const ContactCard = styled.div` 73 | display: flex; 74 | align-items: center; 75 | padding: 14px 12px; 76 | cursor: pointer; 77 | border-bottom: 1px solid ${({ theme }) => theme.soft}; 78 | &:hover { 79 | background-color: ${({ theme }) => theme.soft}; 80 | } 81 | ` 82 | 83 | const Message = styled.span` 84 | font-size: 14px; 85 | color: ${({ theme }) => theme.soft2}; 86 | ` 87 | const Time = styled.span` 88 | font-size: 12px; 89 | color: ${({ theme }) => theme.soft2}; 90 | margin-left: auto; 91 | ` 92 | 93 | 94 | const ChatContact = ({ showChat, setShowChat }) => { 95 | return ( 96 | 97 | 98 | 99 | 100 | Messaging 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | setShowChat(true)}> 109 | 110 | 111 | John Doe 112 | Test message this is 113 | 114 | 115 | 116 | 117 | 118 | 119 | John Doe 120 | Test message this is 121 | 122 | 123 | 124 | 125 | 126 | 127 | John Doe 128 | Test message this is 129 | 130 | 131 | 132 | 133 | 134 | 135 | John Doe 136 | Test message this is 137 | 138 | 139 | 140 | 141 | 142 | 143 | John Doe 144 | Test message this is 145 | 146 | 147 | 148 | 149 | 150 | 151 | John Doe 152 | Test message this is 153 | 154 | 155 | 156 | 157 | 158 | 159 | John Doe 160 | Test message this is 161 | 162 | 163 | 164 | 165 | 166 | 167 | John Doe 168 | Test message this is 169 | 170 | 171 | 172 | 173 | 174 | 175 | John Doe 176 | Test message this is 177 | 178 | 179 | 180 | 181 | 182 | ) 183 | } 184 | 185 | export default ChatContact -------------------------------------------------------------------------------- /client/src/data/data.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { 3 | id: 1, 4 | title: "Dashboard Design for Task Manager App", 5 | desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", 6 | tags: ["React", "Node", "MongoDB"], 7 | time: "2 days ago", 8 | status: "Working", 9 | image: "https://cdn.dribbble.com/userupload/3800319/file/original-54a969b772fca3ef0ec9b3b39f0f1b48.jpg?compress=1&resize=450x338&vertical=top", 10 | members: [ 11 | { name: "John", image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTUB8kqGZ74kvQczb_fL00a6LecB331zRp5SQ&usqp=CAU" }, 12 | { name: "John", image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTal_yVhjVwOho5Ck1i0mqlutKZPxcOsRfIBg&usqp=CAU" }, 13 | { name: "John", image: "https://source.unsplash.com/random" }, 14 | ], 15 | }, 16 | { 17 | id: 2, 18 | title: "Project 2", 19 | desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", 20 | tags: ["React", "Node", "MongoDB"], 21 | time: "2 days ago", 22 | status: "In Progress", 23 | image: "", 24 | members: [ 25 | { name: "John", image: "https://source.unsplash.com/random" }, 26 | { name: "John", image: "https://source.unsplash.com/random" }, 27 | { name: "John", image: "https://source.unsplash.com/random" }, 28 | ], 29 | }, 30 | { 31 | id: 3, 32 | title: "Project 1", 33 | desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", 34 | tags: ["Android", "MERN", "MongoDB"], 35 | time: "2 days ago", 36 | status: "In Progress", 37 | image: "", 38 | members: [ 39 | { name: "John", image: "https://source.unsplash.com/random" }, 40 | { name: "John", image: "https://source.unsplash.com/random" }, 41 | { name: "John", image: "https://source.unsplash.com/random" }, 42 | ], 43 | }, 44 | { 45 | id: 4, 46 | title: "Project 1", 47 | desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", 48 | tags: ["React", "Node", "MongoDB"], 49 | time: "2 days ago", 50 | status: "Completed", 51 | image: "", 52 | members: [ 53 | { name: "John", image: "https://source.unsplash.com/random" }, 54 | { name: "John", image: "https://source.unsplash.com/random" }, 55 | { name: "John", image: "https://source.unsplash.com/random" }, 56 | ], 57 | }, 58 | { 59 | id: 5, 60 | title: "Project 2", 61 | desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", 62 | tags: ["React", "Node", "MongoDB"], 63 | time: "2 days ago", 64 | status: "Completed", 65 | image: "https://cdn.dribbble.com/userupload/3801107/file/original-1a14267f7088e99ef74ced21616a4137.png?compress=1&resize=1504x1128", 66 | members: [ 67 | { name: "John", image: "https://source.unsplash.com/random" }, 68 | { name: "John", image: "https://source.unsplash.com/random" }, 69 | { name: "John", image: "https://source.unsplash.com/random" }, 70 | ], 71 | }, 72 | { 73 | id: 6, 74 | title: "Project 3", 75 | desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", 76 | tags: ["React", "Node", "MongoDB"], 77 | time: "2 days ago", 78 | status: "Completed", 79 | image: "", 80 | members: [ 81 | { name: "John", image: "https://source.unsplash.com/random" }, 82 | { name: "John", image: "https://source.unsplash.com/random" }, 83 | { name: "John", image: "https://source.unsplash.com/random" }, 84 | ], 85 | }, 86 | ]; 87 | const statuses = [ 88 | { 89 | status: "Working", 90 | icon: "⭕️", 91 | color: "#EB5A46", 92 | }, 93 | { 94 | status: "In Progress", 95 | icon: "🔆️", 96 | color: "#00C2E0", 97 | }, 98 | { 99 | status: "Completed", 100 | icon: "📝", 101 | color: "#C377E0", 102 | }, 103 | ]; 104 | 105 | const tagColors = [ 106 | "#FF69B4", 107 | "#9932CC", 108 | "#FFA07A", 109 | "#FFD700", 110 | "#90EE90", 111 | "#20B2AA", 112 | "#AFEEEE", 113 | "#FFF0F5", 114 | "#B5E4CA", 115 | "#CABDFF", 116 | "#B1E5FC", 117 | "#FFBC99", 118 | 119 | ] 120 | 121 | const tools = [ 122 | { 123 | name: "Slack", 124 | icon: "https://cdn.worldvectorlogo.com/logos/slack-1.svg", 125 | }, 126 | { 127 | name: "Figma", 128 | icon: "https://cdn.worldvectorlogo.com/logos/figma-1.svg", 129 | }, 130 | { 131 | name: "Adobe XD", 132 | icon: "https://cdn.worldvectorlogo.com/logos/adobe-xd.svg", 133 | }, 134 | { 135 | name: "Github", 136 | icon: "https://cdn.worldvectorlogo.com/logos/github-icon.svg", 137 | }, 138 | { 139 | name: "GitLab", 140 | icon: "https://cdn-icons-png.flaticon.com/512/5968/5968853.png", 141 | }, 142 | { 143 | name: "Bitbucket", 144 | icon: "https://cdn.worldvectorlogo.com/logos/bitbucket.svg", 145 | }, 146 | ] 147 | 148 | const members = [ 149 | { 150 | name: "John Abharim", 151 | email: "john123@gmail.com", 152 | role: "Manager", 153 | access: "Owner", 154 | image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTUB8kqGZ74kvQczb_fL00a6LecB331zRp5SQ&usqp=CAU", 155 | }, 156 | { 157 | name: "John Abharim", 158 | email: "john123@gmail.com", 159 | role: "Developer", 160 | access: "Editor", 161 | image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTUB8kqGZ74kvQczb_fL00a6LecB331zRp5SQ&usqp=CAU", 162 | }, 163 | { 164 | name: "John Abharim", 165 | email: "john123@gmail.com", 166 | role: "Designer", 167 | access: "View Only", 168 | image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTUB8kqGZ74kvQczb_fL00a6LecB331zRp5SQ&usqp=CAU", 169 | }, 170 | ] 171 | 172 | const ideas = [ 173 | "Lorem ipsum dolor sit amet, adipiscing elit. ", 174 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", 175 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", 176 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", 177 | ] 178 | 179 | 180 | export { statuses, data , tagColors, tools, members, ideas }; 181 | -------------------------------------------------------------------------------- /client/src/components/WorkCards.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Fragment, useState, useRef } from "react"; 3 | import styled from "styled-components"; 4 | import { 5 | ImportantDevices, 6 | MoreVert, 7 | TimelapseRounded, 8 | StarsRounded 9 | } from "@mui/icons-material"; 10 | import LinearProgress, { 11 | linearProgressClasses, 12 | } from "@mui/material/LinearProgress"; 13 | import { format } from "timeago.js"; 14 | import { useDrag, useDrop } from "react-dnd"; 15 | import ITEM_TYPE from "../data/types"; 16 | import { tagColors } from "../data/data"; 17 | import { Link } from "react-router-dom"; 18 | import { color } from "@mui/system"; 19 | import { Avatar, IconButton } from "@mui/material"; 20 | import WorkDetails from "./WorkDetails"; 21 | 22 | const Container = styled.div` 23 | padding: 14px; 24 | text-align: left; 25 | margin: 2px 0px; 26 | font-size: 16px; 27 | font-weight: 500; 28 | border-radius: 10px; 29 | background-color: ${({ theme }) => theme.card}; 30 | color: ${({ theme }) => theme.text}; 31 | cursor: pointer; 32 | box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.09); 33 | &:hover { 34 | transition: all 0.6s ease-in-out; 35 | box-shadow: 0 0 18px 0 rgba(0, 0, 0, 0.5); 36 | } 37 | `; 38 | 39 | const Top = styled.div` 40 | display: flex; 41 | justify-content: space-between; 42 | align-items: center; 43 | `; 44 | 45 | const Title = styled.div` 46 | font-size: 16px; 47 | font-weight: 500; 48 | color: ${({ theme }) => theme.textSoft}; 49 | margin-top: 6px; 50 | flex: 7; 51 | line-height: 1.5; 52 | overflow: hidden; 53 | text-overflow: ellipsis; 54 | display: -webkit-box; 55 | -webkit-line-clamp: 2; /* number of lines to show */ 56 | line-clamp: 2; 57 | -webkit-box-orient: vertical; 58 | `; 59 | 60 | const Desc = styled.div` 61 | font-size: 14px; 62 | font-weight: 400; 63 | color: ${({ theme }) => theme.soft2}; 64 | margin-top: 4px; 65 | line-height: 1.5; 66 | overflow: hidden; 67 | text-overflow: ellipsis; 68 | display: -webkit-box; 69 | -webkit-line-clamp: 5; /* number of lines to show */ 70 | line-clamp: 5; 71 | -webkit-box-orient: vertical; 72 | `; 73 | 74 | const Progress = styled.div` 75 | position: relative; 76 | `; 77 | 78 | const Text = styled.div` 79 | display: flex; 80 | justify-content: space-between; 81 | align-items: center; 82 | font-size: 12px; 83 | font-weight: 400; 84 | color: ${({ theme }) => theme.soft2}; 85 | margin: 14px 0px 10px 0px; 86 | line-height: 1.5; 87 | overflow: hidden; 88 | `; 89 | 90 | const Tags = styled.div` 91 | display: flex; 92 | flex-wrap: wrap; 93 | flex-direction: row; 94 | gap: 4px; 95 | margin-top: 8px; 96 | `; 97 | 98 | const Tag = styled.div` 99 | padding: 4px 10px; 100 | border-radius: 8px; 101 | color: ${({ tagColor, theme }) => tagColor + theme.lightAdd}; 102 | background-color: ${({ tagColor, theme }) => tagColor + "10"}; 103 | font-size: 10px; 104 | font-weight: 500; 105 | `; 106 | 107 | const Span = styled.span` 108 | font-size: 12px; 109 | font-weight: 600; 110 | color: ${({ theme }) => theme.soft2}; 111 | line-height: 1.5; 112 | `; 113 | 114 | const Bottom = styled.div` 115 | display: flex; 116 | justify-content: space-between; 117 | align-items: center; 118 | margin: 20px 0px 14px 0px; 119 | `; 120 | 121 | const Time = styled.div` 122 | display: flex; 123 | align-items: center; 124 | gap: 8px; 125 | font-size: 12px; 126 | font-weight: 500; 127 | color: ${({ theme }) => theme.soft2 + "99"}; 128 | `; 129 | 130 | const AvatarGroup = styled.div` 131 | display: flex; 132 | align-items: center; 133 | margin-right: 12px; 134 | `; 135 | const IcoBtn = styled(IconButton)` 136 | color: ${({ theme }) => theme.textSoft} !important; 137 | `; 138 | 139 | const Card = ({ status, work }) => { 140 | const [color, setColor] = useState("primary"); 141 | const [task, setTask] = useState(work.tasks); 142 | const [tag, setTag] = useState(work.tags); 143 | const [completed, setCompleted] = useState(0); 144 | const [progress, setProgress] = useState(0); 145 | const [members, setMembers] = useState([]); 146 | const [openWork, setOpenWork] = useState(false); 147 | 148 | useEffect(() => { 149 | if (status === "Completed") { 150 | setColor("success"); 151 | } 152 | }, [status]); 153 | 154 | //check the no of tasks completed in the work and set the progress 155 | useEffect(() => { 156 | let count = 0; 157 | let Members = []; 158 | task.forEach((item) => { 159 | if (item.status === "Completed") { 160 | count++; 161 | } 162 | console.log(item); 163 | if (item.members.length > 0) { 164 | item.members.forEach((items) => { 165 | let isPresent = Members.some((member) => member._id === items._id); 166 | if (!isPresent) { 167 | Members.push(items); 168 | } 169 | }); 170 | } 171 | }); 172 | setCompleted(count); 173 | setProgress(completed); 174 | setMembers(Members); 175 | }, [task]); 176 | 177 | //get the members of the work from all the tasks and add it in members array withb the image and name 178 | 179 | return ( 180 | 181 | 182 | {work.title} 183 | {work.priority === "Low" && 184 | } 185 | 186 | 187 | 188 | 189 | {work.desc} 190 | 191 | {tag.map((tag) => ( 192 | 195 | {tag} 196 | 197 | ))} 198 | 199 | 200 | 201 | Tasks Completed 202 | 203 | {completed} / {task.length} 204 | 205 | 206 | 212 | 213 | 214 | 218 | 219 | {members.slice(0, 2).map((member) => ( 220 | 229 | {member.name.charAt(0)} 230 | 231 | ))} 232 | {members.length > 2 && ( 233 | 241 | +{members.length - 2} 242 | 243 | )} 244 | 245 | 246 | 247 | ); 248 | }; 249 | 250 | export default Card; 251 | -------------------------------------------------------------------------------- /client/src/components/Menu.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useState } from "react"; 3 | import styled from "styled-components"; 4 | import SettingsBrightnessOutlinedIcon from "@mui/icons-material/SettingsBrightnessOutlined"; 5 | import { Link } from "react-router-dom"; 6 | import { 7 | Add, 8 | Dashboard, 9 | CloseRounded, 10 | Groups2Rounded, 11 | HubRounded, 12 | Logout, 13 | StreamRounded, 14 | WorkspacesRounded, 15 | Public, 16 | AccountTreeRounded, 17 | DashboardRounded, 18 | AddTaskRounded, 19 | } from "@mui/icons-material"; 20 | import { tagColors } from "../data/data"; 21 | import LogoIcon from "../Images/Logo.svg"; 22 | import { useDispatch } from "react-redux"; 23 | import { logout } from "../redux/userSlice"; 24 | import { openSnackbar } from "../redux/snackbarSlice"; 25 | import axios from "axios"; 26 | import { useSelector } from "react-redux"; 27 | import { getUsers, notifications } from "../api/index"; 28 | import { useNavigate } from 'react-router-dom'; 29 | import { Avatar, CircularProgress } from "@mui/material"; 30 | import Skeleton from "@mui/material/Skeleton"; 31 | 32 | const Container = styled.div` 33 | flex: 1.3; 34 | background-color: ${({ theme }) => theme.bgLighter}; 35 | height: 100vh; 36 | border-top-right-radius: 14px; 37 | border-bottom-right-radius: 14px; 38 | color: ${({ theme }) => theme.text}; 39 | font-size: 14px; 40 | position: sticky; 41 | top: 0; 42 | box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.04); 43 | transition: 0.3s ease-in-out; 44 | @media (max-width: 1100px) { 45 | position: fixed; 46 | z-index: 100; 47 | width: 100%; 48 | max-width: 250px; 49 | left: ${({ setMenuOpen }) => (setMenuOpen ? "0" : "-100%")}; 50 | transition: 0.3s ease-in-out; 51 | } 52 | `; 53 | const ContainerWrapper = styled.div` 54 | height: 90%; 55 | overflow-y: scroll !important; 56 | margin-top: 0px; 57 | `; 58 | const Space = styled.div` 59 | height: 50px; 60 | `; 61 | const Flex = styled.div` 62 | display: flex; 63 | justify-content: space-between; 64 | align-items: center; 65 | padding: 24px 24px; 66 | `; 67 | 68 | const Logo = styled.div` 69 | color: ${({ theme }) => theme.primary}; 70 | display: flex; 71 | align-items: center; 72 | gap: 16px; 73 | font-weight: bold; 74 | font-size: 26px; 75 | `; 76 | 77 | const Close = styled.div` 78 | display: none; 79 | @media (max-width: 1100px) { 80 | display: block; 81 | } 82 | `; 83 | 84 | const Image = styled.img` 85 | height: 32px; 86 | `; 87 | 88 | const Item = styled.div` 89 | display: flex; 90 | color: ${({ theme }) => theme.itemText}; 91 | align-items: center; 92 | gap: 20px; 93 | cursor: pointer; 94 | padding: 7.5px 26px; 95 | &:hover { 96 | background-color: ${({ theme }) => theme.itemHover}; 97 | } 98 | `; 99 | 100 | const Hr = styled.hr` 101 | margin: 15px 15px 15px 0px; 102 | border: 0.5px solid ${({ theme }) => theme.soft}; 103 | `; 104 | 105 | const Title = styled.h2` 106 | font-size: 15px; 107 | font-weight: 500; 108 | color: ${({ theme }) => theme.textSoft + "99"}; 109 | margin-bottom: 4px; 110 | padding: 0px 26px; 111 | display: flex; 112 | align-items: center; 113 | gap: 12px; 114 | `; 115 | 116 | const TeamIcon = styled(WorkspacesRounded)` 117 | color: ${({ tagColor }) => tagColor}; 118 | font-size: 18px; 119 | margin-left: 2px; 120 | `; 121 | 122 | const Menu = ({ darkMode, setDarkMode, setMenuOpen, setNewTeam }) => { 123 | const [teamsLoading, setTeamsLoading] = useState(true); 124 | const token = localStorage.getItem("token"); 125 | const dispatch = useDispatch(); 126 | const navigate = useNavigate(); 127 | const logoutUser = () => { 128 | dispatch(logout()); 129 | navigate(`/`); 130 | }; 131 | 132 | const [team, setTeams] = useState([]); 133 | const { currentUser } = useSelector(state => state.user); 134 | 135 | const getteams = async () => { 136 | setTeamsLoading(true); 137 | await getUsers(token) 138 | .then((res) => { 139 | setTeams(res.data.teams); 140 | setTeamsLoading(false); 141 | }) 142 | .catch((err) => { 143 | dispatch(openSnackbar({ message: err.message, type: "error" })); 144 | if (err.response.status === 401 || err.response.status === 402) logoutUser(); 145 | }); 146 | }; 147 | 148 | 149 | useEffect(() => { 150 | getteams(); 151 | }, [currentUser]); 152 | 153 | return ( 154 | 155 | 156 | 157 | 158 | 159 | VEXA 160 | 161 | 162 | 163 | setMenuOpen(false)} /> 164 | 165 | 166 | 167 | 168 | 169 | 170 | Dashboard 171 | 172 | 173 | 177 | 178 | 179 | Projects 180 | 181 | 182 | 186 | 187 | 188 | Your Works 189 | 190 | 191 | 195 | 196 | 197 | Community 198 | 199 | 200 |
201 | 202 | <Groups2Rounded /> Teams 203 | 204 | {teamsLoading ? ( 205 |
206 | 207 |
208 | ) : (<> 209 | {team.map((team, i) => ( 210 | 214 | 215 | {team.img !== "" ? 216 | {team.name[0]} : 217 | } 218 | {team.name} 219 | 220 | 221 | ))} 222 | 223 | )} 224 | setNewTeam(true)}> 225 | 226 | New Team 227 | 228 |
229 | setDarkMode(!darkMode)}> 230 | 231 | {darkMode ? "Light" : "Dark"} Mode 232 | 233 | logoutUser()}> 234 | 235 | Logout 236 | 237 | 238 |
239 |
240 | ); 241 | }; 242 | 243 | export default Menu; 244 | -------------------------------------------------------------------------------- /server/controllers/works.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import User from "../models/User.js"; 3 | import bcrypt from "bcrypt"; 4 | import { createError } from "../error.js"; 5 | import jwt from "jsonwebtoken"; 6 | import nodemailer from "nodemailer"; 7 | import dotenv from 'dotenv'; 8 | import Works from "../models/Works.js"; 9 | 10 | 11 | export const addWork = async (req, res, next) => { 12 | const user = await User.findById(req.user.id); 13 | if (!user) { 14 | return next(createError(404, "User not found")); 15 | } 16 | 17 | const newWork = new Project({ members: [{ id: user.id, role: "d", access: "Owner" }], ...req.body }); 18 | try { 19 | const saveProject = await (await newWork.save()); 20 | User.findByIdAndUpdate(user.id, { $push: { projects: saveProject._id } }, { new: true }, (err, doc) => { 21 | if (err) { 22 | next(err); 23 | } 24 | }); 25 | res.status(200).json(saveProject); 26 | } catch (err) { 27 | next(err); 28 | } 29 | }; 30 | 31 | 32 | 33 | export const deleteProject = async (req, res, next) => { 34 | try { 35 | const project = await Project.findById(req.params.id); 36 | if (!project) return next(createError(404, "Project not found!")); 37 | for (let i = 0; i < project.members.length; i++) { 38 | if (project.members[i].id === req.user.id) { 39 | if (project.members[i].access === "Owner") { 40 | await project.delete(); 41 | User.findByIdAndUpdate(req.user.id, { $pull: { projects: req.params.id } }, { new: true }, (err, doc) => { 42 | if (err) { 43 | next(err); 44 | } 45 | }); 46 | res.status(200).json("Project has been deleted..."); 47 | } else { 48 | return next(createError(403, "You are not allowed to delete this project!")); 49 | } 50 | } 51 | } 52 | } catch (err) { 53 | next(err); 54 | } 55 | }; 56 | 57 | export const getProject = async (req, res, next) => { 58 | try { 59 | const project = await Project.findById(req.params.id); 60 | const members = [] 61 | var verified = false 62 | await Promise.all( 63 | project.members.map(async (Member) => { 64 | console.log(Member.id) 65 | if (Member.id === req.user.id) { 66 | verified = true 67 | } 68 | await User.findById(Member.id).then((member) => { 69 | console.log(member) 70 | members.push({ id: member.id, role: Member.role, access: Member.access, name: member.name, img: member.img, email: member.email }); 71 | }) 72 | }) 73 | ) 74 | .then(() => { 75 | if (verified) { 76 | return res.status(200).json({ project, members }); 77 | } else { 78 | return next(createError(403, "You are not allowed to view this project!")); 79 | } 80 | }); 81 | 82 | } catch (err) { 83 | next(err); 84 | } 85 | }; 86 | 87 | 88 | export const updateProject = async (req, res, next) => { 89 | try { 90 | const project = await Project.findById(req.params.id); 91 | if (!project) return next(createError(404, "project not found!")); 92 | for (let i = 0; i < project.members.length; i++) { 93 | if (project.members[i].id === req.user.id) { 94 | if (project.members[i].access === "Owner" || project.members[i].access === "Admin" || project.members[i].access === "Editor") { 95 | const updatedproject = await Project.findByIdAndUpdate( 96 | req.params.id, 97 | { 98 | $set: req.body, 99 | }, 100 | { new: true } 101 | ); 102 | res.status(200).json(updatedproject); 103 | } else { 104 | return next(createError(403, "You are not allowed to update this project!")); 105 | } 106 | } else { 107 | return next(createError(403, "You can update only if you are a member of this project!")); 108 | } 109 | } 110 | } catch (err) { 111 | next(err); 112 | } 113 | }; 114 | 115 | 116 | 117 | dotenv.config(); 118 | 119 | const transporter = nodemailer.createTransport({ 120 | service: "gmail", 121 | auth: { 122 | user: process.env.EMAIL_USERNAME, 123 | pass: process.env.EMAIL_PASSWORD 124 | }, 125 | port: 465, 126 | host: 'smtp.gmail.com' 127 | }); 128 | 129 | export const inviteProjectMember = async (req, res, next) => { 130 | //send mail using nodemailer 131 | const user = await User.findById(req.user.id); 132 | if (!user) { 133 | return next(createError(404, "User not found")); 134 | } 135 | 136 | const project = await Project.findById(req.params.id); 137 | if (!project) return next(createError(404, "Project not found!")); 138 | for (let i = 0; i < project.members.length; i++) { 139 | if (project.members[i].id === req.user.id) { 140 | if (project.members[i].access === "Owner" || project.members[i].access === "Admin" || project.members[i].access === "Editor") { 141 | 142 | const mailOptions = { 143 | from: process.env.EMAIL, 144 | to: req.body.email, 145 | subject: "Invitation to join project", 146 | text: `Hi ${req.body.name}, you have been invited to join project ${project.title} by ${user.name}. Please click on the link to join the project. http://localhost:8080/api/project/invite/${req.params.id}/${req.body.id}`, 147 | }; 148 | transporter.sendMail(mailOptions, (err, data) => { 149 | if (err) { 150 | return next(err); 151 | } else { 152 | res.status(200).json({ message: "Email sent successfully" }); 153 | } 154 | }); 155 | } else { 156 | return next(createError(403, "You are not allowed to invite members to this project!")); 157 | } 158 | } 159 | } 160 | }; 161 | 162 | //verify invitation and add to project member 163 | export const verifyInvitation = async (req, res, next) => { 164 | try { 165 | const project = await Project.findById(req.params.projectId); 166 | if (!project) return next(createError(404, "Project not found!")); 167 | const user = await User.findById(req.params.userId); 168 | if (!user) { 169 | return next(createError(404, "User not found")); 170 | } 171 | for (let i = 0; i < project.members.length; i++) { 172 | if (project.members[i].id === user.id) { 173 | return next(createError(403, "You are already a member of this project!")); 174 | } 175 | } 176 | const newMember = { id: user.id, img: user.img, name: user.name, email: user.email, role: "d", access: "View Only" }; 177 | const updatedProject = await Project.findByIdAndUpdate( 178 | req.params.projectId, 179 | { 180 | $push: { members: newMember }, 181 | }, 182 | { new: true } 183 | ); 184 | User.findByIdAndUpdate(user.id, { $push: { projects: updatedProject._id } }, { new: true }, (err, doc) => { 185 | if (err) { 186 | next(err); 187 | } 188 | }); 189 | res.status(200).json(updatedProject); 190 | } catch (err) { 191 | next(err); 192 | } 193 | }; 194 | 195 | 196 | export const getProjectMembers = async (req, res, next) => { 197 | try { 198 | const project = await Project.findById(req.params.id); 199 | if (!project) return next(createError(404, "Project not found!")); 200 | res.status(200).json(project.members); 201 | } catch (err) { 202 | next(err); 203 | } 204 | } -------------------------------------------------------------------------------- /client/src/components/ChatContainer.jsx: -------------------------------------------------------------------------------- 1 | import { ArrowBack, AttachFile, DoneAll, Send, Telegram } from '@mui/icons-material' 2 | import { Avatar, IconButton } from '@mui/material' 3 | import React, { useRef, useEffect } from 'react' 4 | import styled from 'styled-components' 5 | 6 | const Container = styled.div` 7 | display: flex; 8 | width: 100%; 9 | height: 100%; 10 | flex-direction: column; 11 | background-color: ${({ theme }) => theme.card}; 12 | ` 13 | 14 | const TopBar = styled.div` 15 | height: 70px; 16 | border-bottom: 1px solid ${({ theme }) => theme.soft}; 17 | display: flex; 18 | align-items: center; 19 | padding: 0px 16px; 20 | @media (max-width: 800px) { 21 | height: 60px; 22 | padding: 0px 16px 0px 6px; 23 | } 24 | ` 25 | 26 | const Chat = styled.div` 27 | flex: 1; 28 | overflow-y: scroll; 29 | padding: 20px 6px; 30 | background-color: ${({ theme }) => theme.chat_background}; 31 | @media (max-width: 800px) { 32 | padding: 20px 0; 33 | } 34 | 35 | ` 36 | 37 | const RecievedMessage = styled.p` 38 | margin: 16px 16px 0 16px; 39 | padding: 12px 16px; 40 | background-color: ${({ theme }) => theme.recieve_message}; 41 | border-radius: 12px; 42 | color: ${({ theme }) => theme.text}; 43 | font-size: 14px; 44 | max-width: 70%; 45 | width: fit-content; 46 | box-shadow: 0 0 6px rgba(0,0,0,0.2); 47 | position: relative; 48 | &:after { 49 | content: ''; 50 | position: absolute; 51 | visibility: visible; 52 | top: 0px; 53 | left: -10px; 54 | border: 10px solid transparent; 55 | border-top: 10px solid ${({ theme }) => theme.recieve_message}; 56 | clear: both; 57 | } 58 | ` 59 | 60 | const SentMessage = styled.p` 61 | margin: 16px 16px 0 auto; 62 | padding: 12px 16px; 63 | background-color: ${({ theme }) => theme.send_message}; 64 | border-radius: 12px 0px 12px 12px; 65 | color: ${({ theme }) => theme.send_message_text_color}; 66 | font-size: 14px; 67 | max-width: 70%; 68 | width: fit-content; 69 | box-shadow: 0 0 6px rgba(0,0,0,0.4); 70 | position: relative; 71 | &:after { 72 | content: ''; 73 | position: absolute; 74 | visibility: visible; 75 | top: 0px; 76 | right: -10px; 77 | border: 10px solid transparent; 78 | border-top: 10px solid ${({ theme }) => theme.send_message}; 79 | clear: both; 80 | } 81 | ` 82 | 83 | const Time = styled.span` 84 | font-size: 12px; 85 | padding: 10px 16px; 86 | color: ${({ theme }) => theme.soft2}; 87 | margin: 0 16px; 88 | display: flex; 89 | align-items: center; 90 | justify-content: flex-end; 91 | gap: 10px; 92 | ${({ message }) => message === 'recieved' && ` 93 | justify-content: flex-start; 94 | `} 95 | ` 96 | 97 | const SendMessage = styled.div` 98 | min-height: 70px; 99 | border-top: 1px solid ${({ theme }) => theme.soft}; 100 | display: flex; 101 | align-items: center; 102 | padding: 0 16px; 103 | gap: 10px; 104 | @media (max-width: 800px) { 105 | position: fixed; 106 | background-color: ${({ theme }) => theme.card}; 107 | bottom: 0; 108 | left: 0; 109 | right: 0; 110 | z-index: 100; 111 | gap: 6px; 112 | padding: 0 2px; 113 | } 114 | ` 115 | 116 | const Profile = styled.div` 117 | display: flex; 118 | flex-direction: column; 119 | margin-left: 16px; 120 | gap: 4px; 121 | ` 122 | const Name = styled.span` 123 | font-weight: 600; 124 | font-size: 16px; 125 | color: ${({ theme }) => theme.text}; 126 | ` 127 | const Status = styled.span` 128 | font-size: 12px; 129 | color: ${({ theme }) => theme.text}; 130 | ` 131 | 132 | const MessageBox = styled.div` 133 | display: flex; 134 | align-items: center; 135 | justify-content: space-between; 136 | width: 100%; 137 | background-color: ${({ theme }) => theme.bg}; 138 | border-radius: 12px; 139 | padding: 16px 8px; 140 | ` 141 | 142 | const Message = styled.input` 143 | border: none; 144 | flex: 1; 145 | height: 100%; 146 | width: 100%; 147 | background-color: transparent; 148 | color: ${({ theme }) => theme.text}; 149 | font-size: 16px; 150 | padding: 0 16px; 151 | &:focus { 152 | outline: none; 153 | } 154 | `; 155 | 156 | 157 | const ChatContainer = ({showChat,setShowChat}) => { 158 | 159 | //get the window size and hide the chat container for mobile and dislay it for desktop 160 | const [width, setWidth] = React.useState(window.innerWidth) 161 | const breakpoint = 768 162 | 163 | useEffect(() => { 164 | const handleWindowResize = () => setWidth(window.innerWidth) 165 | window.addEventListener("resize", handleWindowResize) 166 | return () => window.removeEventListener("resize", handleWindowResize) 167 | }, []) 168 | 169 | const messagesEndRef = useRef(null) 170 | const scrollToBottom = () => { 171 | messagesEndRef.current.scrollIntoView({ behavior: "smooth" }) 172 | } 173 | useEffect(scrollToBottom); 174 | 175 | return ( 176 | 177 | 178 | {width < breakpoint && 179 | setShowChat(false)}> 180 | 181 | } 182 | 183 | 184 | John Doe 185 | Online 186 | 187 | 188 | 189 | hola fghtdfhhhhhhhhhhhhhhhh trw twr twrtrw44t rwerewty rewyetryetyetyetryery ertyetyertyertyetry e5ty5et444444444444y 5y54ey5yy y53y5e4ye45 190 | 191 | hola fghtdfhhhhhhhhhhhhhhhh trw twr twrtrw44t rwerewty rewyetryetyetyetryery ertyetyertyertyetry e5ty5et444444444444y 5y54ey5yy y53y5e4ye45 192 | 193 | hola fghtdfhhhhhhhhhhhhhhhh trw twr twrtrw44t rwerewty rewyetryetyetyetryery ertyetyertyertyetry e5ty5et444444444444y 5y54ey5yy y53y5e4ye45 194 | 195 | hola fghtdfhhhhhhhhhhhhhhhh trw twr twrtrw44t rwerewty rewyetryetyetyetryery ertyetyertyertyetry e5ty5et444444444444y 5y54ey5yy y53y5e4ye45 196 | 197 | 198 |
199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | ) 213 | } 214 | 215 | export default ChatContainer -------------------------------------------------------------------------------- /server/controllers/user.js: -------------------------------------------------------------------------------- 1 | import { createError } from "../error.js"; 2 | import User from "../models/User.js"; 3 | import Project from "../models/Project.js"; 4 | import Teams from "../models/Teams.js"; 5 | import Notifications from "../models/Notifications.js"; 6 | 7 | export const update = async (req, res, next) => { 8 | if (req.params.id === req.user.id) { 9 | try { 10 | const updatedUser = await User.findByIdAndUpdate( 11 | req.params.id, 12 | { 13 | $set: req.body, 14 | }, 15 | { new: true } 16 | ); 17 | res.status(200).json(updatedUser); 18 | } catch (err) { 19 | next(err); 20 | } 21 | } else { 22 | return next(createError(403, "You can update only your account!")); 23 | } 24 | } 25 | 26 | export const deleteUser = async (req, res, next) => { 27 | if (req.params.id === req.user.id) { 28 | try { 29 | await User.findByIdAndDelete(req.params.id); 30 | res.status(200).json("User has been deleted."); 31 | } catch (err) { 32 | next(err); 33 | } 34 | } else { 35 | return next(createError(403, "You can delete only your account!")); 36 | } 37 | } 38 | 39 | export const findUser = async (req, res, next) => { 40 | try { 41 | const user = await User.findById(req.params.id); 42 | res.status(200).json(user); 43 | } catch (err) { 44 | next(err); 45 | } 46 | } 47 | 48 | export const getUser = async (req, res, next) => { 49 | //if the req.user id is not present then it will give a message of user not authenticated 50 | 51 | try { 52 | const user = await User.findById(req.user.id).populate("notifications").populate({ 53 | path: "teams", 54 | populate: { 55 | path: "members.id", 56 | select: "_id name email", 57 | } 58 | }).populate("projects").populate("works").populate("tasks"); 59 | //extract the notification from the user and send it to the client 60 | console.log(user) 61 | res.status(200).json(user); 62 | } catch (err) { 63 | console.log(req.user) 64 | next(err); 65 | } 66 | } 67 | 68 | // get notifications of the user 69 | 70 | export const getNotifications = async (req, res, next) => { 71 | try { 72 | const user = await User.findById(req.user.id); 73 | //extract the notification from the user and send it to the client 74 | const notifications = user.notifications; 75 | const notificationArray = []; 76 | for (let i = 0; i < notifications.length; i++) { 77 | const notification = await Notifications.findById(notifications[i]); 78 | notificationArray.push(notification); 79 | } 80 | res.status(200).json(notificationArray); 81 | } catch (err) { 82 | next(err); 83 | } 84 | } 85 | 86 | 87 | 88 | //fetch all the works of the user 89 | export const getWorks = async (req, res, next) => { 90 | try { 91 | const user = await User.findById(req.user.id).populate( 92 | { 93 | path: "works", 94 | populate: { 95 | path: "tasks", 96 | populate: { 97 | path: "members", 98 | select: "name img", 99 | }, 100 | } 101 | }).populate({ 102 | path: "works", 103 | populate: { 104 | path: "creatorId", 105 | select: "name img", 106 | } 107 | }) 108 | .sort({ updatedAt: -1 });; 109 | if (!user) return next(createError(404, "User not found!")); 110 | //store all the works of the user in an array and send it to the client 111 | const works = []; 112 | await Promise.all( 113 | user.works.map(async (work) => { 114 | works.push(work); 115 | }) 116 | ).then(() => { 117 | res.status(200).json(works); 118 | }); 119 | } catch (err) { 120 | next(err); 121 | } 122 | }; 123 | 124 | //get all the tasks of the user 125 | export const getTasks = async (req, res, next) => { 126 | try { 127 | const user = await User.findById(req.user.id).populate({ 128 | path: "tasks", 129 | populate: { 130 | path: "members", 131 | select: "name img", 132 | } 133 | }).sort({ end_date: 1 }); 134 | if (!user) return next(createError(404, "User not found!")); 135 | //store all the tasks of the user in an array and send it to the client 136 | const tasks = []; 137 | await Promise.all( 138 | user.tasks.map(async (task) => { 139 | tasks.push(task); 140 | }) 141 | ).then(() => { 142 | res.status(200).json(tasks); 143 | }); 144 | } catch (err) { 145 | next(err); 146 | } 147 | }; 148 | 149 | 150 | 151 | export const subscribe = async (req, res, next) => { 152 | try { 153 | await User.findByIdAndUpdate(req.user.id, { 154 | $push: { subscribedUsers: req.params.id }, 155 | }); 156 | await User.findByIdAndUpdate(req.params.id, { 157 | $inc: { subscribers: 1 }, 158 | }); 159 | res.status(200).json("Subscription successfull.") 160 | } catch (err) { 161 | next(err); 162 | } 163 | } 164 | 165 | export const unsubscribe = async (req, res, next) => { 166 | try { 167 | try { 168 | await User.findByIdAndUpdate(req.user.id, { 169 | $pull: { subscribedUsers: req.params.id }, 170 | }); 171 | await User.findByIdAndUpdate(req.params.id, { 172 | $inc: { subscribers: -1 }, 173 | }); 174 | res.status(200).json("Unsubscription successfull.") 175 | } catch (err) { 176 | next(err); 177 | } 178 | } catch (err) { 179 | next(err); 180 | } 181 | } 182 | 183 | //find project id from user and get it from projects collection and send it to client 184 | export const getUserProjects = async (req, res, next) => { 185 | try { 186 | const user = await User.findById(req.user.id).populate("projects") 187 | const projects = [] 188 | await Promise.all(user.projects.map(async (project) => { 189 | await Project.findById(project).populate("members.id", "_id name email img").then((project) => { 190 | projects.push(project) 191 | }).catch((err) => { 192 | next(err) 193 | }) 194 | })).then(() => { 195 | res.status(200).json(projects) 196 | }).catch((err) => { 197 | next(err) 198 | }) 199 | } catch (err) { 200 | next(err); 201 | } 202 | } 203 | 204 | //find team id from user and get it from teams collection and send it to client 205 | export const getUserTeams = async (req, res, next) => { 206 | try { 207 | const user = await User.findById(req.user.id).populate("teams") 208 | const teams = [] 209 | await Promise.all(user.teams.map(async (team) => { 210 | await Teams.findById(team.id).then((team) => { 211 | teams.push(team) 212 | }).catch((err) => { 213 | next(err) 214 | }) 215 | })).then(() => { 216 | res.status(200).json(teams) 217 | }).catch((err) => { 218 | next(err) 219 | }) 220 | } catch (err) { 221 | next(err); 222 | } 223 | } 224 | 225 | //find user from email and send it to client 226 | export const findUserByEmail = async (req, res, next) => { 227 | const email = req.params.email; 228 | const users = []; 229 | try { 230 | await User.findOne({ email: { $regex: email, $options: "i" } }).then((user) => { 231 | if(user!=null) 232 | { 233 | users.push(user); 234 | res.status(200).json(users); 235 | }else{ 236 | res.status(201).json({message:"No user found"}); 237 | } 238 | }).catch((err) => { 239 | next(err) 240 | }) 241 | } catch (err) { 242 | next(err); 243 | } 244 | } -------------------------------------------------------------------------------- /client/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import styled from "styled-components"; 3 | import SearchIcon from "@mui/icons-material/Search"; 4 | import { useSelector } from "react-redux"; 5 | import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined"; 6 | import SignUp from "./SignUp"; 7 | import SignIn from "./SignIn"; 8 | import MenuIcon from "@mui/icons-material/Menu"; 9 | import { IconButton } from "@mui/material"; 10 | import { Forum, NotificationsRounded } from "@mui/icons-material"; 11 | import Badge from "@mui/material/Badge"; 12 | import { useDispatch } from "react-redux"; 13 | import Avatar from "@mui/material/Avatar"; 14 | import AccountDialog from "./AccountDialog"; 15 | import NotificationDialog from "./NotificationDialog"; 16 | import { getUsers, notifications } from "../api/index"; 17 | import { openSnackbar } from "../redux/snackbarSlice"; 18 | import { logout } from "../redux/userSlice"; 19 | import { useNavigate } from "react-router-dom"; 20 | 21 | const Container = styled.div` 22 | position: sticky; 23 | top: 0; 24 | height: 56px; 25 | margin: 6px 6px 0px 6px; 26 | border-radius: 12px; 27 | z-index: 99; 28 | box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.06); 29 | background-color: ${({ theme }) => theme.bgLighter}; 30 | @media screen and (max-width: 480px) { 31 | margin: 0px 0px 0px 0px; 32 | height: 60px; 33 | } 34 | `; 35 | const Wrapper = styled.div` 36 | height: 100%; 37 | display: flex; 38 | align-items: center; 39 | justify-content: flex-end; 40 | padding: 0px 14px; 41 | @media screen and (max-width: 480px) { 42 | padding: 0px 4px; 43 | } 44 | position: relative; 45 | `; 46 | 47 | const IcoButton = styled(IconButton)` 48 | color: ${({ theme }) => theme.textSoft} !important; 49 | `; 50 | 51 | const Search = styled.div` 52 | width: 40%; 53 | @media screen and (max-width: 480px) { 54 | width: 50%; 55 | } 56 | left: 0px; 57 | right: 0px; 58 | margin: auto; 59 | display: flex; 60 | align-items: center; 61 | justify-content: space-between; 62 | border-radius: 100px; 63 | color: ${({ theme }) => theme.textSoft}; 64 | background-color: ${({ theme }) => theme.bgDark}; 65 | `; 66 | const Input = styled.input` 67 | width: 100%; 68 | border: none; 69 | font-size: 16px; 70 | padding: 10px 20px; 71 | border-radius: 100px; 72 | background-color: transparent; 73 | outline: none; 74 | color: ${({ theme }) => theme.textSoft}; 75 | `; 76 | 77 | const Button = styled.button` 78 | padding: 5px 18px; 79 | background-color: transparent; 80 | border: 1px solid ${({ theme }) => theme.primary}; 81 | color: ${({ theme }) => theme.primary}; 82 | border-radius: 3px; 83 | font-weight: 500; 84 | cursor: pointer; 85 | display: flex; 86 | align-items: center; 87 | gap: 5px; 88 | font-size: 15px; 89 | border-radius: 100px; 90 | transition: all 0.3s ease; 91 | &:hover { 92 | background-color: ${({ theme }) => theme.primary}; 93 | color: ${({ theme }) => theme.text}; 94 | } 95 | `; 96 | 97 | const User = styled.div` 98 | display: flex; 99 | align-items: center; 100 | gap: 12px; 101 | font-weight: 500; 102 | font-size: 18px; 103 | padding: 0px 8px; 104 | color: ${({ theme }) => theme.text}; 105 | @media (max-width: 800px) { 106 | gap: 2px; 107 | } 108 | `; 109 | 110 | 111 | 112 | const Navbar = ({ menuOpen, setMenuOpen }) => { 113 | const [SignUpOpen, setSignUpOpen] = useState(false); 114 | const [SignInOpen, setSignInOpen] = useState(false); 115 | const [verifyEmail, setVerifyEmail] = useState(false); 116 | const dispatch = useDispatch(); 117 | const { currentUser } = useSelector((state) => state.user); 118 | const [users, setUsers] = useState([]); 119 | const token = localStorage.getItem("token"); 120 | const navigate = useNavigate(); 121 | 122 | const [notification, setNotification] = useState([]); 123 | useEffect(() => { 124 | getUsers(token).then((res) => { 125 | setUsers(res.data); 126 | }).catch((err) => { 127 | if (err.response.status === 401) { 128 | dispatch(logout()) 129 | } 130 | }); 131 | }, [dispatch]); 132 | 133 | 134 | const getNotifications = async () => { 135 | try { 136 | notifications(token).then((res) => { 137 | setNotification(res.data); 138 | console.log(notification); 139 | }); 140 | } catch (error) { 141 | console.log(error); 142 | } 143 | }; 144 | 145 | useEffect(() => { 146 | getNotifications(); 147 | }, []); 148 | 149 | useEffect(() => { 150 | if (!currentUser && !SignUpOpen) { 151 | setSignInOpen(true); 152 | setSignUpOpen(false); 153 | } else if (!currentUser && SignUpOpen) { 154 | setSignInOpen(false); 155 | setSignUpOpen(true); 156 | } 157 | console.log(currentUser); 158 | if (currentUser && !currentUser.verified) { 159 | setVerifyEmail(true); 160 | } else { 161 | setVerifyEmail(false); 162 | } 163 | }, [currentUser, SignInOpen, SignUpOpen, setVerifyEmail, users]); 164 | 165 | //Open the account dialog 166 | const [anchorEl, setAnchorEl] = useState(null); 167 | const open = Boolean(anchorEl); 168 | const id = open ? "simple-popover" : undefined; 169 | const handleClick = (event) => { 170 | setAnchorEl(event.currentTarget); 171 | }; 172 | 173 | const handleClose = () => { 174 | setAnchorEl(null); 175 | }; 176 | 177 | //Open the notification dialog 178 | const [anchorEl2, setAnchorEl2] = useState(null); 179 | const open2 = Boolean(anchorEl2); 180 | const id2 = open2 ? "simple-popover" : undefined; 181 | const notificationClick = (event) => { 182 | setAnchorEl2(event.currentTarget); 183 | }; 184 | 185 | const notificationClose = () => { 186 | setAnchorEl2(null); 187 | }; 188 | 189 | return ( 190 | <> 191 | 192 | 193 | setMenuOpen(!menuOpen)}> 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | {currentUser ? ( 202 | <> 203 | navigate('/chats')}> 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 224 | 229 | {currentUser.name.charAt(0)} 230 | 231 | 232 | 233 | 234 | ) : ( 235 | 238 | )} 239 | 240 | 241 | 242 | {currentUser && ( 243 | 250 | )} 251 | {currentUser && ( 252 | 260 | )} 261 | 262 | ); 263 | }; 264 | 265 | export default Navbar; 266 | -------------------------------------------------------------------------------- /client/src/components/InviteMembers.jsx: -------------------------------------------------------------------------------- 1 | import { CloseRounded, SearchOutlined, SendRounded } from "@mui/icons-material"; 2 | import { Modal } from "@mui/material"; 3 | import React, { useState } from "react"; 4 | import styled from "styled-components"; 5 | import { Avatar } from "@mui/material"; 6 | import { useSelector } from "react-redux"; 7 | import { inviteTeamMembers, inviteProjectMembers, searchUsers } from "../api/index"; 8 | import { openSnackbar } from "../redux/snackbarSlice"; 9 | import { useDispatch } from "react-redux"; 10 | import CircularProgress from "@mui/material/CircularProgress"; 11 | 12 | const Container = styled.div` 13 | width: 100%; 14 | height: 100%; 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | background-color: #000000a7; 19 | display: flex; 20 | justify-content: center; 21 | `; 22 | 23 | const Wrapper = styled.div` 24 | width: 430px; 25 | height: min-content; 26 | border-radius: 16px; 27 | background-color: ${({ theme }) => theme.bgLighter}; 28 | color: ${({ theme }) => theme.text}; 29 | padding: 10px; 30 | display: flex; 31 | margin-top: 80px; 32 | flex-direction: column; 33 | position: relative; 34 | @media (max-width: 768px) { 35 | width: 100%; 36 | } 37 | `; 38 | 39 | const Title = styled.div` 40 | font-size: 16px; 41 | font-weight: 500; 42 | color: ${({ theme }) => theme.text}; 43 | margin: 12px; 44 | `; 45 | 46 | const Search = styled.div` 47 | margin: 6px 6px; 48 | display: flex; 49 | align-items: center; 50 | justify-content: space-between; 51 | border-radius: 12px; 52 | color: ${({ theme }) => theme.textSoft}; 53 | background-color: ${({ theme }) => theme.bgDark}; 54 | `; 55 | 56 | const Input = styled.input` 57 | width: 100%; 58 | border: none; 59 | font-size: 14px; 60 | padding: 10px 20px; 61 | border-radius: 100px; 62 | background-color: transparent; 63 | outline: none; 64 | color: ${({ theme }) => theme.textSoft}; 65 | `; 66 | 67 | const UsersList = styled.div` 68 | padding: 18px 8px; 69 | display: flex; 70 | margin-bottom: 12px; 71 | flex-direction: column; 72 | gap: 12px; 73 | border-radius: 12px; 74 | color: ${({ theme }) => theme.textSoft}; 75 | `; 76 | 77 | const MemberCard = styled.div` 78 | display: flex; 79 | align-items: center; 80 | gap: 6px; 81 | justify-content: space-between; 82 | @media (max-width: 768px) { 83 | gap: 6px; 84 | } 85 | `; 86 | const UserData = styled.div` 87 | display: flex; 88 | align-items: center; 89 | gap: 12px; 90 | `; 91 | 92 | const Details = styled.div` 93 | gap: 4px; 94 | `; 95 | 96 | const Name = styled.div` 97 | font-size: 13px; 98 | font-weight: 500; 99 | max-width: 100px; 100 | word-wrap: break-word; 101 | color: ${({ theme }) => theme.textSoft}; 102 | `; 103 | 104 | const EmailId = styled.div` 105 | font-size: 10px; 106 | font-weight: 400; 107 | max-width: 100px; 108 | word-wrap: break-word; 109 | color: ${({ theme }) => theme.textSoft + "99"}; 110 | `; 111 | 112 | const Flex = styled.div` 113 | display: flex; 114 | flex-direction: row; 115 | gap: 2px; 116 | @media (max-width: 768px) { 117 | display: flex; 118 | flex-direction: column; 119 | align-items: center; 120 | } 121 | `; 122 | 123 | const Access = styled.div` 124 | padding: 6px 10px; 125 | border-radius: 12px; 126 | display: flex; 127 | align-items: center; 128 | justify-content: center; 129 | background-color: ${({ theme }) => theme.bgDark}; 130 | `; 131 | 132 | const Select = styled.select` 133 | border: none; 134 | font-size: 12px; 135 | background-color: transparent; 136 | outline: none; 137 | color: ${({ theme }) => theme.text}; 138 | background-color: ${({ theme }) => theme.bgDark}; 139 | `; 140 | 141 | const Role = styled.div` 142 | background-color: ${({ theme }) => theme.bgDark}; 143 | border-radius: 12px; 144 | `; 145 | 146 | const InviteButton = styled.button` 147 | padding: 6px 8px; 148 | background-color: transparent; 149 | border: 1px solid ${({ theme }) => theme.primary}; 150 | color: ${({ theme }) => theme.primary}; 151 | border-radius: 1px; 152 | font-weight: 500; 153 | cursor: pointer; 154 | display: flex; 155 | align-items: center; 156 | gap: 5px; 157 | font-size: 11px; 158 | border-radius: 10px; 159 | transition: all 0.3s ease; 160 | &:hover { 161 | background-color: ${({ theme }) => theme.primary}; 162 | color: ${({ theme }) => theme.text}; 163 | } 164 | `; 165 | 166 | const InviteMembers = ({ setInvitePopup, id, teamInvite }) => { 167 | const [search, setSearch] = React.useState(""); 168 | const [users, setUsers] = React.useState([]); 169 | const [message, setMessage] = React.useState(""); 170 | const { currentUser } = useSelector((state) => state.user); 171 | const token = localStorage.getItem("token"); 172 | 173 | const [role, setRole] = React.useState("Member"); 174 | const [access, setAccess] = React.useState("View Only"); 175 | const [Loading, setLoading] = React.useState(false); 176 | 177 | const handleSearch = async (e) => { 178 | setSearch(e.target.value); 179 | searchUsers(e.target.value, token) 180 | .then((res) => { 181 | if (res.status === 200) { 182 | setUsers(res.data); 183 | setMessage(""); 184 | } 185 | else { 186 | setUsers([]); 187 | setMessage(res.status); 188 | } 189 | }) 190 | .catch((err) => { 191 | setUsers([]); 192 | setMessage(err.message); 193 | }); 194 | }; 195 | 196 | const handleInvite = async (user) => { 197 | setLoading(true); 198 | const User = { 199 | id: user._id, 200 | name: user.name, 201 | email: user.email, 202 | role: role, 203 | access: access, 204 | }; 205 | console.log(User); 206 | if (teamInvite) { 207 | inviteTeamMembers(id, User, token) 208 | .then((res) => { 209 | console.log(res); 210 | if (res.status === 200) 211 | dispatch(openSnackbar({ message: `Invitation sent to ${user.name}`, type: "success" })); 212 | setLoading(false); 213 | }) 214 | .catch((err) => { 215 | dispatch(openSnackbar({ message: err.message, type: "error" })); 216 | setLoading(false); 217 | console.log(err); 218 | }); 219 | } else { 220 | console.log("project"); 221 | inviteProjectMembers(id, User, token) 222 | .then((res) => { 223 | if (res.status === 200) 224 | dispatch(openSnackbar({ message: `Invitation sent to ${user.name}`, type: "success" })); 225 | setLoading(false); 226 | }) 227 | .catch((err) => { 228 | console.log(err); 229 | dispatch(openSnackbar({ message: err.message, type: "error" })); 230 | setLoading(false); 231 | }); 232 | } 233 | }; 234 | 235 | const dispatch = useDispatch(); 236 | 237 | return ( 238 | setInvitePopup(false)}> 239 | 240 | 241 | setInvitePopup(false)} 250 | /> 251 | Invite Members 252 | 253 | handleSearch(e)} 256 | /> 257 | 261 | 262 | {message &&
{message}
} 263 | 264 | {users.map((user) => ( 265 | 266 | 267 | 268 | {user.name[0]} 269 | 270 |
271 | {user.name} 272 | {user.email} 273 |
274 |
275 | 276 | 277 | 284 | 285 | 286 | setRole(e.target.value)} /> 287 | 288 | 289 | 290 | handleInvite(user)}> 291 | {Loading ? ( 292 | 293 | ) : (<> 294 | 295 | Invite 296 | )} 297 | 298 |
299 | ))} 300 |
301 | 302 |
303 |
304 |
305 | ); 306 | }; 307 | 308 | export default InviteMembers; 309 | -------------------------------------------------------------------------------- /client/src/components/WorkDetails.jsx: -------------------------------------------------------------------------------- 1 | import { IconButton, Modal, Snackbar } from "@mui/material"; 2 | import React, { useState, useEffect } from "react"; 3 | import CircularProgress from "@mui/material/CircularProgress"; 4 | import styled from "styled-components"; 5 | import { 6 | CloseRounded, 7 | } from "@mui/icons-material"; 8 | import { Avatar } from "@mui/material"; 9 | import { useDispatch } from "react-redux"; 10 | import { tagColors } from "../data/data"; 11 | import TaskCard from "./TaskCard"; 12 | 13 | const Container = styled.div` 14 | width: 100%; 15 | height: 100%; 16 | overflow-y: scroll !important; 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | background-color: #000000a7; 21 | display: flex; 22 | justify-content: center; 23 | align-item: center; 24 | `; 25 | 26 | const Wrapper = styled.div` 27 | width: 100%; 28 | height: min-content; 29 | margin: 2%; 30 | max-width: 800px; 31 | border-radius: 8px; 32 | background-color: ${({ theme }) => theme.bgLighter}; 33 | color: ${({ theme }) => theme.text}; 34 | display: flex; 35 | flex-direction: column; 36 | position: relative; 37 | `; 38 | 39 | const FlexDisplay = styled.div` 40 | display: flex; 41 | justify-content: space-between; 42 | align-items: center; 43 | `; 44 | 45 | const Top = styled.div` 46 | padding: 6px 16px 0px 16px; 47 | `; 48 | 49 | const Title = styled.div` 50 | font-size: 20px; 51 | @media screen and (max-width: 480px) { 52 | font-size: 20px; 53 | } 54 | font-weight: 500; 55 | color: ${({ theme }) => theme.text}; 56 | flex: 7; 57 | line-height: 1.5; 58 | overflow: hidden; 59 | text-overflow: ellipsis; 60 | display: -webkit-box; 61 | -webkit-line-clamp: 2; /* number of lines to show */ 62 | line-clamp: 2; 63 | -webkit-box-orient: vertical; 64 | `; 65 | 66 | const Desc = styled.div` 67 | font-size: 13px; 68 | font-weight: 400; 69 | color: ${({ theme }) => theme.soft2}; 70 | flex: 7; 71 | line-height: 1.5; 72 | overflow: hidden; 73 | text-overflow: ellipsis; 74 | display: -webkit-box;m 75 | -webkit-line-clamp: 2; /* number of lines to show */ 76 | line-clamp: 2; 77 | -webkit-box-orient: vertical; 78 | `; 79 | 80 | const Tags = styled.div` 81 | display: flex; 82 | flex-wrap: wrap; 83 | flex-direction: row; 84 | gap: 4px; 85 | margin-top: 8px; 86 | `; 87 | 88 | const Tag = styled.div` 89 | padding: 3px 8px; 90 | border-radius: 8px; 91 | color: ${({ tagColor, theme }) => tagColor + theme.lightAdd}; 92 | background-color: ${({ tagColor, theme }) => tagColor + "10"}; 93 | font-size: 12px; 94 | font-weight: 500; 95 | `; 96 | 97 | const Members = styled.div` 98 | display: flex; 99 | flex-direction: row; 100 | align-items: center; 101 | `; 102 | 103 | const Hr = styled.hr` 104 | margin: 2px 0px; 105 | border: 0.5px solid ${({ theme }) => theme.soft + "99"}; 106 | `; 107 | 108 | const Bottom = styled.div` 109 | display: flex; 110 | justify-content: space-between; 111 | align-items: center; 112 | margin: 20px 0px 14px 0px; 113 | `; 114 | 115 | const Table = styled.div` 116 | width: 100%; 117 | display: flex; 118 | flex-direction: column; 119 | padding: 6px 10px; 120 | `; 121 | 122 | const TableHeader = styled.div` 123 | display: flex; 124 | justify-content: space-between; 125 | align-items: center; 126 | padding: 12px 10px; 127 | gap: 8px; 128 | border-radius: 8px 8px 0px 0px; 129 | border: 1.8px solid ${({ theme }) => theme.soft + "99"}; 130 | background-color: ${({ theme }) => theme.card}; 131 | color: ${({ theme }) => theme.text}; 132 | cursor: pointer; 133 | background-color: ${({ theme }) => theme.bgDark}; 134 | `; 135 | 136 | 137 | const No = styled.div` 138 | width: 4%; 139 | font-size: 12px; 140 | text-overflow: ellipsis; 141 | font-weight: 500; 142 | color: ${({ theme }) => theme.soft2}; 143 | display: -webkit-box; 144 | -webkit-line-clamp: 5; /* number of lines to show */ 145 | line-clamp: 5; 146 | -webkit-box-orient: vertical; 147 | 148 | ${({ completed, theme }) => 149 | completed === "Completed" && 150 | ` 151 | text-decoration: line-through; 152 | `} 153 | `; 154 | 155 | const Task = styled.div` 156 | width: 50%; 157 | font-size: 12px; 158 | font-weight: 500; 159 | color: ${({ theme }) => theme.soft2}; 160 | display: -webkit-box; 161 | -webkit-line-clamp: 5; /* number of lines to show */ 162 | line-clamp: 5; 163 | -webkit-box-orient: vertical; 164 | padding: 6px; 165 | 166 | ${({ completed, theme }) => 167 | completed === "Completed" && 168 | ` 169 | text-decoration: line-through; 170 | `} 171 | `; 172 | 173 | const Date = styled.div` 174 | font-size: 12px; 175 | font-weight: 500; 176 | text-align: center; 177 | text-overflow: ellipsis; 178 | width: 14%; 179 | color: ${({ theme }) => theme.soft2}; 180 | ${({ enddate, theme }) => 181 | enddate && 182 | ` 183 | color: ${theme.pink}; 184 | `} 185 | display: -webkit-box; 186 | -webkit-line-clamp: 5; /* number of lines to show */ 187 | line-clamp: 5; 188 | -webkit-box-orient: vertical; 189 | 190 | ${({ completed, theme }) => 191 | completed === "Completed" && 192 | ` 193 | text-decoration: line-through; 194 | `} 195 | `; 196 | 197 | 198 | const WorkDetails = ({ setOpenWork, work }) => { 199 | const dispatch = useDispatch(); 200 | const [task, setTask] = useState(work.tasks); 201 | const [tag, setTag] = useState(work.tags); 202 | const [completed, setCompleted] = useState(0); 203 | const [progress, setProgress] = useState(0); 204 | const [members, setMembers] = useState([]); 205 | 206 | useEffect(() => { 207 | let count = 0; 208 | let Members = []; 209 | task.forEach((item) => { 210 | if (item.status === "Completed") { 211 | count++; 212 | } 213 | 214 | if (item.members.length > 0) { 215 | item.members.forEach((items) => { 216 | //check if the same member is already present in the array 217 | let isPresent = Members.some((member) => member._id === items._id); 218 | if (!isPresent) 219 | { 220 | Members.push(items); 221 | } 222 | 223 | }); 224 | } 225 | }); 226 | setCompleted(count); 227 | setProgress((completed / task.length) * 100); 228 | setMembers(Members); 229 | }, [work]); 230 | 231 | return ( 232 | setOpenWork(false)}> 233 | 234 | 235 | 236 | 237 | {work.title} 238 | setOpenWork(false)} 244 | > 245 | 246 | 247 | 248 | 249 | {work.desc} 250 | 251 | 252 | {work.tags.map((tag) => ( 253 | 258 | {tag} 259 | 260 | ))} 261 | 262 | 267 | {members.slice(0, 10).map((member) => ( 268 | 277 | {member.name.charAt(0)} 278 | 279 | ))} 280 | {members.length > 9 && ( 281 | 289 | +{members.length - 9} 290 | 291 | )} 292 | 293 | 294 |
295 | 296 | 297 | 298 | No 299 | 302 | Tasks 303 | 304 | 305 | Start Date 306 | 307 | 308 | Deadline 309 | 310 | 311 | Status 312 | 313 | 321 | Members 322 | 323 | 324 | {task.map((item, index) => ( 325 | 326 | ))} 327 |
328 |
329 |
330 |
331 |
332 | ); 333 | }; 334 | 335 | export default WorkDetails; 336 | --------------------------------------------------------------------------------