├── public ├── _redirects └── index.html ├── .env ├── src ├── utils │ └── currencyFormatter.js ├── index.css ├── components │ ├── Footer.js │ ├── ServerStatusCheck .js │ ├── Navbar.js │ ├── ProjectDetails.js │ └── ProjectForm.js ├── hooks │ ├── useAuthContext.js │ ├── useProjectsContext.js │ ├── useLogout.js │ ├── useLogin.js │ ├── useSignup.js │ └── useServerStatus.js ├── index.js ├── context │ ├── AuthContext.js │ └── ProjectContext.js ├── App.js └── pages │ ├── Home.js │ ├── Signup.js │ └── Login.js ├── tailwind.config.js ├── .gitignore ├── package.json └── README.md /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_URL=https://proxima-owyi.onrender.com -------------------------------------------------------------------------------- /src/utils/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 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | const Footer = () => { 2 | return ( 3 | 6 | ); 7 | }; 8 | 9 | export default Footer; 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Proxima 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 | return context; 13 | }; 14 | -------------------------------------------------------------------------------- /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 | return context; 13 | }; 14 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/hooks/useLogout.js: -------------------------------------------------------------------------------- 1 | import { useAuthContext } from "./useAuthContext"; 2 | import { useProjectsContext } from "../hooks/useProjectsContext"; 3 | 4 | export const useLogout = () => { 5 | const { dispatch: logoutDispatch } = useAuthContext(); 6 | const { dispatch: projectsDispatch } = useProjectsContext(); 7 | 8 | const logout = () => { 9 | // Clear local storage 10 | localStorage.removeItem("user"); 11 | 12 | //Dispacth 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 { AuthContextProvider } from "./context/AuthContext"; 6 | import { ProjectContextProvider } from "./context/ProjectContext"; 7 | import "./index.css"; 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 Home from "./pages/Home"; 3 | import Footer from "./components/Footer"; 4 | import Navbar from "./components/Navbar"; 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/components/ServerStatusCheck .js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useServerStatus } from "../hooks/useServerStatus"; 3 | 4 | const ServerStatusCheck = ({ children }) => { 5 | const { status, error, isChecking, initialCheckDone, checkServerStatus } = 6 | useServerStatus(); 7 | 8 | if ( 9 | (status === "loading" || status === "idle") && 10 | initialCheckDone === false 11 | ) { 12 | return ( 13 |
14 |

Please wait, the server is waking up...

15 |

This may take up to 30 seconds for the first request.

16 | {isChecking &&

Checking server status...

} 17 |
18 | ); 19 | } 20 | 21 | if (status === "failed") { 22 | return ( 23 |
24 |

Error: {error}

25 | 28 |
29 | ); 30 | } 31 | 32 | return <>{children}; 33 | }; 34 | 35 | export default ServerStatusCheck; 36 | -------------------------------------------------------------------------------- /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 | // res.ok === false 28 | if (!res.ok) { 29 | setLoading(false); 30 | setError(json.error); 31 | } 32 | 33 | if (res.ok) { 34 | //Update auth context 35 | dispatch({ type: "LOGIN", payload: json }); 36 | 37 | //Save user in to the 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 (fullName, 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({ fullName, email, password }), 22 | } 23 | ); 24 | 25 | const json = await res.json(); 26 | 27 | //res.ok ===false 28 | if (!res.ok) { 29 | setLoading(false); 30 | setError(json.error); 31 | } 32 | 33 | if (res.ok) { 34 | //Update auth context 35 | dispatch({ type: "LOGIN", payload: json }); 36 | 37 | //Save user in 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/hooks/useServerStatus.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | export const useServerStatus = () => { 4 | const [status, setStatus] = useState("idle"); 5 | const [error, setError] = useState(null); 6 | const [isChecking, setIsChecking] = useState(false); 7 | const [initialCheckDone, setInitialCheckDone] = useState(false); 8 | 9 | const checkServerStatus = async () => { 10 | if (isChecking) return; 11 | 12 | setIsChecking(true); 13 | setError(null); 14 | 15 | try { 16 | const response = await fetch( 17 | `${process.env.REACT_APP_BASE_URL}/api/health` 18 | ); 19 | if (response.ok) { 20 | setStatus("succeeded"); 21 | setInitialCheckDone(true); 22 | } else { 23 | throw new Error("Server is not responding"); 24 | } 25 | } catch (error) { 26 | setStatus("failed"); 27 | setError(error.message); 28 | } finally { 29 | setIsChecking(false); 30 | setInitialCheckDone(true); 31 | } 32 | }; 33 | 34 | useEffect(() => { 35 | checkServerStatus(); 36 | const intervalId = setInterval(checkServerStatus, 5000); // Check every 5 seconds 37 | 38 | return () => clearInterval(intervalId); 39 | }, []); 40 | 41 | return { status, error, isChecking, initialCheckDone, checkServerStatus }; 42 | }; 43 | -------------------------------------------------------------------------------- /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 | case "CREATE_PROJECT": 15 | return { 16 | ...state, 17 | projects: [action.payload, ...state.projects], 18 | }; 19 | case "DELETE_PROJECT": 20 | return { 21 | ...state, 22 | projects: state.projects.filter( 23 | (project) => project._id !== action.payload._id 24 | ), 25 | }; 26 | case "UPDATE_PROJECT": 27 | const [existingProject] = state.projects.filter( 28 | (project) => project._id === action.payload._id 29 | ); 30 | return { 31 | ...state, 32 | projects: [ 33 | action.payload, 34 | ...state.projects.filter( 35 | (project) => project._id !== existingProject._id 36 | ), 37 | ], 38 | }; 39 | default: 40 | return state; 41 | } 42 | }; 43 | 44 | export const ProjectContext = createContext(); 45 | 46 | export const ProjectContextProvider = ({ children }) => { 47 | const [state, dispatch] = useReducer(projectsReducer, initialState); 48 | 49 | return ( 50 | 51 | {children} 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import ProjectDetails from "../components/ProjectDetails"; 3 | import ProjectForm from "../components/ProjectForm"; 4 | import { useAuthContext } from "../hooks/useAuthContext"; 5 | import { useProjectsContext } from "../hooks/useProjectsContext"; 6 | 7 | const Home = () => { 8 | const { projects, dispatch } = useProjectsContext(); 9 | const { user } = useAuthContext(); 10 | 11 | useEffect(() => { 12 | const getAllProjects = async () => { 13 | const res = await fetch( 14 | `${process.env.REACT_APP_BASE_URL}/api/projects`, 15 | { 16 | headers: { 17 | Authorization: `Bearer ${user.token}`, 18 | }, 19 | } 20 | ); 21 | const json = await res.json(); 22 | 23 | if (res.ok) { 24 | dispatch({ type: "SET_PROJECTS", payload: json }); 25 | } 26 | }; 27 | 28 | if (user) { 29 | getAllProjects(); 30 | } 31 | }, [dispatch, user]); 32 | 33 | return ( 34 |
35 |
36 |

37 | {projects.length < 1 ? ( 38 | "No project available here!" 39 | ) : ( 40 | 41 | {projects.length === 1 ? "Your Project" : "Your all projects"} 42 | 43 | )} 44 |

45 |
46 | {projects && 47 | projects.map((project) => ( 48 | 49 | ))} 50 |
51 |
52 | 53 |
54 | ); 55 | }; 56 | 57 | export default Home; 58 | -------------------------------------------------------------------------------- /src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { useAuthContext } from "../hooks/useAuthContext"; 4 | import { useLogout } from "../hooks/useLogout"; 5 | 6 | const Navbar = () => { 7 | const [hover, setHover] = useState(false); 8 | const { user } = useAuthContext(); 9 | const { logout } = useLogout(); 10 | 11 | const handleLogout = () => { 12 | logout(); 13 | }; 14 | 15 | return ( 16 |
17 | 18 | Proxima 19 | 20 | 21 | 70 |
71 | ); 72 | }; 73 | 74 | export default Navbar; 75 | -------------------------------------------------------------------------------- /src/pages/Signup.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useSignup } from "../hooks/useSignup"; 3 | 4 | const Signup = () => { 5 | const [fullName, setFullName] = useState(""); 6 | const [email, setEmail] = useState(""); 7 | const [password, setPassword] = useState(""); 8 | 9 | const { signup, error, loading } = useSignup(); 10 | 11 | const handleSignup = async (e) => { 12 | e.preventDefault(); 13 | 14 | //Signup user 15 | await signup(fullName, email, password); 16 | }; 17 | 18 | return ( 19 |
23 |

Sign up

24 | 25 |
26 | 32 | setFullName(e.target.value)} 35 | type='text' 36 | id='full-name' 37 | placeholder='e.g Jhon Doe' 38 | className='py-3 px-5 rounded-md bg-transparent border border-slate-500 outline-none focus:border-sky-400 duration-300' 39 | /> 40 |
41 |
42 | 48 | setEmail(e.target.value)} 51 | type='email' 52 | id='email' 53 | placeholder='e.g jhon@gmail.com' 54 | className='py-3 px-5 rounded-md bg-transparent border border-slate-500 outline-none focus:border-sky-400 duration-300' 55 | /> 56 |
57 |
58 | 64 | setPassword(e.target.value)} 67 | type='password' 68 | id='password' 69 | placeholder='Write a strong password' 70 | className='py-3 px-5 rounded-md bg-transparent border border-slate-500 outline-none focus:border-sky-400 duration-300' 71 | /> 72 |
73 | 80 | 81 | {error && ( 82 |

83 | {error} 84 |

85 | )} 86 |
87 | ); 88 | }; 89 | 90 | export default Signup; 91 | -------------------------------------------------------------------------------- /src/components/ProjectDetails.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useProjectsContext } from "../hooks/useProjectsContext"; 3 | import { useAuthContext } from "../hooks/useAuthContext"; 4 | import { currencyFormatter } from "../utils/currencyFormatter"; 5 | import moment from "moment"; 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 | Budget: {currencyFormatter(project.budget)} 59 | 60 | Added: {moment(project.createdAt).format("MMM DD, hh:mm A")} 61 | 62 | 63 | Updated: {moment(project.updatedAt).format("MMM DD, hh:mm A")} 64 | 65 |
66 |
67 | Manager: {project.manager} 68 | Developers: {project.dev} 69 | 70 | Duration:{" "} 71 | {`${project.duration} Week${project.duration === 1 ? "" : "s"}`} 72 | 73 |
74 |
75 |
76 | 82 | 88 |
89 | 90 | {/* OVERLAY */} 91 |
97 | 98 | {/* MODAL */} 99 |
104 |

Update project

105 | 110 |
111 |
112 | ); 113 | }; 114 | 115 | export default ProjectDetails; 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proxima: Project Management Web Application 2 | 3 | Proxima is a project management application built using the MERN stack. The application allows users to create, update, and delete their own projects with ease. Proxima is designed with high-level security in mind, featuring strong JWT authentication and front-end route protection to ensure the safety and privacy of user data. 4 | 5 | ## Features 6 | 7 | Proxima is a feature-rich project management application, offering users a wide range of useful tools and functions, including: 8 | 9 | - **Effortlessly manage your projects:** With Proxima's intuitive interface, you can create, update, and delete projects quickly and easily. All project-related information is centralized, allowing you to stay organized and focused. 10 | - **State-of-the-Art Security Measures:** Proxima prioritizes the safety and security of your data with its highly robust security features. The app uses JWT authentication and frontend route protection to provide a secure platform for your project management needs, giving you peace of mind that your data is always protected. 11 | - **Personalized Project Views:** User-specific project views, so each user can only see the projects they have created. This ensures that project information is kept private and secure, while also giving users the flexibility to manage their projects according to their unique needs. 12 | - **Project Timeline Management:** Users with a powerful timeline management tool, allowing them to track the creation date of a project, as well as its last update date. With this feature, users can easily monitor the progress of their projects and ensure that they are staying on schedule. This helps teams to optimize their workflows, allocate resources efficiently, and meet project deadlines. 13 | - **Intuitive and Streamlined UI:** User interface is designed to be both intuitive and streamlined, making project management a breeze. The clean and modern design of the interface allows users to easily navigate the app's many features, while also providing a visually pleasing and enjoyable user experience. 14 | 15 | ## Tools 16 | 17 | Proxima is built using the MERN stack, featuring the following powerful tools: 18 | 19 | - **MongoDB:** A highly flexible NoSQL database, ideal for managing large and complex data sets. 20 | - **Express:** A popular and highly flexible backend web application framework for Node.js. 21 | - **React:** A powerful and popular frontend JavaScript library, ideal for building user interfaces. 22 | - **Node.js:** A powerful and popular server-side JavaScript runtime environment. 23 | - **Tailwind CSS:** A highly customizable CSS framework, designed to make building sleek and intuitive user interfaces a breeze. 24 | 25 | ## Installation 26 | 27 | To install Proxima, Follow the simple steps below: 28 | 29 | > **Note** 30 | > **You must have Node.js and MongoDB installed on your system!** 31 | 1. Clone the `client` repository using 32 | 33 | ``` 34 | git clone https://github.com/masudranashawon/proxima-client.git` 35 | ``` 36 | 37 | 2. Clone the `server` repository using 38 | 39 | ``` 40 | git clone https://github.com/masudranashawon/proxima-server.git 41 | ``` 42 | 43 | 3. Install the required dependencies in both the `client` and `server` directories by running 44 | 45 | ``` 46 | npm install 47 | ``` 48 | 49 | **Or** 50 | 51 | ``` 52 | npm i 53 | ``` 54 | 55 | 4. Create a `.env` file in the root directory of `server` and add the following variables: 56 | 57 | - `MONGO_URI`: the MongoDB connection string 58 | - `SECRET`: a secret string for JWT authentication 59 | 60 | 5. Create a `.env` file in the root directory of `client` and add the following variable: 61 | - `REACT_APP_BASE_URL`: for example `http://localhost:5000` 62 | 6. Start the backend `server` by running 63 | 64 | ``` 65 | npm start 66 | ``` 67 | 68 | 7. Start the `frontend` by running 69 | 70 | ``` 71 | npm start 72 | ``` 73 | 74 | ## Links 75 | 76 | - [Live Demo](https://proxima-application.netlify.app) 77 | - [Front-End Repository](https://github.com/masudranashawon/proxima-client) 78 | - [Back-End Repository](https://github.com/masudranashawon/proxima-server) 79 | 80 | ## Thanks for visiting this repository! 81 | -------------------------------------------------------------------------------- /src/pages/Login.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useLogin } from "../hooks/useLogin"; 3 | // import { useServerStatus } from "../hooks/useServerStatus"; 4 | 5 | const Login = () => { 6 | const [email, setEmail] = useState(""); 7 | const [password, setPassword] = useState(""); 8 | const [serverReady, setServerReady] = useState(true); 9 | const [serverError, setServerError] = useState(false); 10 | const [isLoading, setIsLoading] = useState(false); 11 | 12 | const { login, error, loading } = useLogin(); 13 | // const { 14 | // status, 15 | // error: serverError, 16 | // isChecking, 17 | // initialCheckDone, 18 | // checkServerStatus, 19 | // } = useServerStatus(); 20 | 21 | const checkServerStatus = async () => { 22 | try { 23 | setIsLoading(true); 24 | 25 | const response = await fetch( 26 | `${process.env.REACT_APP_BASE_URL}/api/health` 27 | ); 28 | 29 | if (response.ok) { 30 | setIsLoading(false); 31 | setServerReady(true); 32 | } else { 33 | setIsLoading(false); 34 | setServerReady(false); 35 | throw new Error("Server is not responding"); 36 | } 37 | } catch (error) { 38 | setIsLoading(false); 39 | setServerError(true); 40 | console.error("Server health check failed:", error); 41 | } 42 | }; 43 | 44 | const handleLogin = async (e) => { 45 | e.preventDefault(); 46 | checkServerStatus(); 47 | 48 | if (isLoading !== true && serverError !== true && serverReady === true) { 49 | await login(email, password); 50 | } 51 | }; 52 | 53 | return ( 54 |
58 |

Log in

59 |
60 | 66 | setEmail(e.target.value)} 69 | type="email" 70 | id="email" 71 | placeholder="e.g jhon@gmail.com" 72 | className="py-3 px-5 rounded-md bg-transparent border border-slate-500 outline-none focus:border-sky-400 duration-300" 73 | /> 74 |
75 |
76 | 82 | setPassword(e.target.value)} 85 | type="password" 86 | id="password" 87 | placeholder="Enter your password" 88 | className="py-3 px-5 rounded-md bg-transparent border border-slate-500 outline-none focus:border-sky-400 duration-300" 89 | /> 90 |
91 | 98 | 99 | {error && ( 100 |

101 | {error} 102 |

103 | )} 104 | 105 | {serverError && ( 106 |

107 | Something went wrong ! 108 |

109 | )} 110 | 111 | {isLoading === true || 112 | (serverReady === false && ( 113 |
114 |

Please wait, the server is waking up...

115 |

This may take up to 30 seconds for the first request.

116 | {isLoading &&

Checking server status...

} 117 |
118 | ))} 119 |
120 | ); 121 | }; 122 | 123 | export default Login; 124 | -------------------------------------------------------------------------------- /src/components/ProjectForm.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useAuthContext } from "../hooks/useAuthContext"; 3 | import { useProjectsContext } from "../hooks/useProjectsContext"; 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("Please log in to access this feature."); 23 | return; 24 | } 25 | 26 | //Data 27 | const projectObj = { 28 | title, 29 | tech, 30 | budget, 31 | duration, 32 | manager, 33 | dev, 34 | }; 35 | 36 | //Is there is no project, sent post req 37 | if (!project) { 38 | //Post req 39 | const res = await fetch( 40 | `${process.env.REACT_APP_BASE_URL}/api/projects`, 41 | { 42 | method: "POST", 43 | headers: { 44 | "Content-Type": "application/json", 45 | Authorization: `Bearer ${user.token}`, 46 | }, 47 | body: JSON.stringify(projectObj), 48 | } 49 | ); 50 | const json = await res.json(); 51 | 52 | //res.ok is false set error 53 | if (!res.ok) { 54 | setError(json.error); 55 | setEmptyFields(json.emptyFields); 56 | } 57 | 58 | //res.ok is true, reset 59 | if (res.ok) { 60 | setTitle(""); 61 | setTech(""); 62 | setBudget(""); 63 | setDuration(""); 64 | setManager(""); 65 | setDev(""); 66 | setError(null); 67 | setEmptyFields([]); 68 | 69 | //Dispath 70 | dispatch({ type: "CREATE_PROJECT", payload: json }); 71 | } 72 | return; 73 | } 74 | 75 | //Is there is a project, sent patch req 76 | if (project) { 77 | //Sent patch req 78 | const res = await fetch( 79 | `${process.env.REACT_APP_BASE_URL}/api/projects/${project._id}`, 80 | { 81 | method: "PATCH", 82 | headers: { 83 | "Content-Type": "application/json", 84 | Authorization: `Bearer ${user.token}`, 85 | }, 86 | body: JSON.stringify(projectObj), 87 | } 88 | ); 89 | 90 | const json = await res.json(); 91 | 92 | //!res.ok 93 | if (!res.ok) { 94 | setError(json.error); 95 | setEmptyFields(json.emptyFields); 96 | } 97 | 98 | //res.ok 99 | if (res.ok) { 100 | setError(null); 101 | setEmptyFields([]); 102 | 103 | //Dispatch 104 | dispatch({ type: "UPDATE_PROJECT", payload: json }); 105 | 106 | //Close overlay & modal 107 | setIsModalOpen(false); 108 | setIsOverlayOpen(false); 109 | } 110 | return; 111 | } 112 | }; 113 | 114 | return ( 115 |
116 |

117 | Add a new Project 118 |

119 | 120 |
121 | 127 | setTitle(e.target.value)} 130 | type='text' 131 | id={`${project ? "update-" : ""}title`} 132 | placeholder='e.g e-commerce website' 133 | className={`bg-transparent py-3 px-5 outline-none focus:border-sky-400 duration-300 ${ 134 | emptyFields?.includes("title") 135 | ? "border border-rose-500" 136 | : "border border-slate-500" 137 | }`} 138 | /> 139 |
140 | 141 |
142 | 148 | setTech(e.target.value)} 151 | type='text' 152 | id={`${project ? "update-" : ""}tech`} 153 | placeholder='e.g node.js, react.js, redux etc.' 154 | className={`bg-transparent py-3 px-5 outline-none focus:border-sky-400 duration-300 ${ 155 | emptyFields?.includes("tech") 156 | ? "border border-rose-500" 157 | : "border border-slate-500" 158 | }`} 159 | /> 160 |
161 | 162 |
163 | 169 | setBudget(e.target.value)} 172 | type='number' 173 | id={`${project ? "update-" : ""}budget`} 174 | placeholder='e.g 1500' 175 | className={`bg-transparent py-3 px-5 outline-none focus:border-sky-400 duration-300 ${ 176 | emptyFields?.includes("budget") 177 | ? "border border-rose-500" 178 | : "border border-slate-500" 179 | }`} 180 | /> 181 |
182 | 183 |
184 | 190 | setDuration(e.target.value)} 193 | type='number' 194 | id={`${project ? "update-" : ""}duration`} 195 | placeholder='e.g 2' 196 | className={`bg-transparent py-3 px-5 outline-none focus:border-sky-400 duration-300 ${ 197 | emptyFields?.includes("duration") 198 | ? "border border-rose-500" 199 | : "border border-slate-500" 200 | }`} 201 | /> 202 |
203 | 204 |
205 | 211 | setManager(e.target.value)} 214 | type='text' 215 | id={`${project ? "update-" : ""}manager`} 216 | placeholder='e.g john doe' 217 | className={`bg-transparent py-3 px-5 outline-none focus:border-sky-400 duration-300 ${ 218 | emptyFields?.includes("manager") 219 | ? "border border-rose-500" 220 | : "border border-slate-500" 221 | }`} 222 | /> 223 |
224 | 225 |
226 | 232 | setDev(e.target.value)} 235 | type='number' 236 | id={`${project ? "update-" : ""}dev`} 237 | placeholder='e.g 5' 238 | className={`bg-transparent py-3 px-5 outline-none focus:border-sky-400 duration-300 ${ 239 | emptyFields?.includes("dev") 240 | ? "border border-rose-500" 241 | : "border border-slate-500" 242 | }`} 243 | /> 244 | 250 | {error && ( 251 |

252 | {error} 253 |

254 | )} 255 |
256 |
257 | ); 258 | }; 259 | 260 | export default ProjectForm; 261 | --------------------------------------------------------------------------------