├── public ├── _redirects ├── favicon.ico ├── robots.txt ├── manifest.json └── index.html ├── src ├── styles │ ├── plan.png │ ├── Circular.ttf │ ├── index.css │ └── custom.css ├── index.js ├── components │ ├── Modal.jsx │ ├── ChecklistProgress.jsx │ ├── Task.jsx │ ├── Checklist.jsx │ ├── BoardList.jsx │ ├── Column.jsx │ └── Icons.jsx ├── firebase │ └── fbConfig.js ├── hooks │ ├── useBoards.js │ ├── useAuth.js │ └── useKanbanData.js ├── App.js ├── screens │ ├── Login.jsx │ ├── Home.jsx │ ├── AddTask.jsx │ ├── TaskDetails.jsx │ └── Kanban.jsx └── utils.js ├── screenshots ├── details.png ├── kanban.png ├── landing.png └── board-list.png ├── craco.config.js ├── .gitignore ├── tailwind.config.js ├── LICENSE ├── package.json └── README.md /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drkPrince/agilix/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/styles/plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drkPrince/agilix/HEAD/src/styles/plan.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /screenshots/details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drkPrince/agilix/HEAD/screenshots/details.png -------------------------------------------------------------------------------- /screenshots/kanban.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drkPrince/agilix/HEAD/screenshots/kanban.png -------------------------------------------------------------------------------- /screenshots/landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drkPrince/agilix/HEAD/screenshots/landing.png -------------------------------------------------------------------------------- /src/styles/Circular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drkPrince/agilix/HEAD/src/styles/Circular.ttf -------------------------------------------------------------------------------- /screenshots/board-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drkPrince/agilix/HEAD/screenshots/board-list.png -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | style: { 3 | postcss: { 4 | plugins: [ 5 | require('tailwindcss'), 6 | require('autoprefixer'), 7 | ], 8 | }, 9 | }, 10 | } -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .select { 7 | @apply px-1 py-2 mr-3 outline-none bg-gray-300 rounded-sm hover:bg-gray-400; 8 | } 9 | 10 | .option { 11 | @apply bg-gray-200 outline-none my-6 border-none py-3; 12 | } 13 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './styles/index.css'; 4 | import './styles/custom.css'; 5 | import App from './App'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Agilix", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.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 | .env 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: { 6 | colors: { 7 | primary: '#5222d0', 8 | secondary: '#ec615b' 9 | }, 10 | gridAutoColumns: { 11 | '270': '270px', 12 | '220': '220px', 13 | } 14 | }, 15 | }, 16 | variants: { 17 | extend: {}, 18 | }, 19 | plugins: [require('@tailwindcss/typography')], 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Modal.jsx: -------------------------------------------------------------------------------- 1 | import { Dialog } from "@reach/dialog" 2 | import { Cross } from './Icons' 3 | 4 | const Modal = ({ modal, setModal, children, ariaText }) => { 5 | 6 | return ( 7 | setModal(false)} aria-label={ariaText} className='z-20 fade-in'> 8 |
9 |
setModal(false)} > 10 |
11 | 12 |
13 |
14 |
15 | {children} 16 |
17 | ) 18 | } 19 | 20 | export default Modal -------------------------------------------------------------------------------- /src/components/ChecklistProgress.jsx: -------------------------------------------------------------------------------- 1 | import {CheckedOutline} from './Icons' 2 | 3 | const ChecklistProgress = ({todos}) => { 4 | const tasksCompleted = todos.filter(todo => todo.done === true) 5 | 6 | return ( 7 |
8 |
9 | 10 |

{`${tasksCompleted.length}/${todos.length}`}

11 |
12 | 13 |
14 | ) 15 | } 16 | 17 | export default ChecklistProgress -------------------------------------------------------------------------------- /src/firebase/fbConfig.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app' 2 | import 'firebase/firestore' 3 | import 'firebase/auth' 4 | 5 | 6 | const APIKEY = process.env.REACT_APP_APIKEY 7 | 8 | const firebaseConfig = 9 | { 10 | apiKey: APIKEY, 11 | authDomain: "kanban-42358.firebaseapp.com", 12 | projectId: "kanban-42358", 13 | storageBucket: "kanban-42358.appspot.com", 14 | messagingSenderId: "300388039581", 15 | appId: "1:300388039581:web:dc35b313665b4c310d8d74", 16 | measurementId: "G-KCLK585LB9" 17 | }; 18 | 19 | // Initialize Firebase 20 | firebase.initializeApp(firebaseConfig) 21 | 22 | 23 | const db = firebase.firestore() 24 | 25 | // firebase.firestore().enablePersistence() 26 | 27 | export {firebase, db} 28 | -------------------------------------------------------------------------------- /src/hooks/useBoards.js: -------------------------------------------------------------------------------- 1 | import {useState, useEffect} from 'react' 2 | import {db} from '../firebase/fbConfig' 3 | 4 | const useBoards = (userId) => { 5 | const [boards, setBoards] = useState(null) 6 | 7 | useEffect(() => { 8 | return db.collection(`users`).doc(userId).get() 9 | .then(doc => { 10 | try { 11 | if(doc){ 12 | return db.collection(`users/${doc.id}/boards`).onSnapshot(snap => { 13 | const documents = [] 14 | snap.forEach(doc => documents.push({id: doc.id, ...doc.data()})) 15 | setBoards(documents) 16 | }) 17 | } 18 | else return 19 | } 20 | 21 | catch(e) { 22 | console.log(e) 23 | } 24 | }) 25 | }, [userId]) 26 | 27 | 28 | return boards 29 | } 30 | 31 | export default useBoards -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Prince Kumar Singh 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/App.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import useAuth from './hooks/useAuth' 4 | import Home from './screens/Home' 5 | import Login from './screens/Login' 6 | 7 | 8 | const App = () => 9 | { 10 | 11 | const [user, loginWithGoogle, logOut, error, anon] = useAuth() 12 | 13 | if(navigator.onLine !== true) 14 | { 15 | return ( 16 |
17 |
18 |

The network is disconnected. Connect and try again

19 |
20 |
21 | ) 22 | } 23 | 24 | //error while logging in 25 | if (error) 26 | return ( 27 |
28 |

{error}

29 | 30 |
31 | ) 32 | 33 | 34 | //Not logged in 35 | if (user === false) { 36 | return 37 | } 38 | 39 | //state of loading 40 | if (user === null) { 41 | return
42 | } 43 | 44 | //logged in 45 | else return 46 | } 47 | 48 | 49 | 50 | export default App; 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agility", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^6.1.1", 7 | "@reach/dialog": "^0.13.2", 8 | "@tailwindcss/typography": "^0.4.0", 9 | "@testing-library/jest-dom": "^5.11.4", 10 | "@testing-library/react": "^11.1.0", 11 | "@testing-library/user-event": "^12.1.10", 12 | "firebase": "^8.3.0", 13 | "react": "^17.0.1", 14 | "react-beautiful-dnd": "^13.0.0", 15 | "react-dom": "^17.0.1", 16 | "react-markdown": "^5.0.3", 17 | "react-router-dom": "^5.2.0", 18 | "react-scripts": "4.0.3", 19 | "remark-gfm": "^1.0.0", 20 | "uuid": "^8.3.2", 21 | "web-vitals": "^1.0.1" 22 | }, 23 | "scripts": { 24 | "start": "craco start", 25 | "build": "craco build", 26 | "test": "craco test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "@tailwindcss/postcss7-compat": "^2.0.3", 49 | "autoprefixer": "^9", 50 | "postcss": "^7", 51 | "tailwindcss": "npm:@tailwindcss/postcss7-compat" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/screens/Login.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | const Login = ({loginWithGoogle, signInAnon}) => 4 | { 5 | return ( 6 | <> 7 |
8 |
9 |

Stay on top of the game called life with Agilix.

10 |

Agilix is an opinionated, simplified Kanban planner for personal use that helps you organise your life and accomplish more.

11 |
12 | 13 | 14 |
15 |

* Your data will be deleted once you log out.

16 |
17 |
18 | plan 19 |
20 |
21 | 22 | ) 23 | } 24 | 25 | export default Login -------------------------------------------------------------------------------- /src/hooks/useAuth.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { firebase, db } from '../firebase/fbConfig' 3 | 4 | import {createBoardForAnons} from '../utils' 5 | 6 | const useAuth = () => 7 | { 8 | const [user, setUser] = useState(null) 9 | const [error, setError] = useState(null) 10 | 11 | const loginWithGoogle = async () => { 12 | try { 13 | const provider = new firebase.auth.GoogleAuthProvider() 14 | await firebase.auth().signInWithRedirect(provider) 15 | setError(null) 16 | } catch (err) { 17 | console.log(err) 18 | setError(err.message) 19 | } 20 | } 21 | 22 | const loginAnonymously = () => { 23 | firebase.auth().signInAnonymously() 24 | .then((user) => { 25 | console.log('Welcome Anon') 26 | createBoardForAnons(user.user.uid) 27 | }) 28 | } 29 | 30 | const logOut = () => { 31 | firebase.auth().signOut() 32 | } 33 | 34 | useEffect(() => { 35 | return firebase.auth().onAuthStateChanged(user => { 36 | if (user) { 37 | setUser(user) 38 | db.collection('users') 39 | .doc(user.uid) 40 | .set({ id: user.uid, name: user.displayName, email: user.email }, { merge: true }) 41 | } else setUser(false) 42 | }) 43 | }, [user]) 44 | 45 | return [user, loginWithGoogle, logOut, error, loginAnonymously] 46 | } 47 | 48 | 49 | export default useAuth -------------------------------------------------------------------------------- /src/screens/Home.jsx: -------------------------------------------------------------------------------- 1 | 2 | import {db} from '../firebase/fbConfig' 3 | import {BrowserRouter, Route} from 'react-router-dom' 4 | import useBoards from '../hooks/useBoards' 5 | 6 | import BoardList from '../components/BoardList' 7 | 8 | import Kanban from './Kanban' 9 | 10 | import {v4 as uuidv4} from 'uuid'; 11 | 12 | 13 | const Home = ({logOut, userId, loginWithGoogle, name, isAnon}) => 14 | { 15 | 16 | const boards = useBoards(userId) 17 | 18 | const addNewBoard = (e) => { 19 | e.preventDefault() 20 | const uid = uuidv4() 21 | 22 | db.collection(`users/${userId}/boards`) 23 | .doc(uid) 24 | .set({name: e.target.elements.boardName.value}) 25 | 26 | const columnOrder = {id: 'columnOrder', order: []} 27 | 28 | db.collection(`users/${userId}/boards/${uid}/columns`) 29 | .doc('columnOrder') 30 | .set(columnOrder) 31 | 32 | e.target.elements.boardName.value = '' 33 | 34 | } 35 | 36 | const deleteBoard = (id) => { 37 | db.collection(`users/${userId}/boards`) 38 | .doc(id) 39 | .delete() 40 | } 41 | 42 | return boards !== null ? ( 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ) :
55 | } 56 | 57 | export default Home -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 26 | Agilix 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Agilix - A simplified Kanban Planner. 2 | 3 | ![React](https://img.shields.io/badge/React-17.0.1-61dafb) 4 | ![React Beautiful DND](https://img.shields.io/badge/react_beautiful_dnd-^13.0.0-0baf7c) 5 | ![Firebase](https://img.shields.io/badge/Firebase-8.3.0-ffa611) 6 | ![Tailwind](https://img.shields.io/badge/Tailwind-2.0.3-06b6d4) 7 | ![React Markdown](https://img.shields.io/badge/react_markdown-^5.0.3-333383) 8 | [![Netlify Status](https://api.netlify.com/api/v1/badges/7e065aaf-ae63-4df2-ba16-b9e7ba491bf5/deploy-status)](https://app.netlify.com/sites/agilix/deploys) 9 | 10 | Agilix is an agile planner web app made with **React, Firebase, React Beautiful DND, TailwindCSS and React Markdown**. It is heavily inspired by Trello. 11 | 12 | [Checkout the website](http://agilix.netlify.app). If you like it, leave a 🌟 because it keeps a beginner motivated. 😊 13 | 14 | ## Features 15 | - Add multiple boards. 16 | - Google sign-in. 17 | - Ability to try it out with Guest mode. 18 | - Reorderable tasks and columns. 19 | - Add subtasks to tasks. 20 | - Reorderable subtasks. 21 | - Write Descriptions using Markdown. 22 | - Fully Responsive on mobile screens. 23 | 24 | ## Screenshots 25 | 26 | ### Landing Page 27 | 28 | 29 | 30 | ### After the user logs in 31 | 32 | 33 | 34 | ### The Kanban Board 35 | 36 | 37 | 38 | ### Task Details 39 | 40 | 41 | 42 | 43 | ## Todo 44 | 45 | - Implement WIP limitation. 46 | - Dark Mode. 47 | 48 | 49 | ## Author 50 | [Prince Kumar Singh](http://twitter.com/drkPrns) 51 | 52 | -------------------------------------------------------------------------------- /src/hooks/useKanbanData.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { db } from '../firebase/fbConfig' 3 | 4 | const useKanban = (userId, boardId) => { 5 | const [tasks, setTasks] = useState(null) 6 | const [columns, setColumns] = useState(null) 7 | const [final, setFinal] = useState(null) 8 | const [boardName, setBoardName] = useState('') 9 | 10 | 11 | useEffect(() => { 12 | return db.collection(`users/${userId}/boards/${boardId}/tasks`) 13 | .onSnapshot(snap => { 14 | const documents = [] 15 | snap.forEach(d => { 16 | documents.push({ id: d.id, ...d.data() }) 17 | }) 18 | setTasks(documents) 19 | }) 20 | }, [userId, boardId]) 21 | 22 | 23 | useEffect(() => { 24 | return db.collection(`users/${userId}/boards`) 25 | .doc(boardId) 26 | .get() 27 | .then(d => setBoardName(d.data().name)) 28 | }, [userId, boardId]) 29 | 30 | 31 | useEffect(() => { 32 | return db.collection(`users/${userId}/boards/${boardId}/columns`) 33 | .onSnapshot(snap => { 34 | const documents = [] 35 | snap.forEach(d => { 36 | documents.push({ id: d.id, ...d.data() }) 37 | }) 38 | setColumns(documents) 39 | }) 40 | }, [userId, boardId]) 41 | 42 | 43 | useEffect(() => { 44 | if (tasks && columns) { 45 | const finalObject = {} 46 | 47 | const co = columns.find(c => c.id === 'columnOrder') 48 | const cols = columns.filter(c => c.id !== 'columnOrder') 49 | 50 | finalObject.columnOrder = co?.order 51 | finalObject.columns = {} 52 | finalObject.tasks = {} 53 | 54 | tasks.forEach(t => finalObject.tasks[t.id] = t) 55 | cols.forEach(c => finalObject.columns[c.id] = c) 56 | 57 | setFinal(finalObject) 58 | } 59 | }, [tasks, columns]) 60 | 61 | 62 | return { initialData: final, setInitialData: setFinal, boardName } 63 | 64 | } 65 | 66 | export default useKanban -------------------------------------------------------------------------------- /src/components/Task.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { Draggable } from 'react-beautiful-dnd' 3 | import ChecklistProgress from './ChecklistProgress' 4 | import { extractPriority } from '../utils' 5 | 6 | import Modal from './Modal' 7 | import TaskDetails from '../screens/TaskDetails' 8 | import {Description} from './Icons' 9 | import {useState} from 'react' 10 | 11 | 12 | const Task = ({ allData, id, index, boardId, userId, columnDetails, filterBy }) => { 13 | 14 | 15 | const [modal, setModal] = useState(false) 16 | 17 | const theTask = allData.tasks[id] 18 | 19 | let matched = '' 20 | 21 | if (filterBy === null) { 22 | matched = 'all' 23 | } else { 24 | matched = theTask.priority === filterBy 25 | } 26 | 27 | return ( 28 |
29 | 30 | 31 | setModal(false)} boardId={boardId} userId={userId} columnDetails={columnDetails} /> 32 | 33 | 34 | 35 | {(provided, snapshot) => 36 |
setModal(true)} {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef} className={`shadow-lg transition-shadow duration-300 hover:shadow-xl mb-4 rounded px-1.5 py-2.5 ${snapshot.isDragging ? 'bg-gradient-to-r from-red-100 to-blue-100 text-gray-900' : 'bg-white text-gray-800'}`}> 37 |
38 |

{theTask.title}

39 |
40 | {extractPriority(theTask.priority)} 41 | {theTask.todos.length >= 1 && } 42 | {(theTask.description !== null && theTask.description?.length > 1) ? : null } 43 |
44 |
45 |
46 | } 47 |
48 | 49 |
50 | ) 51 | } 52 | 53 | 54 | 55 | export default Task -------------------------------------------------------------------------------- /src/screens/AddTask.jsx: -------------------------------------------------------------------------------- 1 | 2 | import {useState} from 'react' 3 | import {db, firebase} from '../firebase/fbConfig' 4 | import {v4 as uuidv4} from 'uuid'; 5 | 6 | const AddTask = ({boardId, userId, close, allCols}) => 7 | { 8 | const [description, setDescription] = useState(null) 9 | 10 | const addTask = (e) => { 11 | e.preventDefault() 12 | 13 | const uid = uuidv4() 14 | const title = e.target.elements.newTaskTitle.value 15 | const priority = e.target.elements.priority.value 16 | const column = e.target.elements.column.value 17 | 18 | db.collection(`users/${userId}/boards/${boardId}/tasks`) 19 | .doc(uid) 20 | .set({title, priority , description, todos: [], dateAdded: firebase.firestore.FieldValue.serverTimestamp() }) 21 | 22 | db.collection(`users/${userId}/boards/${boardId}/columns`) 23 | .doc(column) 24 | .update({taskIds: firebase.firestore.FieldValue.arrayUnion(uid)}) 25 | 26 | close() 27 | } 28 | 29 | 30 | return ( 31 |
32 |
33 |

Add a New Task

34 | 35 |
36 |
37 | 38 | 39 |
40 | 41 |
42 |
43 | 44 | 49 | 50 |
51 | 52 |
53 | 54 | 57 |
58 |
59 | 60 |
61 | 62 |
63 | 64 |