├── .env ├── code.png ├── src ├── utls │ └── currencyFomrmatter.js ├── index.css ├── components │ ├── Footer.js │ ├── Navbar.js │ ├── ProjectDetails.js │ └── ProjectForm.js ├── hooks │ ├── useAuthContext.js │ ├── useProjectsContext.js │ ├── useLogout.js │ ├── useLogin.js │ └── useSignup.js ├── index.js ├── context │ ├── AuthContext.js │ └── ProjectContext.js ├── App.js └── pages │ ├── Home.js │ ├── Login.js │ └── Signup.js ├── tailwind.config.js ├── .gitignore ├── public └── index.html ├── package.json └── README.md /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_URL=https://proxima-csb4.onrender.com -------------------------------------------------------------------------------- /code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadrilamin1999/proxima-client/HEAD/code.png -------------------------------------------------------------------------------- /src/utls/currencyFomrmatter.js: -------------------------------------------------------------------------------- 1 | export const currencyFormatter = (amount) => { 2 | return amount?.toLocaleString("en-US", { 3 | style: "currency", 4 | currency: "USD", 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"); 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | /* font-family: 'Inter', sans-serif; */ 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: { 6 | fontFamily: { 7 | sans: ["Inter, sans-serif"], 8 | }, 9 | }, 10 | }, 11 | plugins: [], 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | const Footer = () => { 2 | return ( 3 |
4 |
5 |

©{new Date().getFullYear()} Proxima, All Rights Reserved

6 |
7 |
8 | ); 9 | }; 10 | 11 | export default Footer; 12 | -------------------------------------------------------------------------------- /src/hooks/useAuthContext.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { AuthContext } from "../context/AuthContext"; 3 | 4 | export const useAuthContext = () => { 5 | const context = useContext(AuthContext); 6 | 7 | if (!context) { 8 | throw new Error( 9 | "You must call useAuthContext inside a AuthContextProvider" 10 | ); 11 | } 12 | 13 | return context; 14 | }; 15 | -------------------------------------------------------------------------------- /src/hooks/useProjectsContext.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { ProjectContext } from "../context/ProjectContext"; 3 | 4 | export const useProjectsContext = () => { 5 | const context = useContext(ProjectContext); 6 | 7 | if (!context) { 8 | throw new Error( 9 | "You must call useProjectsContext inside a ProjectContextProvider" 10 | ); 11 | } 12 | 13 | return context; 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | proxima 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /src/hooks/useLogout.js: -------------------------------------------------------------------------------- 1 | import { useAuthContext } from "./useAuthContext"; 2 | import { useProjectsContext } from "./useProjectsContext"; 3 | 4 | export const useLogout = () => { 5 | const { dispatch: logoutDispatch } = useAuthContext(); 6 | const { dispatch: projectsDispatch } = useProjectsContext(); 7 | 8 | const logout = () => { 9 | // clear ls 10 | localStorage.removeItem("user"); 11 | 12 | // dispatch logout 13 | logoutDispatch({ type: "LOGOUT" }); 14 | projectsDispatch({ type: "SET_PROJECTS", payload: [] }); 15 | }; 16 | 17 | return { logout }; 18 | }; 19 | -------------------------------------------------------------------------------- /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 { BrowserRouter } from "react-router-dom"; 6 | import { ProjectContextProvider } from "./context/ProjectContext"; 7 | import { AuthContextProvider } from "./context/AuthContext"; 8 | 9 | const root = ReactDOM.createRoot(document.getElementById("root")); 10 | root.render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/context/AuthContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useReducer } from "react"; 2 | 3 | const initialState = { 4 | user: localStorage.getItem("user") 5 | ? JSON.parse(localStorage.getItem("user")) 6 | : null, 7 | }; 8 | 9 | export const authReducer = (state, action) => { 10 | switch (action.type) { 11 | case "LOGIN": 12 | return { 13 | ...state, 14 | user: action.payload, 15 | }; 16 | case "LOGOUT": 17 | return { 18 | ...state, 19 | user: null, 20 | }; 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | export const AuthContext = createContext(); 27 | 28 | export const AuthContextProvider = ({ children }) => { 29 | const [state, dispatch] = useReducer(authReducer, initialState); 30 | 31 | return ( 32 | 33 | {children} 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { Routes, Route, Navigate } from "react-router-dom"; 2 | import Navbar from "./components/Navbar"; 3 | import Footer from "./components/Footer"; 4 | import Home from "./pages/Home"; 5 | import Login from "./pages/Login"; 6 | import Signup from "./pages/Signup"; 7 | import { useAuthContext } from "./hooks/useAuthContext"; 8 | 9 | function App() { 10 | const { user } = useAuthContext(); 11 | return ( 12 |
13 | 14 | 15 | : } /> 16 | : } 19 | /> 20 | : } 23 | /> 24 | } /> 25 | 26 |
28 | ); 29 | } 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "moment": "^2.29.4", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "react-router-dom": "^6.8.1", 13 | "react-scripts": "5.0.1", 14 | "web-vitals": "^2.1.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "tailwindcss": "^3.2.7" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/hooks/useLogin.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useAuthContext } from "./useAuthContext"; 3 | 4 | export const useLogin = () => { 5 | const [error, setError] = useState(null); 6 | const [loading, setLoading] = useState(false); 7 | 8 | const { dispatch } = useAuthContext(); 9 | 10 | const login = async (email, password) => { 11 | setLoading(true); 12 | setError(null); 13 | 14 | const res = await fetch( 15 | `${process.env.REACT_APP_BASE_URL}/api/user/login`, 16 | { 17 | method: "POST", 18 | headers: { 19 | "Content-Type": "application/json", 20 | }, 21 | body: JSON.stringify({ email, password }), 22 | } 23 | ); 24 | 25 | const json = await res.json(); 26 | console.log(json); 27 | // res.ok === false 28 | if (!res.ok) { 29 | setLoading(false); 30 | setError(json.error); 31 | } 32 | 33 | // res.ok === true 34 | if (res.ok) { 35 | // update auth context 36 | dispatch({ type: "LOGIN", payload: json }); 37 | // save user to local storage 38 | localStorage.setItem("user", JSON.stringify(json)); 39 | 40 | setLoading(false); 41 | } 42 | }; 43 | 44 | return { login, error, loading }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/hooks/useSignup.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useAuthContext } from "./useAuthContext"; 3 | 4 | export const useSignup = () => { 5 | const [error, setError] = useState(null); 6 | const [loading, setLoading] = useState(false); 7 | 8 | const { dispatch } = useAuthContext(); 9 | 10 | const signup = async (email, password) => { 11 | setLoading(true); 12 | setError(null); 13 | 14 | const res = await fetch( 15 | `${process.env.REACT_APP_BASE_URL}/api/user/signup`, 16 | { 17 | method: "POST", 18 | headers: { 19 | "Content-Type": "application/json", 20 | }, 21 | body: JSON.stringify({ email, password }), 22 | } 23 | ); 24 | 25 | const json = await res.json(); 26 | console.log(json); 27 | // res.ok === false 28 | if (!res.ok) { 29 | setLoading(false); 30 | setError(json.error); 31 | } 32 | 33 | // res.ok === true 34 | if (res.ok) { 35 | // update auth context 36 | dispatch({ type: "LOGIN", payload: json }); 37 | // save user to local storage 38 | localStorage.setItem("user", JSON.stringify(json)); 39 | 40 | setLoading(false); 41 | } 42 | }; 43 | 44 | return { signup, error, loading }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/context/ProjectContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useReducer } from "react"; 2 | 3 | const initialState = { 4 | projects: [], 5 | }; 6 | 7 | export const projectReducer = (state, action) => { 8 | switch (action.type) { 9 | case "SET_PROJECTS": 10 | return { 11 | projects: action.payload, 12 | }; 13 | case "CREATE_PROJECTS": 14 | return { 15 | projects: [action.payload, ...state.projects], 16 | }; 17 | case "DELETE_PROJECT": 18 | return { 19 | ...state, 20 | projects: state.projects.filter( 21 | (project) => project._id !== action.payload._id 22 | ), 23 | }; 24 | case "UPDATE_PROJECT": 25 | const [existingProject] = state.projects.filter( 26 | (project) => project._id === action.payload._id 27 | ); 28 | return { 29 | ...state, 30 | projects: [ 31 | action.payload, 32 | ...state.projects.filter( 33 | (project) => project._id !== existingProject._id 34 | ), 35 | ], 36 | }; 37 | default: 38 | return state; 39 | } 40 | }; 41 | export const ProjectContext = createContext(); 42 | 43 | export const ProjectContextProvider = ({ children }) => { 44 | const [state, dispatch] = useReducer(projectReducer, initialState); 45 | 46 | return ( 47 | 48 | {children} 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import ProjectDetails from "../components/ProjectDetails"; 3 | import ProjectForm from "../components/ProjectForm"; 4 | import { useProjectsContext } from "../hooks/useProjectsContext"; 5 | import { useAuthContext } from "../hooks/useAuthContext"; 6 | 7 | const Home = () => { 8 | const { projects, dispatch } = useProjectsContext(); 9 | const { user } = useAuthContext(); 10 | useEffect(() => { 11 | const getAllProjects = async () => { 12 | const res = await fetch( 13 | `${process.env.REACT_APP_BASE_URL}/api/projects`, 14 | { 15 | headers: { 16 | Authorization: `Bearer ${user.token}`, 17 | }, 18 | } 19 | ); 20 | const json = await res.json(); 21 | 22 | if (res.ok) { 23 | dispatch({ type: "SET_PROJECTS", payload: json }); 24 | } 25 | }; 26 | if (user) { 27 | getAllProjects(); 28 | } 29 | }, [dispatch, user]); 30 | return ( 31 |
32 |
33 |

34 | {projects.length < 1 ? "No projects" : "All Projects"} 35 |

36 |
37 | {projects && 38 | projects.map((project) => ( 39 | 40 | ))} 41 |
42 |
43 | 44 |
45 | ); 46 | }; 47 | 48 | export default Home; 49 | -------------------------------------------------------------------------------- /src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { useAuthContext } from "../hooks/useAuthContext"; 3 | import { useLogout } from "../hooks/useLogout"; 4 | 5 | const Navbar = () => { 6 | const { user } = useAuthContext(); 7 | 8 | const { logout } = useLogout(); 9 | 10 | const handleLogout = () => { 11 | logout(); 12 | }; 13 | return ( 14 |
15 |
16 | 20 | Proxima 21 | 22 | 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default Navbar; 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proxima - Project Management Web Application 2 | Proxima is a web application built for project managers to efficiently plan, execute and monitor projects. It provides a user-friendly interface that simplifies project management tasks such as task assignment, progress tracking, and deadline setting. This application is built using React, Node.js, Express, MongoDB, and Tailwind CSS. 3 | 4 | ## Features 5 | - Effortlessly manage projects: Create, update, and delete, projects quickly and easily using the user-friendly interface. 6 | = Robust security features: The app features highly secure JWT authentication and frontend route protection, ensuring that your data is always safe and secure. 7 | = User-specific project views: Users can only see the projects they have created, ensuring that project information is kept private and secure. 8 | = Intuitive and streamlined UI: The app’s sleek and intuitive user interface makes managing projects a breeze. 9 | 10 | 11 | ## Tools 12 | - React 13 | - Node.js 14 | - Express 15 | - MongoDB 16 | - Tailwind CSS 17 | - Moment.js 18 | - Mongoose 19 | - JSON Web Token 20 | - CORS 21 | - Bcrypt 22 | - Validator 23 | 24 | ## Installation 25 | 1. Clone the repository: git clone https://github.com/yourusername/proxima.git 26 | 2. Navigate to the project directory: `cd proxima-client` 27 | 3. Install dependencies:`npm install` 28 | 4. Start the application:`npm start` 29 | 30 | ## Conclusion 31 | Proxima is an efficient and user-friendly project management web application. Its features such as user authentication, task management, and progress tracking make it an essential tool for project managers. The application is built using modern web technologies such as React, Node.js, Express, and MongoDB. With its intuitive user interface and features, it provides an optimal user experience for project management. 32 | -------------------------------------------------------------------------------- /src/pages/Login.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useLogin } from "../hooks/useLogin.js"; 3 | 4 | const Login = () => { 5 | const [email, setEmail] = useState(""); 6 | const [password, setPassword] = useState(""); 7 | 8 | const { login, error, loading } = useLogin(); 9 | 10 | const handleLogin = async (e) => { 11 | e.preventDefault(); 12 | 13 | // login user 14 | await login(email, password); 15 | }; 16 | return ( 17 |
21 |

Login

22 | 23 |
24 | 30 | setEmail(e.target.value)} 36 | className="bg-transparent border border-gray-300 py-3 px-5 rounded-md outline-none focus:border-sky-400 duration-300" 37 | /> 38 |
39 | 40 |
41 | 47 | setPassword(e.target.value)} 53 | className="bg-transparent border border-gray-300 py-3 px-5 rounded-md outline-none focus:border-sky-400 duration-300" 54 | /> 55 |
56 | 57 | 64 | 65 | {error && ( 66 |

67 | {error} 68 |

69 | )} 70 |
71 | ); 72 | }; 73 | 74 | export default Login; 75 | -------------------------------------------------------------------------------- /src/pages/Signup.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useSignup } from "../hooks/useSignup"; 3 | 4 | const Signup = () => { 5 | const [email, setEmail] = useState(""); 6 | const [password, setPassword] = useState(""); 7 | 8 | const { signup, error, loading } = useSignup(); 9 | 10 | const handleSignup = async (e) => { 11 | e.preventDefault(); 12 | 13 | // signup user 14 | await signup(email, password); 15 | }; 16 | return ( 17 |
21 |

Signup

22 | 23 |
24 | 30 | setEmail(e.target.value)} 36 | className="bg-transparent border border-gray-300 py-3 px-5 rounded-md outline-none focus:border-sky-400 duration-300" 37 | /> 38 |
39 | 40 |
41 | 47 | setPassword(e.target.value)} 53 | className="bg-transparent border border-gray-300 py-3 px-5 rounded-md outline-none focus:border-sky-400 duration-300" 54 | /> 55 |
56 | 57 | 64 | {error && ( 65 |

66 | {error} 67 |

68 | )} 69 |
70 | ); 71 | }; 72 | 73 | export default Signup; 74 | -------------------------------------------------------------------------------- /src/components/ProjectDetails.js: -------------------------------------------------------------------------------- 1 | import { currencyFormatter } from "../utls/currencyFomrmatter"; 2 | import { useProjectsContext } from "../hooks/useProjectsContext"; 3 | import { useAuthContext } from "../hooks/useAuthContext"; 4 | import moment from "moment"; 5 | import { useState } from "react"; 6 | import ProjectForm from "./ProjectForm"; 7 | 8 | const ProjectDetails = ({ project }) => { 9 | const [isModalOpen, setIsModalOpen] = useState(false); 10 | const [isOverlayOpen, setIsOverlayOpen] = useState(false); 11 | const { dispatch } = useProjectsContext(); 12 | const { user } = useAuthContext(); 13 | 14 | const handleDelete = async () => { 15 | if (!user) { 16 | return; 17 | } 18 | const res = await fetch( 19 | `${process.env.REACT_APP_BASE_URL}/api/projects/${project._id}`, 20 | { 21 | method: "DELETE", 22 | headers: { 23 | Authorization: `Bearer ${user.token}`, 24 | }, 25 | } 26 | ); 27 | const json = await res.json(); 28 | 29 | if (res.ok) { 30 | dispatch({ type: "DELETE_PROJECT", payload: json }); 31 | } 32 | }; 33 | const handleUpdate = async () => { 34 | setIsModalOpen(true); 35 | setIsOverlayOpen(true); 36 | }; 37 | 38 | const handleOverlay = () => { 39 | setIsModalOpen(false); 40 | setIsOverlayOpen(false); 41 | }; 42 | return ( 43 |
44 |
45 | ID: {project._id} 46 |

47 | {project.title} 48 |

49 | 50 | {project.tech} 51 | 52 |
53 |
54 |
55 | Budget: {currencyFormatter(project.budget)} 56 | 57 | Added: {moment(project.createdAt).format("DD-MMM hh:mm A")} 58 | 59 | 60 | Updated: {moment(project.updatedAt).format("DD-MMM hh:mm A")} 61 | 62 |
63 |
64 | Manager: {project.manager} 65 | Developers: {project.dev} 66 | 67 | Duration:{" "} 68 | {`${project.duration} week${project.duration === 1 ? "" : "s"}`} 69 | 70 |
71 |
72 |
73 | 79 | 85 |
86 | {/* overlay */} 87 |
93 | {/* modal */} 94 |
99 |

100 | Update project 101 |

102 | 107 |
108 |
109 | ); 110 | }; 111 | 112 | export default ProjectDetails; 113 | -------------------------------------------------------------------------------- /src/components/ProjectForm.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useProjectsContext } from "../hooks/useProjectsContext"; 3 | import { useAuthContext } from "../hooks/useAuthContext"; 4 | 5 | const ProjectForm = ({ project, setIsModalOpen, setIsOverlayOpen }) => { 6 | const [title, setTitle] = useState(project ? project.title : ""); 7 | const [tech, setTech] = useState(project ? project.tech : ""); 8 | const [budget, setBudget] = useState(project ? project.budget : ""); 9 | const [duration, setDuration] = useState(project ? project.duration : ""); 10 | const [manager, setManager] = useState(project ? project.manager : ""); 11 | const [dev, setDev] = useState(project ? project.dev : ""); 12 | const [error, setError] = useState(null); 13 | const [emptyFields, setEmptyfields] = useState([]); 14 | 15 | const { dispatch } = useProjectsContext(); 16 | const { user } = useAuthContext(); 17 | 18 | const handleSubmit = async (e) => { 19 | e.preventDefault(); 20 | 21 | if (!user) { 22 | setError("You must be logged in!"); 23 | return; 24 | } 25 | 26 | // data 27 | const projectObj = { title, tech, budget, duration, manager, dev }; 28 | 29 | // if there is no project 30 | if (!project) { 31 | // post request 32 | const res = await fetch( 33 | `${process.env.REACT_APP_BASE_URL}/api/projects`, 34 | { 35 | method: "POST", 36 | headers: { 37 | "Content-Type": "application/json", 38 | Authorization: `Bearer ${user.token}`, 39 | }, 40 | body: JSON.stringify(projectObj), 41 | } 42 | ); 43 | 44 | const json = await res.json(); 45 | 46 | // ste error 47 | if (!res.ok) { 48 | setError(json.error); 49 | setEmptyfields(json.emptyFields); 50 | } 51 | 52 | // reset 53 | if (res.ok) { 54 | setTitle(""); 55 | setTech(""); 56 | setBudget(""); 57 | setDuration(""); 58 | setManager(""); 59 | setDev(""); 60 | setError(null); 61 | setEmptyfields([]); 62 | dispatch({ type: "CREATE_PROJECTS", payload: json }); 63 | } 64 | return; 65 | } 66 | 67 | // there is a project, send patch request 68 | if (project) { 69 | //send patch 70 | const res = await fetch( 71 | `${process.env.REACT_APP_BASE_URL}/api/projects/${project._id}`, 72 | { 73 | method: "PATCH", 74 | headers: { 75 | "Content-Type": "application/json", 76 | Authorization: `Bearer ${user.token}`, 77 | }, 78 | body: JSON.stringify(projectObj), 79 | } 80 | ); 81 | 82 | const json = await res.json(); 83 | // !res.ok 84 | if (!res.ok) { 85 | setError(json.error); 86 | setEmptyfields(json.emptyFields); 87 | } 88 | // res.ok 89 | if (res.ok) { 90 | setError(null); 91 | setEmptyfields([]); 92 | 93 | // dispatch 94 | dispatch({ type: "UPDATE_PROJECT", payload: json }); 95 | 96 | // close overlay modal 97 | setIsModalOpen(false); 98 | setIsOverlayOpen(false); 99 | } 100 | return; 101 | } 102 | }; 103 | return ( 104 |
105 |

110 | Add a new project 111 |

112 | 113 |
114 | 120 | setTitle(e.target.value)} 123 | type="text" 124 | placeholder="e.g e-commerce website" 125 | id="title" 126 | className={`bg-transparent border border-gray-300 py-3 px-5 rounded-md outline-none focus:border-sky-400 duration-300 ${ 127 | emptyFields?.includes("title") 128 | ? "border-rose-500" 129 | : "border-slate-500" 130 | }`} 131 | /> 132 |
133 |
134 | 140 | setTech(e.target.value)} 143 | type="text" 144 | placeholder="node.js, react, redux etc." 145 | id="tech" 146 | className={`bg-transparent border border-gray-300 py-3 px-5 rounded-md outline-none focus:border-sky-400 duration-300 ${ 147 | emptyFields?.includes("tech") 148 | ? "border-rose-500" 149 | : "border-slate-500" 150 | }`} 151 | /> 152 |
153 |
154 | 160 | setBudget(e.target.value)} 163 | type="number" 164 | placeholder="e.g 500" 165 | id="budget" 166 | className={`bg-transparent border border-gray-300 py-3 px-5 rounded-md outline-none focus:border-sky-400 duration-300 ${ 167 | emptyFields?.includes("budget") 168 | ? "border-rose-500" 169 | : "border-slate-500" 170 | }`} 171 | /> 172 |
173 |
174 | 180 | setDuration(e.target.value)} 183 | type="number" 184 | placeholder="e.g 3" 185 | id="duration" 186 | className={`bg-transparent border border-gray-300 py-3 px-5 rounded-md outline-none focus:border-sky-400 duration-300 ${ 187 | emptyFields?.includes("duration") 188 | ? "border-rose-500" 189 | : "border-slate-500" 190 | }`} 191 | /> 192 |
193 |
194 | 200 | setManager(e.target.value)} 203 | type="text" 204 | placeholder="e.g Mr Habib" 205 | id="manager" 206 | className={`bg-transparent border border-gray-300 py-3 px-5 rounded-md outline-none focus:border-sky-400 duration-300 ${ 207 | emptyFields?.includes("manager") 208 | ? "border-rose-500" 209 | : "border-slate-500" 210 | }`} 211 | /> 212 |
213 |
214 | 220 | setDev(e.target.value)} 223 | type="number" 224 | placeholder="e.g 4" 225 | id="dev" 226 | className={`bg-transparent border border-gray-300 py-3 px-5 rounded-md outline-none focus:border-sky-400 duration-300 ${ 227 | emptyFields?.includes("developer") 228 | ? "border-rose-500" 229 | : "border-slate-500" 230 | }`} 231 | /> 232 |
233 | 234 | 240 | {error && ( 241 |

242 | {error} 243 |

244 | )} 245 |
246 | ); 247 | }; 248 | 249 | export default ProjectForm; 250 | --------------------------------------------------------------------------------