├── 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 |
28 |
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 |
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 |
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 |
253 | );
254 | };
255 |
256 | export default React.memo(ProjectForm);
257 |
--------------------------------------------------------------------------------