├── .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 | not found 13 | 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 | 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 | 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 | 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 |
7 | 22 |
23 | 30 | 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 | 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 | 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 | 59 | 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 | 49 |
50 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | GitHub issues 8 | 9 | 10 | GitHub forks 11 | 12 | 13 | GitHub stars 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 | ![image](https://user-images.githubusercontent.com/46266986/187219476-79af8483-2820-4861-92a9-58a76a3f98b3.png) 35 | ### Project view: 36 | ![image](https://user-images.githubusercontent.com/46266986/187219570-8d6ae78a-02bc-4ced-9156-da6e135a858e.png) 37 | ### Dark mode: 38 | ![image](https://user-images.githubusercontent.com/46266986/187219896-ae4b8f02-98f6-453a-ac40-4b523800e803.png) 39 | ### Sign in: 40 | ![image](https://user-images.githubusercontent.com/46266986/187220094-d15be4b8-a811-4a32-bc3a-8099dec47ed1.png) 41 | ### Landing page: 42 | ![image](https://user-images.githubusercontent.com/46266986/187220176-0ae0704c-e6e3-4685-ad65-11c34aec12c3.png) 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 | 80 | 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 |
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 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
67 |
68 |
69 | {fields.map((field) => ( 70 | 82 | ))} 83 |
84 | 85 | 86 | 87 |
88 |
89 |
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 | not found 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 | 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 | 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 | 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 | 137 |