├── .env
├── public
├── app.png
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── src
├── static
│ ├── app.png
│ ├── app2.png
│ └── 24.svg
├── setupTests.js
├── App.test.js
├── reportWebVitals.js
├── support
│ ├── Date.js
│ └── formFields.js
├── index.css
├── components
│ ├── Submit.jsx
│ ├── Header.jsx
│ ├── Input.jsx
│ ├── FormOptions.jsx
│ ├── Nav.jsx
│ ├── Task.jsx
│ ├── Search.jsx
│ ├── Loading.jsx
│ ├── Alert.jsx
│ ├── Navbar.jsx
│ ├── Lane.jsx
│ ├── ProjectHeader.jsx
│ ├── Board.jsx
│ ├── Options.jsx
│ ├── Footer.jsx
│ ├── TaskForm.jsx
│ ├── ProjectForm.jsx
│ └── SideBar.jsx
├── index.js
├── pages
│ ├── NotFound.jsx
│ ├── Home.jsx
│ ├── SignIn.jsx
│ ├── Project.jsx
│ ├── Projects.jsx
│ └── SignUp.jsx
├── hooks
│ └── useDataFetching.js
├── App.js
├── logo.svg
├── App.css
└── context
│ └── AuthContext.js
├── postcss.config.js
├── .gitignore
├── tailwind.config.js
├── LICENSE.md
├── package.json
└── README.md
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_BACKEND_URL = "api"
--------------------------------------------------------------------------------
/public/app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudFettal/simple-project/HEAD/public/app.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudFettal/simple-project/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudFettal/simple-project/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudFettal/simple-project/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/static/app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudFettal/simple-project/HEAD/src/static/app.png
--------------------------------------------------------------------------------
/src/static/app2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudFettal/simple-project/HEAD/src/static/app2.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render( );
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/.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 |
25 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: "class",
4 | content: ["./src/**/*.{js,jsx,ts,tsx}"],
5 | theme: {
6 | minWidth: {
7 | 500: "325px",
8 | },
9 | extend: {
10 | colors: {
11 | notFound: "#F2949C",
12 | },
13 | maxWidth: {
14 | "8xl": "96rem",
15 | },
16 | },
17 | },
18 | plugins: [require("@tailwindcss/line-clamp")],
19 | };
20 |
--------------------------------------------------------------------------------
/src/support/Date.js:
--------------------------------------------------------------------------------
1 | const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2 | const MONTHS = [
3 | "Jan",
4 | "Feb",
5 | "Mar",
6 | "Apr",
7 | "May",
8 | "Jun",
9 | "Jul",
10 | "Aug",
11 | "Sep",
12 | "Oct",
13 | "Nov",
14 | "Dec",
15 | ];
16 |
17 | const date = (day) => {
18 | return `${DAYS[day.getDay()]} ${day.getDate()} ${
19 | MONTHS[day.getMonth()]
20 | } ${day.getFullYear()} - ${day.getHours()}:${day.getMinutes()}`;
21 | };
22 |
23 | export default date;
24 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | margin: 0;
7 | overflow-x: hidden;
8 | width: 100vw;
9 | font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
10 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
11 | sans-serif;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | code {
17 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
18 | monospace;
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Submit.jsx:
--------------------------------------------------------------------------------
1 | function Submit({ type = "Button", action = "submit", text }) {
2 | return (
3 | <>
4 | {type === "Button" ? (
5 |
10 | ) : (
11 | <>>
12 | )}
13 | >
14 | );
15 | }
16 |
17 | export default Submit;
18 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import './index.css';
5 | import App from './App';
6 | import reportWebVitals from './reportWebVitals';
7 |
8 | const root = ReactDOM.createRoot(document.getElementById('root'));
9 | root.render(
10 |
11 |
12 |
13 | );
14 |
15 | // If you want to start measuring performance in your app, pass a function
16 | // to log results (for example: reportWebVitals(console.log))
17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
18 | reportWebVitals();
19 |
--------------------------------------------------------------------------------
/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | function Header({ heading, paragraph, linkName, linkUrl = "#" }) {
5 | return (
6 |
7 |
8 |
9 | SimpleProject {" "}
10 |
11 |
12 |
13 | {heading}
14 |
15 |
16 | {paragraph}{" "}
17 |
18 | {linkName}
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default Header;
26 |
--------------------------------------------------------------------------------
/src/pages/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import notFound from "../static/24.svg";
3 | import { Link } from "react-router-dom";
4 |
5 | function NotFound() {
6 | return (
7 |
8 |
9 |
10 | Page not found!
11 |
12 |
13 |
14 | Return to home
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | export default NotFound;
22 |
--------------------------------------------------------------------------------
/src/components/Input.jsx:
--------------------------------------------------------------------------------
1 | const fixedInputClass =
2 | "dark:text-white appearance-none relative block w-full py-2.5 bg-transparent placeholder-gray-500 text-gray-900 focus:outline-none border-b-2 border-b-gray-900 dark:border-b-white focus:z-10 text-lg font-semibold sm:text-sm";
3 |
4 | export default function Input({
5 | handleChange,
6 | value,
7 | labelText,
8 | labelFor,
9 | id,
10 | name,
11 | type,
12 | isRequired = false,
13 | placeholder,
14 | customClass,
15 | }) {
16 | return (
17 |
18 |
19 | {labelText}
20 |
21 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/FormOptions.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function FormOptions() {
4 | return (
5 |
6 | {" "}
7 |
8 |
9 |
15 |
19 | Remember me
20 |
21 |
22 |
23 |
24 |
25 | Forgot your password?
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | export default FormOptions;
34 |
--------------------------------------------------------------------------------
/src/hooks/useDataFetching.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useContext } from "react";
2 | import AuthContext from "../context/AuthContext";
3 | import axios from "axios";
4 |
5 | const baseURL = process.env.REACT_APP_BACKEND_URL;
6 |
7 | function useDataFetching(dataSource) {
8 | const { token, badge } = useContext(AuthContext);
9 | const [loading, setLoading] = useState(false);
10 | const [data, setData] = useState([]);
11 | const [error, setError] = useState("");
12 |
13 | useEffect(() => {
14 | axios
15 | .get(`${baseURL}/${dataSource}/`, {
16 | headers: {
17 | "Content-Type": "application/json",
18 | Authorization: "Bearer " + String(token.access),
19 | },
20 | })
21 | .then((response) => {
22 | setData(response.data);
23 | setLoading(false);
24 | })
25 | .catch((response) => {
26 | setLoading(false);
27 | setError(response.text);
28 | });
29 | }, [dataSource, badge]);
30 |
31 | return [loading, error, data];
32 | }
33 |
34 | export default useDataFetching;
35 |
--------------------------------------------------------------------------------
/src/components/Nav.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { MdOutlineDarkMode } from "react-icons/md";
3 | import { Link } from "react-router-dom";
4 |
5 | function Nav({}) {
6 | const dark = () => {
7 | if (localStorage.theme === "light") {
8 | document.documentElement.classList.add("dark");
9 | localStorage.theme = "dark";
10 | } else {
11 | document.documentElement.classList.remove("dark");
12 | localStorage.theme = "light";
13 | }
14 | };
15 |
16 | return (
17 |
18 |
19 |
20 | SimpleProject {" "}
21 |
22 |
23 |
24 |
27 |
28 |
29 | );
30 | }
31 |
32 | export default Nav;
33 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Mahmoud Fettal
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/Task.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import Options from "./Options";
3 | import TaskForm from "./TaskForm";
4 | import date from "../support/Date";
5 |
6 | function Task({ id, title, body, created, onDragStart, project }) {
7 | const [edit, setEdit] = useState(false);
8 |
9 | return (
10 | <>
11 | {edit ? (
12 | {
14 | setEdit(false);
15 | }}
16 | editBody={body}
17 | editTitle={title}
18 | taskId={id}
19 | project={project}
20 | />
21 | ) : (
22 | onDragStart(e, id)}
26 | >
27 |
28 |
29 |
{title}
30 |
31 | {date(new Date(created))}
32 |
33 |
34 |
setEdit(true)} />
35 |
36 |
{body}
37 |
38 | )}
39 | >
40 | );
41 | }
42 |
43 | export default Task;
44 |
--------------------------------------------------------------------------------
/src/components/Search.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function Search() {
4 | return (
5 |
6 |
23 |
30 |
34 | Search
35 |
36 |
37 | );
38 | }
39 |
40 | export default Search;
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "project-board",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/react": "^11.10.0",
7 | "@emotion/styled": "^11.10.0",
8 | "@mui/material": "^5.9.3",
9 | "@tailwindcss/line-clamp": "^0.4.0",
10 | "@testing-library/jest-dom": "^5.16.4",
11 | "@testing-library/react": "^13.3.0",
12 | "@testing-library/user-event": "^13.5.0",
13 | "axios": "^0.27.2",
14 | "jwt-decode": "^3.1.2",
15 | "react": "^18.2.0",
16 | "react-dom": "^18.2.0",
17 | "react-icons": "^4.4.0",
18 | "react-router-dom": "^6.3.0",
19 | "react-scripts": "5.0.1",
20 | "web-vitals": "^2.1.4"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ]
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | },
46 | "devDependencies": {
47 | "autoprefixer": "^10.4.7",
48 | "postcss": "^8.4.14",
49 | "tailwindcss": "^3.1.6"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/Loading.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function Loading() {
4 | return (
5 |
6 | {" "}
7 |
13 |
17 |
21 |
22 |
Loading...
23 |
24 | );
25 | }
26 |
27 | export default Loading;
28 |
--------------------------------------------------------------------------------
/src/components/Alert.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | MdOutlineError,
4 | MdDangerous,
5 | MdCheckCircle,
6 | MdClose,
7 | } from "react-icons/md";
8 |
9 | const COLORS = {
10 | warning: {
11 | main: "border-orange-200 bg-orange-100",
12 | bg: "bg-orange-500",
13 | text: "text-orange-300 hover:text-orange-700",
14 | },
15 | danger: {
16 | main: "border-red-200 bg-red-100",
17 | bg: "bg-red-500",
18 | text: "text-red-300 hover:text-red-700",
19 | },
20 | success: {
21 | main: "border-green-200 bg-green-100",
22 | bg: "bg-green-500",
23 | text: "text-green-300 hover:text-green-700",
24 | },
25 | };
26 |
27 | const ICONS = {
28 | warning: ,
29 | danger: ,
30 | success: ,
31 | };
32 |
33 | function Alert({ type, title, message, close }) {
34 | return (
35 |
36 |
43 |
49 | {ICONS[type]}
50 |
51 |
52 |
{title}
53 |
{message}
54 |
55 |
56 |
57 |
58 | );
59 | }
60 |
61 | export default Alert;
62 |
--------------------------------------------------------------------------------
/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { MdOutlineDarkMode, MdLogout, MdViewStream } from "react-icons/md";
3 | import AuthContext from "../context/AuthContext";
4 | import { Link } from "react-router-dom";
5 |
6 | function Navbar({ sidebar }) {
7 | const { user, logoutUser } = useContext(AuthContext);
8 |
9 | const dark = () => {
10 | if (localStorage.theme === "light") {
11 | document.documentElement.classList.add("dark");
12 | localStorage.theme = "dark";
13 | } else {
14 | document.documentElement.classList.remove("dark");
15 | localStorage.theme = "light";
16 | }
17 | };
18 |
19 | return (
20 |
21 |
25 |
26 |
27 |
28 |
29 | Project Board
30 |
31 |
32 |
33 |
37 |
38 | |
39 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
50 | export default Navbar;
51 |
--------------------------------------------------------------------------------
/src/components/Lane.jsx:
--------------------------------------------------------------------------------
1 | import Task from "./Task";
2 | import TaskForm from "./TaskForm";
3 | import { useState } from "react";
4 |
5 | function Lane({
6 | laneId,
7 | title,
8 | loading,
9 | error,
10 | tasks,
11 | onDragStart,
12 | onDragOver,
13 | onDrop,
14 | project
15 | }) {
16 | const [open, setOpen] = useState(false);
17 | return (
18 | onDrop(e, laneId)}
22 | >
23 |
24 |
{title}
25 |
26 | {tasks.length}
27 |
28 |
29 |
30 | {loading || error ? (
31 | {error || "Loading..."}
32 | ) : (
33 | tasks.map((task) => (
34 |
42 | ))
43 | )}
44 | {open ? (
45 |
46 | ) : (
47 | setOpen(true)}
49 | className="bg-gray-200 py-2.5 rounded-md text-gray-700 dark:text-gray-200 hover:text-gray-900 hover:bg-gray-300 dark:bg-gray-800 dark:hover:text-white dark:hover:bg-gray-900"
50 | >
51 | Add Task
52 |
53 | )}
54 |
55 |
56 | );
57 | }
58 |
59 | export default Lane;
60 |
--------------------------------------------------------------------------------
/src/support/formFields.js:
--------------------------------------------------------------------------------
1 | const signInFields = [
2 | {
3 | labelText: "Username",
4 | labelFor: "username",
5 | id: "username",
6 | name: "username",
7 | type: "text",
8 | autoComplete: "username",
9 | isRequired: true,
10 | placeholder: "Username",
11 | },
12 | {
13 | labelText: "Password",
14 | labelFor: "password",
15 | id: "password",
16 | name: "password",
17 | type: "password",
18 | autoComplete: "current-password",
19 | isRequired: true,
20 | placeholder: "Password",
21 | },
22 | ];
23 |
24 | const signUpFields = [
25 | {
26 | labelText: "First name",
27 | labelFor: "firstname",
28 | id: "firstname",
29 | name: "firstname",
30 | type: "text",
31 | autoComplete: "firstname",
32 | isRequired: true,
33 | placeholder: "Enter your first name",
34 | },
35 | {
36 | labelText: "Last name",
37 | labelFor: "lastname",
38 | id: "lastname",
39 | name: "lastname",
40 | type: "text",
41 | autoComplete: "lastname",
42 | isRequired: true,
43 | placeholder: "Enter your last name",
44 | },
45 | {
46 | labelText: "Email",
47 | labelFor: "email",
48 | id: "email",
49 | name: "email",
50 | type: "email",
51 | autoComplete: "email",
52 | isRequired: true,
53 | placeholder: "Enter your email",
54 | },
55 | {
56 | labelText: "Username",
57 | labelFor: "username",
58 | id: "username",
59 | name: "username",
60 | type: "text",
61 | autoComplete: "username",
62 | isRequired: true,
63 | placeholder: "Choose a username",
64 | },
65 | {
66 | labelText: "Password",
67 | labelFor: "password",
68 | id: "password",
69 | name: "password",
70 | type: "password",
71 | autoComplete: "password",
72 | isRequired: true,
73 | placeholder: "Enter your password",
74 | },
75 | {
76 | labelText: "Confirm password",
77 | labelFor: "confirm",
78 | id: "confirm",
79 | name: "confirm",
80 | type: "password",
81 | autoComplete: "confirm",
82 | isRequired: true,
83 | placeholder: "Confirm your password",
84 | }
85 | ];
86 |
87 | export { signInFields, signUpFields };
88 |
89 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 | import Project from "./pages/Project";
3 | import Projects from "./pages/Projects";
4 | import SignIn from "./pages/SignIn";
5 | import SignUp from "./pages/SignUp";
6 | import { Routes, Route, Navigate } from "react-router-dom";
7 | import { AuthProvider } from "./context/AuthContext";
8 | import AuthContext from "./context/AuthContext";
9 | import { useContext, useEffect } from "react";
10 | import Alert from "./components/Alert";
11 | import Home from "./pages/Home";
12 | import NotFound from "./pages/NotFound";
13 |
14 | function PrivateRoute({ children, ..._ }) {
15 | const {
16 | user,
17 | loading,
18 | badge,
19 | message,
20 | type,
21 | title,
22 | setBadge,
23 | setTitle,
24 | setMessage,
25 | setType,
26 | } = useContext(AuthContext);
27 | useEffect(() => {
28 | setTimeout(() => {
29 | setBadge(false);
30 | setTitle("");
31 | setMessage("");
32 | setType("");
33 | }, 2500);
34 | }, [badge]);
35 | return (
36 | <>
37 | {badge && (
38 | {
43 | setBadge(false);
44 | }}
45 | />
46 | )}
47 | {!user ? : loading ? null : children}
48 | >
49 | );
50 | }
51 |
52 | function App() {
53 | useEffect(() => {
54 | if (localStorage.theme === "dark") {
55 | document.documentElement.classList.add("dark");
56 | } else {
57 | document.documentElement.classList.remove("dark");
58 | }
59 | }, []);
60 | return (
61 | <>
62 |
63 |
64 |
69 |
70 |
71 | }
72 | />
73 |
78 |
79 |
80 | }
81 | />
82 | } />
83 | } />
84 | } />
85 | } />
86 |
87 |
88 | >
89 | );
90 | }
91 |
92 | export default App;
93 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ProjectHeader.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from "react";
2 | import { MdDelete, MdModeEdit } from "react-icons/md";
3 | import { useNavigate } from "react-router-dom";
4 | import AuthContext from "../context/AuthContext";
5 | import axios from "axios";
6 | import date from "../support/Date";
7 |
8 | const baseURL = process.env.REACT_APP_BACKEND_URL;
9 |
10 | function ProjectHeader({ data, open }) {
11 | const { token, setTitle, setMessage, setBadge, setType } =
12 | useContext(AuthContext);
13 |
14 | const navigate = useNavigate();
15 |
16 | const deleteProject = () => {
17 | axios
18 | .delete(`${baseURL}/project/${data.id}`, {
19 | headers: {
20 | "Content-Type": "application/json",
21 | Authorization: "Bearer " + String(token.access),
22 | },
23 | })
24 | .then((response) => {
25 | setBadge(true);
26 | setTitle("Successful operation");
27 | setMessage("Project deleted successfully");
28 | setType("success");
29 | navigate("/projects");
30 | })
31 | .catch((response) => {
32 | setBadge(true);
33 | setTitle("Error");
34 | setMessage(response.data);
35 | setType("warning");
36 | });
37 | };
38 |
39 | return (
40 | <>
41 |
42 |
43 |
Project:
44 |
{data.name}
45 |
46 | Created: {date(new Date(data.created))}
47 |
48 |
49 | {data.description}
50 |
51 |
52 |
53 |
57 | Delete
58 |
59 |
63 | Edit
64 |
65 |
66 |
67 | >
68 | );
69 | }
70 |
71 | export default ProjectHeader;
72 |
--------------------------------------------------------------------------------
/src/components/Board.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useContext } from "react";
2 | import Lane from "./Lane";
3 | import useDataFetching from "../hooks/useDataFetching";
4 | import AuthContext from "../context/AuthContext";
5 | import axios from "axios";
6 |
7 | const lanes = [
8 | { id: 1, title: "To Do" },
9 | { id: 2, title: "In Progress" },
10 | { id: 3, title: "Review" },
11 | { id: 4, title: "Done" },
12 | ];
13 |
14 | const baseURL = process.env.REACT_APP_BACKEND_URL;
15 |
16 | function Board({ project }) {
17 | const [loading, error, data] = useDataFetching(`tasks/${project.slug}`);
18 | const [tasks, setTasks] = useState([]);
19 | const { token, badge, setTitle, setMessage, setBadge, setType } =
20 | useContext(AuthContext);
21 |
22 | function onDrop(e, laneId) {
23 | const id = e.dataTransfer.getData("id");
24 | const updatedTasks = tasks.filter((task) => {
25 | if (task.id.toString() === id) {
26 | task.stage = laneId;
27 | axios
28 | .put(
29 | `${baseURL}/task/${task.id}`,
30 | JSON.stringify({ stage: laneId }),
31 | {
32 | headers: {
33 | "Content-Type": "application/json",
34 | Authorization: "Bearer " + String(token.access),
35 | },
36 | }
37 | )
38 | .then((response) => {})
39 | .catch((response) => {
40 | setBadge(true);
41 | setTitle("Error");
42 | setMessage(response.data);
43 | setType("warning");
44 | });
45 | }
46 | return task;
47 | });
48 | setTasks(updatedTasks);
49 | }
50 |
51 | function onDragStart(event, id) {
52 | event.dataTransfer.setData("id", id);
53 | }
54 |
55 | function onDragOver(e) {
56 | e.preventDefault();
57 | }
58 |
59 | useEffect(() => {
60 | setTasks(data);
61 | }, [data, badge]);
62 |
63 | return (
64 |
65 |
69 | {lanes.map((lane) => (
70 | +task.stage === lane.id)}
77 | onDragStart={onDragStart}
78 | onDragOver={onDragOver}
79 | onDrop={onDrop}
80 | project={project}
81 | />
82 | ))}
83 |
84 |
85 | );
86 | }
87 |
88 | export default Board;
89 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
15 |
16 |
20 |
24 |
25 |
26 |
30 |
31 |
35 |
36 |
45 | Project Board
46 |
47 |
48 | You need to enable JavaScript to run this app.
49 |
50 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | # What is SimpleProject
25 | SimpleProject is a web app that allows users to create and manage projects them in a kanban board.
26 |
27 | SimpleProject was built with react and tailwind for the frontend, Django rest framework for the backend and hosted on Microsoft Azure, the app implements HTML5 drag and drop API to ensure the functionality of drag and dropping tasks into the right column, all in a minimalist design that is easy to use.
28 |
29 | ## Use the app :
30 | You can use the app using the link: [simple-project.smauj.me](https://simple-project.smauj.me/)
31 |
32 | ## Functionalities :
33 | ### Projects list:
34 | 
35 | ### Project view:
36 | 
37 | ### Dark mode:
38 | 
39 | ### Sign in:
40 | 
41 | ### Landing page:
42 | 
43 |
44 | # How to support the project:
45 | **:thumbsup: Your support means a lot. Thank you for stars that keeps me motivated to share new ideas and do them fast.**
46 |
47 | You can support also the project by [buy me a coffe](https://www.buymeacoffee.com/mahmoudfettal), that would help with the costs of hosting and keep me motivated to add new features in the future.
48 |
49 | # Lisence:
50 | You can clone the repos easily but you will have to create your own APIs in order for the app to work.
51 |
52 | Contact me if you need help.
53 |
--------------------------------------------------------------------------------
/src/components/Options.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useRef, useState } from "react";
2 | import { MdOutlineMoreVert, MdDelete, MdModeEdit } from "react-icons/md";
3 | import axios from "axios";
4 | import AuthContext from "../context/AuthContext";
5 |
6 | const baseURL = process.env.REACT_APP_BACKEND_URL;
7 |
8 | function Options({ taskId, edit }) {
9 | const { token, setTitle, setMessage, setBadge, setType, badge } =
10 | useContext(AuthContext);
11 | const [open, setOpen] = useState(false);
12 | const deleteTask = () => {
13 | axios
14 | .delete(`${baseURL}/task/${taskId}`, {
15 | headers: {
16 | "Content-Type": "application/json",
17 | Authorization: "Bearer " + String(token.access),
18 | },
19 | })
20 | .then((response) => {
21 | setBadge(true);
22 | setTitle("Successful operation");
23 | setMessage("Task deleted successfully");
24 | setType("success");
25 | })
26 | .catch((response) => {
27 | setBadge(true);
28 | setTitle("Error");
29 | setMessage(response.data);
30 | setType("warning");
31 | });
32 | };
33 |
34 | const ref = useRef(null);
35 |
36 | useEffect(() => {
37 | const handleClickOutside = (event) => {
38 | if (ref.current && !ref.current.contains(event.target)) {
39 | setOpen(false);
40 | }
41 | };
42 | document.addEventListener("click", handleClickOutside, true);
43 | return () => {
44 | document.removeEventListener("click", handleClickOutside, true);
45 | };
46 | });
47 |
48 | useEffect(() => {
49 | setTimeout(() => {
50 | setBadge(false);
51 | setTitle("");
52 | setMessage("");
53 | setType("");
54 | }, 5000);
55 | }, [badge]);
56 | return (
57 | <>
58 |
59 |
{
61 | setOpen(!open);
62 | }}
63 | className="text-gray-500 hover:text-gray-800 mb-4 dark:text-gray-300 dark:hover:text-white"
64 | />
65 |
72 |
77 |
78 | Delete task
79 |
80 |
84 |
85 | Edit task
86 |
87 |
88 |
89 | >
90 | );
91 | }
92 |
93 | export default Options;
94 |
--------------------------------------------------------------------------------
/src/pages/Home.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Nav from "../components/Nav";
3 | import app from "../static/app.png";
4 | import Footer from "../components/Footer";
5 |
6 | function Home() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | Welcome to{" "}
14 |
15 | {" "}
16 | SimpleProject {" "}
17 |
18 |
19 |
20 | Manage your projects easily and more effectively
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 | Why use SimpleProject?
35 |
36 |
37 |
41 |
42 |
43 | Simple to use
44 |
45 |
46 | The app is very intuitive and simple to use, so you can easily
47 | start working with it
48 |
49 |
50 |
51 |
52 | Free to use
53 |
54 |
55 | You can use the app for as many projects for free (for
56 | now XD)
57 |
58 |
59 |
60 |
61 | Always something new
62 |
63 |
64 | New features are always in the making you can also request them
65 | on the github repos
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | );
74 | }
75 |
76 | export default Home;
77 |
--------------------------------------------------------------------------------
/src/pages/SignIn.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext, useEffect } from "react";
2 | import Header from "../components/Header";
3 | import { signInFields } from "../support/formFields";
4 | import Input from "../components/Input";
5 | import FormOptions from "../components/FormOptions";
6 | import Submit from "../components/Submit";
7 | import AuthContext from "../context/AuthContext";
8 | import Alert from "../components/Alert";
9 |
10 | const fields = signInFields;
11 | let fieldsState = {};
12 | fields.forEach((field) => (fieldsState[field.id] = ""));
13 |
14 | function SignIn() {
15 | const { loginUser } = useContext(AuthContext);
16 | const [loginState, setLoginState] = useState(fieldsState);
17 |
18 | const {
19 | setMessage,
20 | setTitle,
21 | setBadge,
22 | setType,
23 | badge,
24 | message,
25 | type,
26 | title,
27 | } = useContext(AuthContext);
28 |
29 | useEffect(() => {
30 | setTimeout(() => {
31 | setBadge(false);
32 | setTitle("");
33 | setMessage("");
34 | setType("");
35 | }, 2500);
36 | }, [badge, setMessage, setTitle, setBadge, setType]);
37 |
38 | const handleChange = (e) => {
39 | setLoginState({ ...loginState, [e.target.id]: e.target.value });
40 | };
41 |
42 | return (
43 | <>
44 | {badge && (
45 | {
50 | setBadge(false);
51 | }}
52 | />
53 | )}
54 |
90 | >
91 | );
92 | }
93 |
94 | export default SignIn;
95 |
--------------------------------------------------------------------------------
/src/pages/Project.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from "react";
2 | import axios from "axios";
3 | import Navbar from "../components/Navbar";
4 | import ProjectHeader from "../components/ProjectHeader";
5 | import Board from "../components/Board";
6 | import AuthContext from "../context/AuthContext";
7 | import SideBar from "../components/SideBar";
8 | import notFound from "../static/24.svg";
9 | import Loading from "../components/Loading";
10 | import { useParams } from "react-router-dom";
11 | import ProjectForm from "../components/ProjectForm";
12 |
13 | const baseURL = process.env.REACT_APP_BACKEND_URL;
14 |
15 | function Project() {
16 | const { token, badge } = useContext(AuthContext);
17 |
18 | const [sidebar, setSidebar] = useState(false);
19 | const [loading, setLoading] = useState(true);
20 | const [project, setProject] = useState();
21 | const [open, setOpen] = useState(false);
22 |
23 | const { slug } = useParams();
24 | const loadProjects = () => {
25 | axios
26 | .get(`${baseURL}/projects/${slug}`, {
27 | headers: {
28 | "Content-Type": "application/json",
29 | Authorization: "Bearer " + String(token.access),
30 | },
31 | })
32 | .then((response) => {
33 | setProject(response.data);
34 | setLoading(false);
35 | })
36 | .catch((response) => {
37 | setLoading(false);
38 | });
39 | };
40 |
41 | useEffect(() => {
42 | loadProjects();
43 | }, [slug, badge]);
44 |
45 | return (
46 | <>
47 | {open && (
48 | setOpen(false)}
50 | projectName={project.name}
51 | projectDescription={project.description}
52 | id={project.id}
53 | />
54 | )}
55 | {sidebar && (
56 | {
58 | setSidebar(false);
59 | }}
60 | />
61 | )}
62 |
63 |
{
65 | setSidebar(true);
66 | }}
67 | />
68 |
69 | {project ? (
70 | <>
71 |
setOpen(true)} />
72 |
73 | >
74 | ) : (
75 |
76 | {loading ? (
77 |
78 | ) : (
79 |
80 |
81 | Project{" "}
82 |
83 | not found!
84 |
85 |
86 |
91 |
92 | )}
93 |
94 | )}
95 |
96 |
97 | >
98 | );
99 | }
100 |
101 | export default Project;
102 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .dropdown {
6 | position: relative;
7 | display: inline-block;
8 | }
9 |
10 | .dropdown-content {
11 | display: flex;
12 | flex-direction: column;
13 | position: absolute;
14 | min-width: 125px;
15 | box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
16 | right: 2px;
17 | top: 20px;
18 | z-index: 1;
19 | }
20 |
21 | .home::-webkit-scrollbar {
22 | width: 5px;
23 | height: 7.5px;
24 | }
25 |
26 | .home::-webkit-scrollbar-track {
27 | background: rgba(255, 255, 255, 0.1);
28 | }
29 |
30 | .home::-webkit-scrollbar-thumb {
31 | border-radius: 50px;
32 | background: rgba(165, 165, 165, 0.5);
33 | }
34 |
35 | .home::-webkit-scrollbar-thumb:hover {
36 | width: 15px;
37 | background: rgba(165, 165, 165, 0.75);
38 | }
39 |
40 | .sidebar::-webkit-scrollbar {
41 | width: 5px;
42 | height: 7.5px;
43 | }
44 |
45 | .sidebar::-webkit-scrollbar-track {
46 | background: rgba(255, 255, 255, 0.1);
47 | }
48 |
49 | .sidebar::-webkit-scrollbar-thumb {
50 | border-radius: 50px;
51 | background: rgba(165, 165, 165, 0.5);
52 | }
53 |
54 | .sidebar::-webkit-scrollbar-thumb:hover {
55 | width: 15px;
56 | background: rgba(165, 165, 165, 0.75);
57 | }
58 |
59 | body::-webkit-scrollbar {
60 | width: 5px;
61 | height: 7.5px;
62 | }
63 |
64 | body::-webkit-scrollbar-track {
65 | background: rgba(255, 255, 255, 0.1);
66 | }
67 |
68 | body::-webkit-scrollbar-thumb {
69 | border-radius: 50px;
70 | background: rgba(165, 165, 165, 0.5);
71 | }
72 |
73 | body::-webkit-scrollbar-thumb:hover {
74 | width: 15px;
75 | background: rgba(165, 165, 165, 0.75);
76 | }
77 |
78 | textarea::-webkit-scrollbar {
79 | width: 5px;
80 | height: 7.5px;
81 | }
82 |
83 | textarea::-webkit-scrollbar-track {
84 | background: rgba(255, 255, 255, 0.1);
85 | }
86 |
87 | textarea::-webkit-scrollbar-thumb {
88 | border-radius: 50px;
89 | background: rgba(165, 165, 165, 0.5);
90 | }
91 |
92 | textarea::-webkit-scrollbar-thumb:hover {
93 | width: 15px;
94 | background: rgba(165, 165, 165, 0.75);
95 | }
96 |
97 | .scrollbar::-webkit-scrollbar {
98 | width: 5px;
99 | height: 7.5px;
100 | }
101 |
102 | .scrollbar::-webkit-scrollbar-track {
103 | background: rgba(255, 255, 255, 0.1);
104 | }
105 |
106 | .scrollbar::-webkit-scrollbar-thumb {
107 | border-radius: 50px;
108 | background: rgba(165, 165, 165, 0.5);
109 | }
110 |
111 | .scrollbar::-webkit-scrollbar-thumb:hover {
112 | width: 15px;
113 | background: rgba(165, 165, 165, 0.75);
114 | }
115 |
116 | .App-logo {
117 | height: 40vmin;
118 | pointer-events: none;
119 | }
120 |
121 | @media (prefers-reduced-motion: no-preference) {
122 | .App-logo {
123 | animation: App-logo-spin infinite 20s linear;
124 | }
125 | }
126 |
127 | .header {
128 | grid-template-columns: 1fr 1fr;
129 | }
130 |
131 | @media (max-width: 1024px) {
132 | .header {
133 | grid-template-columns: 1fr auto;
134 | }
135 | }
136 |
137 | .App-header {
138 | background-color: #282c34;
139 | min-height: 100vh;
140 | display: flex;
141 | flex-direction: column;
142 | align-items: center;
143 | justify-content: center;
144 | font-size: calc(10px + 2vmin);
145 | color: transparent;
146 | }
147 |
148 | .App-link {
149 | color: #61dafb;
150 | }
151 |
152 | @keyframes App-logo-spin {
153 | from {
154 | transform: rotate(0deg);
155 | }
156 | to {
157 | transform: rotate(360deg);
158 | }
159 | }
160 |
161 | input:-webkit-autofill,
162 | input:-webkit-autofill:hover,
163 | input:-webkit-autofill:focus,
164 | input:-webkit-autofill:active {
165 | transition: background-color 5000s ease-in-out 0s;
166 | -webkit-text-fill-color: gray !important;
167 | }
168 | .spin{
169 | animation: spin 15s linear infinite;
170 | }
171 |
172 | @keyframes spin {
173 | from {
174 | transform: rotate(0deg);
175 | }
176 | to {
177 | transform: rotate(360deg);
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import { FaLinkedinIn } from "react-icons/fa";
2 |
3 | function Footer() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 | You want to support my app?
11 |
12 |
13 | Buy me a coffee using the{" "}
14 |
15 | Link
16 |
17 |
18 |
19 |
34 |
35 |
89 |
90 |
91 | );
92 | }
93 |
94 | export default Footer;
95 |
--------------------------------------------------------------------------------
/src/pages/Projects.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from "react";
2 | import Navbar from "../components/Navbar";
3 | import SideBar from "../components/SideBar";
4 | import AuthContext from "../context/AuthContext";
5 | import { MdNavigateNext } from "react-icons/md";
6 | import { Link } from "react-router-dom";
7 | import date from "../support/Date";
8 | import ProjectForm from "../components/ProjectForm";
9 | import { useNavigate } from "react-router-dom";
10 |
11 | function Projects() {
12 | const { user, projects } = useContext(AuthContext);
13 |
14 | const [sidebar, setSidebar] = useState(false);
15 | const [open, setOpen] = useState(false);
16 | const navigate = useNavigate()
17 |
18 | return (
19 | <>
20 | {open && setOpen(false)}/>}
21 | {sidebar && (
22 | {
24 | setSidebar(false);
25 | }}
26 | />
27 | )}
28 |
29 |
{
31 | setSidebar(true);
32 | }}
33 | />
34 | {projects ? (
35 |
36 |
37 |
38 |
39 | Welcome back,{" "}
40 |
41 | {user.username}!
42 |
43 |
44 |
45 | Your Projects
46 |
47 |
48 |
49 |
50 |
51 | {projects.map((project, key) => {
52 | return (
53 |
{navigate(`/project/${project.slug}`)}}
57 | >
58 |
62 |
63 |
64 | {project.name}
65 |
66 |
67 | {date(new Date(project.created))}
68 |
69 |
70 |
74 |
75 |
76 |
77 |
78 | {project.description}
79 |
80 |
81 | );
82 | })}
83 |
84 |
85 | Add a new project
86 |
87 | {
89 | setOpen(true);
90 | }}
91 | className="bg-gray-900 dark:bg-white h-10 w-10 text-white dark:text-gray-900 rounded-full text-xl"
92 | >
93 | {" "}
94 | +{" "}
95 |
96 |
97 |
98 |
99 |
100 | ) : (
101 | nothing
102 | )}
103 |
104 | >
105 | );
106 | }
107 |
108 | export default Projects;
109 |
--------------------------------------------------------------------------------
/src/components/TaskForm.jsx:
--------------------------------------------------------------------------------
1 | import { MdOutlineOpenInFull, MdClose, MdCheck } from "react-icons/md";
2 | import axios from "axios";
3 | import AuthContext from "../context/AuthContext";
4 | import { useContext, useEffect, useState } from "react";
5 |
6 | const baseURL = process.env.REACT_APP_BACKEND_URL;
7 |
8 | function TaskForm({ close, laneId, editTitle, editBody, edit, taskId, project }) {
9 | const { token, setTitle, setMessage, setBadge, setType, badge } =
10 | useContext(AuthContext);
11 |
12 | const [title, setTaskTitle] = useState(editTitle ? editTitle : "");
13 | const [body, setBody] = useState(editBody ? editBody : "");
14 |
15 | const saveTask = (event) => {
16 | event.preventDefault();
17 |
18 | if (title === "") {
19 | setBadge(true);
20 | setType("danger");
21 | setTitle("Error");
22 | setMessage("Title musn't be empty!");
23 | } else if (body === "") {
24 | setBadge(true);
25 | setTitle("Error");
26 | setTitle("Error");
27 | setType("danger");
28 | setMessage("Body musn't be empty!");
29 | } else {
30 | const data = new FormData();
31 | data.append("title", title);
32 | data.append("body", body);
33 |
34 | if (edit) {
35 | axios
36 | .put(`${baseURL}/task/${taskId}`, data, {
37 | headers: {
38 | "Content-Type": "application/json",
39 | Authorization: "Bearer " + String(token.access),
40 | },
41 | })
42 | .then((response) => {
43 | setBadge(true);
44 | setTitle("Successful operation");
45 | setMessage("Task updated successfully");
46 | setType("success");
47 | setTaskTitle("");
48 | setBody("");
49 | edit();
50 | })
51 | .catch((response) => {
52 | console.log(response);
53 | setType("danger");
54 | setTitle("Error");
55 | setMessage(response.data);
56 | setBadge(true);
57 | });
58 | } else {
59 | data.append("stage", laneId);
60 | data.append("project", project.id);
61 | axios
62 | .post(`${baseURL}/task/`, data, {
63 | headers: {
64 | "Content-Type": "application/json",
65 | Authorization: "Bearer " + String(token.access),
66 | },
67 | })
68 | .then((response) => {
69 | close(false);
70 | setBadge(true);
71 | setTitle("Successful operation");
72 | setMessage("Task deleted successfully");
73 | setType("success");
74 | setTaskTitle("");
75 | setBody("");
76 | })
77 | .catch((response) => {
78 | setType("danger");
79 | setTitle("Error");
80 | setMessage(response.data);
81 | setBadge(true);
82 | });
83 | }
84 | }
85 | };
86 |
87 | useEffect(() => {
88 | setTimeout(() => {
89 | setBadge(false);
90 | setTitle("");
91 | setMessage("");
92 | setType("");
93 | }, 5000);
94 | }, [badge]);
95 |
96 | return (
97 | <>
98 |
99 |
100 |
101 |
102 | Task title
103 |
104 | {
111 | setTaskTitle(event.target.value);
112 | }}
113 | />
114 |
115 |
116 |
117 | {
119 | if (close) {
120 | close(false);
121 | }
122 | if (edit) {
123 | edit();
124 | }
125 | }}
126 | className="text-lg text-gray-500 hover:text-gray-800 dark:hover:text-gray-200"
127 | />
128 |
132 |
133 |
134 |
135 | Task body
136 |
137 |
149 | >
150 | );
151 | }
152 |
153 | export default TaskForm;
154 |
--------------------------------------------------------------------------------
/src/context/AuthContext.js:
--------------------------------------------------------------------------------
1 | import { createContext, useEffect, useState } from "react";
2 | import jwt_decode from "jwt-decode";
3 | import { useNavigate } from "react-router-dom";
4 | import axios from "axios";
5 |
6 | const baseURL = process.env.REACT_APP_BACKEND_URL;
7 | const AuthContext = createContext();
8 |
9 | export default AuthContext;
10 |
11 | export const AuthProvider = ({ children }) => {
12 | const [user, setUser] = useState(() => {
13 | return localStorage.getItem("authTokens") ||
14 | sessionStorage.getItem("authTokens")
15 | ? jwt_decode(
16 | localStorage.getItem("authTokens")
17 | ? localStorage.getItem("authTokens")
18 | : sessionStorage.getItem("authTokens")
19 | )
20 | : null;
21 | });
22 | const [token, setToken] = useState(() => {
23 | return localStorage.getItem("authTokens") ||
24 | sessionStorage.getItem("authTokens")
25 | ? JSON.parse(
26 | localStorage.getItem("authTokens")
27 | ? localStorage.getItem("authTokens")
28 | : sessionStorage.getItem("authTokens")
29 | )
30 | : null;
31 | });
32 | const [loading, setLoading] = useState(true);
33 | const [remember, setRemember] = useState(true);
34 | const [badge, setBadge] = useState(false);
35 | const [type, setType] = useState("");
36 | const [title, setTitle] = useState("");
37 | const [message, setMessage] = useState("");
38 | let [projects, setProjects] = useState([]);
39 |
40 | const navigate = useNavigate();
41 |
42 | const loginUser = async (event) => {
43 | event.preventDefault();
44 |
45 | axios
46 | .post(
47 | `${baseURL}/token/`,
48 | JSON.stringify({
49 | username: event.target.username.value,
50 | password: event.target.password.value,
51 | }),
52 | {
53 | headers: {
54 | "Content-Type": "application/json",
55 | },
56 | }
57 | )
58 | .then((response) => {
59 | setRemember(event.target.remember.checked);
60 | setToken(response.data);
61 | setUser(jwt_decode(response.data.access));
62 | event.target.remember.checked
63 | ? localStorage.setItem("authTokens", JSON.stringify(response.data))
64 | : sessionStorage.setItem("authTokens", JSON.stringify(response.data));
65 | navigate("/projects");
66 | })
67 | .catch((response) => {
68 | setBadge(true);
69 | setTitle("Sign in error");
70 | setMessage("Password and username combination do not match!");
71 | setType("danger");
72 | });
73 | };
74 |
75 | const updateToken = async (event) => {
76 | if (loading) {
77 | setLoading(false);
78 | } else if (token) {
79 | axios
80 | .post(
81 | `${baseURL}/token/refresh/`,
82 | {
83 | refresh: token?.refresh,
84 | },
85 | {
86 | headers: {
87 | "Content-Type": "application/json",
88 | },
89 | }
90 | )
91 | .then((response) => {
92 | if (response.status === 200) {
93 | setToken(response.data);
94 | setUser(jwt_decode(response.data.access));
95 | remember
96 | ? localStorage.setItem(
97 | "authTokens",
98 | JSON.stringify(response.data)
99 | )
100 | : sessionStorage.setItem(
101 | "authTokens",
102 | JSON.stringify(response.data)
103 | );
104 | } else {
105 | logoutUser();
106 | }
107 | });
108 | }
109 | };
110 |
111 | const logoutUser = () => {
112 | setToken(null);
113 | setUser(null);
114 | localStorage.removeItem("authTokens");
115 | sessionStorage.removeItem("authTokens");
116 | navigate("/signin");
117 | };
118 |
119 | useEffect(() => {
120 | if (loading) {
121 | updateToken();
122 | }
123 | let interval = setInterval(() => {
124 | if (token) {
125 | updateToken();
126 | }
127 | }, 1000 * 60 * 59 * 4);
128 |
129 | return () => clearInterval(interval);
130 | }, [loading, token]);
131 |
132 | useEffect(() => {
133 | if (token) {
134 | axios
135 | .get(`${baseURL}/projects/`, {
136 | headers: {
137 | "Content-Type": "application/json",
138 | Authorization: "Bearer " + String(token?.access),
139 | },
140 | })
141 | .then((response) => {
142 | setProjects(response.data);
143 | })
144 | .catch((response) => {
145 | console.log(response);
146 | });
147 | }
148 | }, [badge, token]);
149 |
150 | const contextData = {
151 | loginUser: loginUser,
152 | logoutUser: logoutUser,
153 | token: token,
154 | setToken: setToken,
155 | setUser: setUser,
156 | user: user,
157 | badge: badge,
158 | setBadge: setBadge,
159 | type: type,
160 | setType: setType,
161 | message: message,
162 | setMessage: setMessage,
163 | title: title,
164 | setTitle: setTitle,
165 | projects: projects,
166 | setProjects: setProjects,
167 | loading: loading,
168 | remember: setRemember,
169 | };
170 |
171 | return (
172 | {children}
173 | );
174 | };
175 |
--------------------------------------------------------------------------------
/src/components/ProjectForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useRef, useState } from "react";
2 | import axios from "axios";
3 | import AuthContext from "../context/AuthContext";
4 | import { useNavigate } from "react-router-dom";
5 |
6 | const baseURL = process.env.REACT_APP_BACKEND_URL;
7 |
8 | function ProjectForm({ close, id, projectName = "", projectDescription = "" }) {
9 | const [name, setName] = useState(projectName);
10 | const [description, setDescription] = useState(projectDescription);
11 | const { token, setTitle, setMessage, setBadge, setType, badge } =
12 | useContext(AuthContext);
13 |
14 | const navigate = useNavigate();
15 | const saveProject = (event) => {
16 | event.preventDefault();
17 |
18 | if (name === "") {
19 | setBadge(true);
20 | setType("danger");
21 | setTitle("Error");
22 | setMessage("Title musn't be empty!");
23 | } else if (description === "") {
24 | setBadge(true);
25 | setTitle("Error");
26 | setTitle("Error");
27 | setType("danger");
28 | setMessage("Body musn't be empty!");
29 | } else {
30 | const data = new FormData();
31 | data.append("name", name);
32 | data.append("description", description);
33 |
34 | if (id) {
35 | axios
36 | .put(`${baseURL}/project/${id}`, data, {
37 | headers: {
38 | "Content-Type": "application/json",
39 | Authorization: "Bearer " + String(token.access),
40 | },
41 | })
42 | .then((response) => {
43 | setBadge(true);
44 | setTitle("Successful operation");
45 | setMessage("Project updated successfully");
46 | setType("success");
47 | setName("");
48 | setDescription("");
49 | close();
50 | navigate(`/project/${response.data}`);
51 | })
52 | .catch((response) => {
53 | setBadge(true);
54 | setType("danger");
55 | setTitle("Error");
56 | setMessage(response.data);
57 | });
58 | } else {
59 | axios
60 | .post(`${baseURL}/project/`, data, {
61 | headers: {
62 | "Content-Type": "application/json",
63 | Authorization: "Bearer " + String(token.access),
64 | },
65 | })
66 | .then((response) => {
67 | setBadge(true);
68 | setTitle("Successful operation");
69 | setMessage("Project created successfully");
70 | setType("success");
71 | setName("");
72 | setDescription("");
73 | navigate(`/project/${response.data}`);
74 | })
75 | .catch((response) => {
76 | setBadge(true);
77 | setType("danger");
78 | setTitle("Error");
79 | setMessage(response.response.data);
80 | });
81 | }
82 | }
83 | };
84 |
85 | const ref = useRef(null);
86 |
87 | useEffect(() => {
88 | const handleClickOutside = (event) => {
89 | if (ref.current && !ref.current.contains(event.target)) {
90 | close();
91 | }
92 | };
93 | document.addEventListener("click", handleClickOutside, true);
94 | return () => {
95 | document.removeEventListener("click", handleClickOutside, true);
96 | };
97 | });
98 |
99 | return (
100 | <>
101 |
159 | >
160 | );
161 | }
162 |
163 | export default ProjectForm;
164 |
--------------------------------------------------------------------------------
/src/components/SideBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState, useEffect } from "react";
2 | import AuthContext from "../context/AuthContext";
3 | import axios from "axios";
4 | import { Link } from "react-router-dom";
5 |
6 | const baseURL = process.env.REACT_APP_BACKEND_URL;
7 |
8 | function SideBar({ sidebar }) {
9 | const {
10 | token,
11 | user,
12 | setTitle,
13 | setMessage,
14 | setBadge,
15 | setType,
16 | badge,
17 | projects,
18 | } = useContext(AuthContext);
19 |
20 | const [add, setAdd] = useState(false);
21 | const [name, setName] = useState("");
22 | const [description, setDescription] = useState("");
23 |
24 | const saveProject = (event) => {
25 | event.preventDefault();
26 |
27 | if (name === "") {
28 | setBadge(true);
29 | setType("danger");
30 | setTitle("Error");
31 | setMessage("Title musn't be empty!");
32 | } else if (description === "") {
33 | setBadge(true);
34 | setTitle("Error");
35 | setTitle("Error");
36 | setType("danger");
37 | setMessage("Body musn't be empty!");
38 | } else {
39 | const data = new FormData();
40 | data.append("name", name);
41 | data.append("description", description);
42 |
43 | axios
44 | .post(`${baseURL}/project/`, data, {
45 | headers: {
46 | "Content-Type": "application/json",
47 | Authorization: "Bearer " + String(token.access),
48 | },
49 | })
50 | .then((response) => {
51 | setBadge(true);
52 | setTitle("Successful operation");
53 | setMessage("Task deleted successfully");
54 | setType("success");
55 | setName("");
56 | setDescription("");
57 | setAdd(false);
58 | })
59 | .catch((response) => {
60 | setType("danger");
61 | setTitle("Error");
62 | setMessage(response.data);
63 | setBadge(true);
64 | });
65 | }
66 | };
67 |
68 | useEffect(() => {
69 | setTimeout(() => {
70 | setBadge(false);
71 | setTitle("");
72 | setMessage("");
73 | setType("");
74 | }, 5000);
75 | }, [badge]);
76 |
77 | return (
78 | <>
79 |
83 |
84 |
85 |
86 | Hello {user.username} ,
87 |
88 |
89 | Your projects
90 |
91 |
92 |
93 | {projects.map((project, key) => {
94 | return (
95 |
{
99 | sidebar();
100 | }}
101 | className="block w-full text-left dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 py-3 px-7 overflow-x-hidden"
102 | style={{ whiteSpace: "nowrap" }}
103 | >
104 | {project.name}
105 |
106 | );
107 | })}
108 |
109 |
110 | {add ? (
111 |
112 |
113 |
114 |
115 | Project title
116 |
117 | {
124 | setName(event.target.value);
125 | }}
126 | />
127 |
128 |
129 |
130 | Project description
131 |
132 | {
140 | setDescription(event.target.value);
141 | }}
142 | />
143 |
144 | {
146 | setAdd(false);
147 | }}
148 | className="w-full bg-gray-200 py-1.5 rounded-md text-gray-700 dark:text-gray-200 hover:text-gray-900 hover:bg-gray-300 dark:bg-gray-700 dark:hover:text-white dark:hover:bg-gray-800"
149 | >
150 | Cancel
151 |
152 |
157 |
158 |
159 | ) : (
160 |
{
162 | setAdd(true);
163 | }}
164 | className="w-full bg-gray-200 py-2.5 rounded-md text-gray-700 dark:text-gray-200 hover:text-gray-900 hover:bg-gray-300 dark:bg-gray-700 dark:hover:text-white dark:hover:bg-gray-800"
165 | >
166 | Add a project
167 |
168 | )}
169 |
170 |
171 | >
172 | );
173 | }
174 |
175 | export default SideBar;
176 |
--------------------------------------------------------------------------------
/src/pages/SignUp.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext, useEffect } from "react";
2 | import Header from "../components/Header";
3 | import axios from "axios";
4 | import { signUpFields } from "../support/formFields";
5 | import Input from "../components/Input";
6 | import Submit from "../components/Submit";
7 | import AuthContext from "../context/AuthContext";
8 | import jwt_decode from "jwt-decode";
9 | import { useNavigate } from "react-router-dom";
10 | import Alert from "../components/Alert";
11 |
12 | const baseURL = process.env.REACT_APP_BACKEND_URL;
13 | const fields = signUpFields;
14 | let fieldsState = {};
15 | fields.forEach((field) => (fieldsState[field.id] = ""));
16 |
17 | function SignUp() {
18 | const {
19 | setMessage,
20 | setTitle,
21 | setBadge,
22 | remember,
23 | setType,
24 | setToken,
25 | setUser,
26 | badge,
27 | message,
28 | type,
29 | title,
30 | } = useContext(AuthContext);
31 |
32 | useEffect(() => {
33 | setTimeout(() => {
34 | setBadge(false);
35 | setTitle("");
36 | setMessage("");
37 | setType("");
38 | }, 2500);
39 | }, [badge, setMessage, setTitle, setBadge, setType]);
40 |
41 | const [loginState, setLoginState] = useState(fieldsState);
42 | const [phase, setPhase] = useState(0);
43 | const navigate = useNavigate();
44 |
45 | const register = (event) => {
46 | event.preventDefault();
47 |
48 | const formData = new FormData();
49 | formData.append("first_name", loginState["firstname"]);
50 | formData.append("last_name", loginState["lastname"]);
51 | formData.append("email", loginState["email"]);
52 | formData.append("username", loginState["username"]);
53 | formData.append("password", loginState["password"]);
54 | formData.append("password2", loginState["confirm"]);
55 |
56 | axios
57 | .post(`${baseURL}/register/`, formData, {
58 | headers: {
59 | "Content-Type": "application/json",
60 | },
61 | })
62 | .then((response) => {
63 | setTimeout(() => {
64 | axios
65 | .post(
66 | `${baseURL}/token/`,
67 | JSON.stringify({
68 | username: loginState["username"],
69 | password: loginState["password"],
70 | }),
71 | {
72 | headers: {
73 | "Content-Type": "application/json",
74 | },
75 | }
76 | )
77 | .then((response) => {
78 | remember(false);
79 | setToken(response.data);
80 | setUser(jwt_decode(response.data.access));
81 | navigate("/projects");
82 | })
83 | .catch((response) => {});
84 | }, 1000);
85 | })
86 | .catch((response) => {
87 | setPhase(0);
88 | setBadge(true);
89 | setType("danger");
90 |
91 | if (response.response.data.email) {
92 | setTitle("Email Error");
93 | setMessage("This email is already used");
94 | } else if (response.response.data.username) {
95 | setTitle("Username Error");
96 | setMessage(response.response.data.username);
97 | } else if (response.response.data.password) {
98 | setTitle("Password Error");
99 | setMessage(response.response.data.password[0]);
100 | } else {
101 | setTitle("Sign Up error");
102 | setMessage("An error has occured!");
103 | }
104 | });
105 | };
106 |
107 | const handleChange = (e) => {
108 | setLoginState({ ...loginState, [e.target.id]: e.target.value });
109 | };
110 |
111 | return (
112 | <>
113 | {badge && (
114 | {
119 | setBadge(false);
120 | }}
121 | />
122 | )}
123 |
124 |
128 |
129 |
130 |
136 |
137 |
138 | {fields.slice(phase * 3, (phase + 1) * 3).map((field) => (
139 |
151 | ))}
152 |
153 | {phase === 0 && (
154 | {
156 | setPhase(phase + 1);
157 | }}
158 | className="group font-semibold relative w-full flex justify-center py-2 px-4 border border-transparent rounded-md text-white bg-gray-500 hover:bg-gray-600 dark:bg-gray-700 dark:hover:bg-gray-800 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-gray-600"
159 | >
160 | Next
161 |
162 | )}
163 | {phase === 1 && (
164 |
165 | {
167 | setPhase(phase - 1);
168 | }}
169 | className="group h-fit font-semibold relative flex justify-center py-2 px-4 border border-transparent rounded-md text-white bg-gray-500 hover:bg-gray-600 dark:bg-gray-700 dark:hover:bg-gray-800 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-gray-600"
170 | >
171 | Back
172 |
173 |
174 |
175 | )}
176 |
177 |
178 |
179 |
180 | >
181 | );
182 | }
183 |
184 | export default SignUp;
185 |
--------------------------------------------------------------------------------
/src/static/24.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
22 |
24 |
26 |
28 |
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
52 |
54 |
56 |
57 |
58 |
59 |
62 |
63 |
64 |
67 |
68 |
69 |
70 |
71 |
72 |
77 |
78 |
79 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
94 |
95 |
96 |
97 |
106 |
107 |
108 |
109 |
112 |
113 |
114 |
116 |
117 |
118 |
119 |
120 |
121 |
128 |
129 |
130 |
131 |
132 |
134 |
135 |
136 |
138 |
139 |
140 |
143 |
144 |
145 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
170 |
171 |
172 |
173 |
174 |
175 |
180 |
181 |
182 |
183 |
184 |
186 |
187 |
188 |
189 |
193 |
194 |
195 |
198 |
199 |
200 |
205 |
206 |
207 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
220 |
221 |
222 |
225 |
226 |
228 |
229 |
231 |
232 |
233 |
236 |
237 |
238 |
243 |
244 |
245 |
249 |
250 |
251 |
253 |
254 |
255 |
257 |
258 |
259 |
261 |
262 |
263 |
265 |
266 |
267 |
269 |
270 |
271 |
273 |
274 |
275 |
276 |
277 |
283 |
284 |
285 |
286 |
292 |
297 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
312 |
313 |
314 |
315 |
320 |
325 |
326 |
327 |
328 |
333 |
334 |
335 |
336 |
344 |
358 |
359 |
360 |
361 |
373 |
385 |
386 |
387 |
389 |
390 |
391 |
392 |
393 |
395 |
396 |
397 |
398 |
399 |
401 |
402 |
403 |
404 |
405 |
407 |
408 |
409 |
410 |
411 |
413 |
414 |
415 |
416 |
418 |
420 |
423 |
426 |
427 |
428 |
429 |
432 |
434 |
435 |
437 |
438 |
439 |
440 |
441 |
--------------------------------------------------------------------------------