├── public ├── _redirects └── index.html ├── .env ├── src ├── utilities │ └── currencyFormatter.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 ├── package.json └── README.md /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_URL = https://proxima-q7sw.onrender.com -------------------------------------------------------------------------------- /src/utilities/currencyFormatter.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 | /* google font */ 2 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"); 3 | 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 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 | 8 | ); 9 | }; 10 | 11 | export default Footer; 12 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Proxima - a project management app 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /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 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /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 localstorage 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 { BrowserRouter } from "react-router-dom"; 4 | import App from "./App"; 5 | import "./index.css"; 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 | 17 | case "LOGOUT": 18 | return { 19 | ...state, 20 | user: null, 21 | }; 22 | 23 | default: 24 | return state; 25 | } 26 | }; 27 | 28 | export const AuthContext = createContext(); 29 | 30 | export const AuthContextProvider = ({ children }) => { 31 | const [state, dispatch] = useReducer(authReducer, initialState); 32 | 33 | return ( 34 | 35 | {children} 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { Routes, Route, Navigate } from "react-router-dom"; 2 | import Footer from "./components/Footer"; 3 | import Navbar from "./components/Navbar"; 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 | 12 | return ( 13 |
14 | 15 | 16 | : } /> 17 | : } 20 | /> 21 | : } 24 | /> 25 | } /> 26 | 27 |
29 | ); 30 | } 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /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.6" 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 | 27 | if (!res.ok) { 28 | setLoading(false); 29 | setError(json.error); 30 | } 31 | 32 | if (res.ok) { 33 | // update the auth context 34 | dispatch({ type: "LOGIN", payload: json }); 35 | 36 | // save the user to local storage 37 | localStorage.setItem("user", JSON.stringify(json)); 38 | 39 | setLoading(false); 40 | } 41 | }; 42 | 43 | return { login, error, loading }; 44 | }; 45 | -------------------------------------------------------------------------------- /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 | 27 | if (!res.ok) { 28 | setLoading(false); 29 | setError(json.error); 30 | } 31 | 32 | if (res.ok) { 33 | // update the auth context 34 | dispatch({ type: "LOGIN", payload: json }); 35 | 36 | // save the user to local storage 37 | localStorage.setItem("user", JSON.stringify(json)); 38 | 39 | setLoading(false); 40 | } 41 | }; 42 | 43 | return { signup, error, loading }; 44 | }; 45 | -------------------------------------------------------------------------------- /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 | const { logout } = useLogout(); 8 | 9 | const handleLogout = () => { 10 | logout(); 11 | }; 12 | 13 | return ( 14 |
15 | 16 | Proxima 17 | 18 | 19 | 45 |
46 | ); 47 | }; 48 | 49 | export default Navbar; 50 | -------------------------------------------------------------------------------- /src/context/ProjectContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useReducer } from "react"; 2 | 3 | const initialState = { 4 | projects: [], 5 | }; 6 | 7 | export const projectsReducer = (state, action) => { 8 | switch (action.type) { 9 | case "SET_PROJECTS": 10 | return { 11 | ...state, 12 | projects: action.payload, 13 | }; 14 | 15 | case "CREATE_PROJECT": 16 | return { 17 | ...state, 18 | projects: [action.payload, ...state.projects], 19 | }; 20 | 21 | case "UPDATE_PROJECT": 22 | const [existingProject] = state.projects.filter( 23 | (project) => project._id === action.payload._id 24 | ); 25 | 26 | return { 27 | ...state, 28 | projects: [ 29 | action.payload, 30 | ...state.projects.filter( 31 | (project) => project._id !== existingProject._id 32 | ), 33 | ], 34 | }; 35 | 36 | case "DELETE_PROJECT": 37 | return { 38 | ...state, 39 | projects: state.projects.filter( 40 | (project) => project._id !== action.payload._id 41 | ), 42 | }; 43 | 44 | default: 45 | return state; 46 | } 47 | }; 48 | 49 | export const ProjectContext = createContext(); 50 | 51 | export const ProjectContextProvider = ({ children }) => { 52 | const [state, dispatch] = useReducer(projectsReducer, initialState); 53 | 54 | return ( 55 | 56 | {children} 57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /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 | 6 | import { useAuthContext } from "../hooks/useAuthContext"; 7 | 8 | const Home = () => { 9 | const { projects, dispatch } = useProjectsContext(); 10 | const { user } = useAuthContext(); 11 | 12 | useEffect(() => { 13 | const getAllProjects = async () => { 14 | const res = await fetch( 15 | `${process.env.REACT_APP_BASE_URL}/api/projects`, 16 | { 17 | headers: { 18 | Authorization: `Bearer ${user.token}`, 19 | }, 20 | } 21 | ); 22 | 23 | const json = await res.json(); 24 | 25 | if (res.ok) { 26 | dispatch({ type: "SET_PROJECTS", payload: json }); 27 | } 28 | }; 29 | 30 | if (user) { 31 | getAllProjects(); 32 | } 33 | }, [dispatch, user]); 34 | 35 | return ( 36 |
37 |
38 |

39 | {projects.length < 1 ? "No Projects" : "All Projects"} 40 |

41 |
42 | {projects && 43 | projects.map((project) => ( 44 | 45 | ))} 46 |
47 |
48 | 49 |
50 | ); 51 | }; 52 | 53 | export default Home; 54 | -------------------------------------------------------------------------------- /src/pages/Login.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useLogin } from "../hooks/useLogin"; 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 | 17 | return ( 18 |
22 |

Login

23 | 24 |
25 | 31 | setEmail(e.target.value)} 37 | className="bg-transparent border border-slate-500 py-3 px-5 rounded-xl outline-none focus:border-sky-400 duration-300" 38 | /> 39 |
40 | 41 |
42 | 48 | 49 | setPassword(e.target.value)} 55 | className="bg-transparent border border-slate-500 py-3 px-5 rounded-xl outline-none focus:border-sky-400 duration-300" 56 | /> 57 |
58 | 59 | 66 | 67 | {error && ( 68 |

69 | {error} 70 |

71 | )} 72 |
73 | ); 74 | }; 75 | 76 | export default Login; 77 | -------------------------------------------------------------------------------- /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 | 17 | return ( 18 |
22 |

Sign Up

23 | 24 |
25 | 31 | setEmail(e.target.value)} 37 | className="bg-transparent border border-slate-500 py-3 px-5 rounded-xl outline-none focus:border-sky-400 duration-300" 38 | /> 39 |
40 | 41 |
42 | 48 | 49 | setPassword(e.target.value)} 55 | className="bg-transparent border border-slate-500 py-3 px-5 rounded-xl outline-none focus:border-sky-400 duration-300" 56 | /> 57 |
58 | 59 | 66 | 67 | {error && ( 68 |

69 | {error} 70 |

71 | )} 72 |
73 | ); 74 | }; 75 | 76 | export default Signup; 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proxima - A Project Management Website 2 | 3 | Proxima is a modern project management website built using the MERN stack, Context API, JWT, and TailwindCSS. With a sleek and intuitive interface, users can easily create, update, and delete their projects. Proxima also features a secure protected route in the frontend, ensuring user data remains safe and secure. 4 | 5 | ## Features 6 | 7 | Proxima comes packed with a variety of useful features, including: 8 | 9 | - Simplify project management tasks: Perform tasks such as creating, updating, and deleting projects with ease using the app's user-friendly interface. 10 | - High-level security: Enjoy peace of mind with the app's highly secure JWT authentication and frontend route protection, which safeguards your data. 11 | - Personalized project views: Keep your projects confidential by allowing users to view only the projects they have created. 12 | - User-friendly interface: Enjoy a smooth and intuitive experience with the app's streamlined UI, which makes project management simple and straightforward. 13 | 14 | ## Tools Used 15 | 16 | - **Node.js** - JavaScript runtime environment that runs on the server. 17 | - **Express** - Fast, minimalist web framework for Node.js. 18 | - **MongoDB** - NoSQL database that uses a document-oriented data model. 19 | - **JWT** - JSON Web Tokens for secure authentication and authorization. 20 | - **React** - JavaScript library for building user interfaces. 21 | - **Context API** - React's Context API for state management. 22 | - **Tailwind CSS** - CSS framework that allows for easy customization and rapid development. 23 | 24 | ## Installation 25 | 26 | 1. Clone the client repository using git clone https://github.com/KeyaAkter/proxima-client 27 | 2. Clone the server repository using git clone https://github.com/KeyaAkter/proxima-server 28 | 3. Install the required dependencies by running `npm install `or `npm i` in both the client and server directories. 29 | 4. Create a `.env` file in the root directory of server and add the following variables: 30 | - `MONGO_URI`: the MongoDB connection string 31 | - `SECRET`: a secret string for JWT authentication 32 | 5. Create a `.env` file in the root directory of client and add the following variable: 33 | 34 | - `REACT_APP_BASE_URL`: for example http://localhost:4000 35 | 36 | 6. Start the backend server by running `npm start`. 37 | 7. Start the backend server by running `npm start`. 38 | 39 | ## Links 40 | 41 | - [Live Demo](https://proxima-project.netlify.app/) 42 | - [Front-End Repository](https://github.com/KeyaAkter/proxima-client) 43 | - [Back-End Repository](https://github.com/KeyaAkter/proxima-server) 44 | -------------------------------------------------------------------------------- /src/components/ProjectDetails.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import moment from "moment"; 3 | import { useProjectsContext } from "../hooks/useProjectsContext"; 4 | import { useAuthContext } from "../hooks/useAuthContext"; 5 | import { currencyFormatter } from "../utilities/currencyFormatter"; 6 | import ProjectForm from "./ProjectForm"; 7 | 8 | const ProjectDetails = ({ project }) => { 9 | const [isModalOpen, setIsModalOpen] = useState(false); 10 | const [isOverlayOpen, setIsOverlayOpen] = useState(false); 11 | 12 | const { dispatch } = useProjectsContext(); 13 | const { user } = useAuthContext(); 14 | 15 | const handleDelete = async () => { 16 | if (!user) { 17 | return; 18 | } 19 | 20 | const res = await fetch( 21 | `${process.env.REACT_APP_BASE_URL}/api/projects/${project._id}`, 22 | { 23 | method: "DELETE", 24 | headers: { 25 | Authorization: `Bearer ${user.token}`, 26 | }, 27 | } 28 | ); 29 | 30 | const json = await res.json(); 31 | 32 | if (res.ok) { 33 | dispatch({ type: "DELETE_PROJECT", payload: json }); 34 | } 35 | }; 36 | 37 | const handleUpdate = () => { 38 | setIsModalOpen(true); 39 | setIsOverlayOpen(true); 40 | }; 41 | 42 | const handleOverlay = () => { 43 | setIsModalOpen(false); 44 | setIsOverlayOpen(false); 45 | }; 46 | 47 | return ( 48 |
49 |
50 | {project._id} 51 |

{project.title}

52 | 53 | {project.tech} 54 | 55 |
56 | 57 |
58 |
59 | Budget : {currencyFormatter(project.budget)} 60 | 61 | Added : {moment(project.createdAt).format("MMM DD, hh:mm A")} 62 | 63 | 64 | Updated : {moment(project.updatedAt).format("MMM DD, hh:mm A")} 65 | 66 |
67 |
68 | Manager : {project.manager} 69 | Developer : {project.dev} 70 | 71 | Duration :{" "} 72 | {`${project.duration} week${project.duration === 1 ? "" : "s"}`} 73 | 74 |
75 |
76 | 77 |
78 | 84 | 90 |
91 | 92 | {/* OVERLAY */} 93 |
99 | 100 | {/* MODAL */} 101 |
106 |

107 | Update project 108 |

109 | 110 | 115 |
116 |
117 | ); 118 | }; 119 | 120 | export default React.memo(ProjectDetails); 121 | -------------------------------------------------------------------------------- /src/components/ProjectForm.js: -------------------------------------------------------------------------------- 1 | import React, { 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 | 13 | const [error, setError] = useState(null); 14 | const [emptyFields, setEmptyFields] = useState([]); 15 | 16 | const { dispatch } = useProjectsContext(); 17 | const { user } = useAuthContext(); 18 | 19 | const handleSubmit = async (e) => { 20 | e.preventDefault(); 21 | 22 | if (!user) { 23 | setError("You must be logged in!"); 24 | return; 25 | } 26 | 27 | // data 28 | const projectObj = { title, tech, budget, duration, manager, dev }; 29 | 30 | // if there is no project, send post req 31 | if (!project) { 32 | // sending post request 33 | const res = await fetch( 34 | `${process.env.REACT_APP_BASE_URL}/api/projects`, 35 | { 36 | method: "POST", 37 | headers: { 38 | "Content-Type": "application/json", 39 | Authorization: `Bearer ${user.token}`, 40 | }, 41 | body: JSON.stringify(projectObj), 42 | } 43 | ); 44 | 45 | const json = await res.json(); 46 | 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 | // project post successfully 63 | dispatch({ type: "CREATE_PROJECT", payload: json }); 64 | } 65 | return; 66 | } 67 | 68 | // if there is a project, send patch req 69 | if (project) { 70 | // sending patch req 71 | const res = await fetch( 72 | `${process.env.REACT_APP_BASE_URL}/api/projects/${project._id}`, 73 | { 74 | method: "PATCH", 75 | headers: { 76 | "Content-Type": "application/json", 77 | Authorization: `Bearer ${user.token}`, 78 | }, 79 | body: JSON.stringify(projectObj), 80 | } 81 | ); 82 | 83 | const json = await res.json(); 84 | 85 | if (!res.ok) { 86 | setError(json.error); 87 | setEmptyFields(json.emptyFields); 88 | } 89 | 90 | if (res.ok) { 91 | setError(null); 92 | setEmptyFields([]); 93 | 94 | // dispatch 95 | dispatch({ type: "UPDATE_PROJECT", payload: json }); 96 | 97 | // closing overlay & modal 98 | setIsModalOpen(false); 99 | setIsOverlayOpen(false); 100 | } 101 | return; 102 | } 103 | }; 104 | 105 | return ( 106 |
107 |

112 | Add a new project 113 |

114 | 115 |
116 | 122 | setTitle(e.target.value)} 125 | type="text" 126 | placeholder="e.g. e-commerce website" 127 | id="title" 128 | className={`bg-transparent border py-3 px-5 rounded-lg outline-none focus:border-sky-400 duration-300 ${ 129 | emptyFields?.includes("title") 130 | ? "border-rose-500" 131 | : "border-slate-500" 132 | }`} 133 | /> 134 |
135 | 136 |
137 | 143 | setTech(e.target.value)} 146 | type="text" 147 | placeholder="e.g. react.js, redux, node.js" 148 | id="tech" 149 | className={`bg-transparent border py-3 px-5 rounded-lg outline-none focus:border-sky-400 duration-300 ${ 150 | emptyFields?.includes("tech") 151 | ? "border-rose-500" 152 | : "border-slate-500" 153 | }`} 154 | /> 155 |
156 | 157 |
158 | 164 | setBudget(e.target.value)} 167 | type="number" 168 | placeholder="e.g. 500" 169 | id="budget" 170 | className={`bg-transparent border py-3 px-5 rounded-lg outline-none focus:border-sky-400 duration-300 ${ 171 | emptyFields?.includes("budget") 172 | ? "border-rose-500" 173 | : "border-slate-500" 174 | }`} 175 | /> 176 |
177 | 178 |
179 | 185 | setDuration(e.target.value)} 188 | type="number" 189 | placeholder="e.g. 6" 190 | id="duration" 191 | className={`bg-transparent border py-3 px-5 rounded-lg outline-none focus:border-sky-400 duration-300 ${ 192 | emptyFields?.includes("duration") 193 | ? "border-rose-500" 194 | : "border-slate-500" 195 | }`} 196 | /> 197 |
198 | 199 |
200 | 206 | setManager(e.target.value)} 209 | type="text" 210 | placeholder="e.g. Tanisha Tanvin" 211 | id="manager" 212 | className={`bg-transparent border py-3 px-5 rounded-lg outline-none focus:border-sky-400 duration-300 ${ 213 | emptyFields?.includes("manager") 214 | ? "border-rose-500" 215 | : "border-slate-500" 216 | }`} 217 | /> 218 |
219 | 220 |
221 | 227 | setDev(e.target.value)} 230 | type="number" 231 | placeholder="e.g. 10" 232 | id="developer" 233 | className={`bg-transparent border py-3 px-5 rounded-lg outline-none focus:border-sky-400 duration-300 ${ 234 | emptyFields?.includes("dev") 235 | ? "border-rose-500" 236 | : "border-slate-500" 237 | }`} 238 | /> 239 |
240 | 241 | 247 | {error && ( 248 |

249 | {error} 250 |

251 | )} 252 |
253 | ); 254 | }; 255 | 256 | export default React.memo(ProjectForm); 257 | --------------------------------------------------------------------------------