├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.tsx ├── components │ ├── AddNewTask.tsx │ ├── CreateNewList.tsx │ ├── DeleteListModal.tsx │ ├── DeleteTaskModal.tsx │ ├── EditListModal.tsx │ ├── EditTaskModal.tsx │ ├── Header.tsx │ ├── Lists.tsx │ ├── MainContent.tsx │ ├── Notification.tsx │ ├── SelectList.tsx │ ├── Sidebar.tsx │ └── Tasks.tsx ├── index.tsx ├── react-app-env.d.ts └── store │ ├── actions │ ├── index.ts │ ├── listActions.ts │ └── notificationActions.ts │ ├── reducers │ ├── listReducer.ts │ └── notificationReducer.ts │ ├── store.ts │ └── types.ts └── tsconfig.json /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # task-list-app-react-and-redux-with-typescript 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-list-react-ts", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-free": "^5.13.1", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "@types/jest": "^24.9.1", 11 | "@types/node": "^12.12.47", 12 | "@types/react": "^16.9.41", 13 | "@types/react-dom": "^16.9.8", 14 | "@types/react-redux": "^7.1.9", 15 | "bulma": "^0.9.0", 16 | "react": "^16.13.1", 17 | "react-dom": "^16.13.1", 18 | "react-redux": "^7.2.0", 19 | "react-scripts": "3.4.1", 20 | "redux": "^4.0.5", 21 | "redux-devtools-extension": "^2.13.8", 22 | "typescript": "^3.7.5" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": "react-app" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "homepage": "." 46 | } 47 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hamza-Rashed/task-list-react-typescript/ad38ce6186d37c8172dda794bf154f4258a35fec/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hamza-Rashed/task-list-react-typescript/ad38ce6186d37c8172dda794bf154f4258a35fec/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hamza-Rashed/task-list-react-typescript/ad38ce6186d37c8172dda794bf154f4258a35fec/public/logo512.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .panel-block { 2 | justify-content: space-between; 3 | } 4 | 5 | .panel-block p { 6 | cursor: pointer; 7 | } 8 | 9 | .panel-icon { 10 | margin-right: 0; 11 | cursor: pointer; 12 | } 13 | 14 | .fullwidth { 15 | width: 100%; 16 | } 17 | 18 | .section { 19 | padding: 2rem 0; 20 | } 21 | 22 | .table tr td:nth-child(2), 23 | .table tr td:nth-child(3) { 24 | width: 100px; 25 | } 26 | 27 | tr.completed { 28 | opacity: 0.5; 29 | } 30 | 31 | tr.completed td:first-child { 32 | text-decoration: line-through; 33 | } 34 | 35 | .notification { 36 | position: fixed; 37 | top: 20px; 38 | left: 20px; 39 | max-width: 300px; 40 | } 41 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import './App.css'; 4 | 5 | import Header from './components/Header'; 6 | import Sidebar from './components/Sidebar'; 7 | import Notification from './components/Notification'; 8 | import { RootState } from './store/store'; 9 | import DeleteListModal from './components/DeleteListModal'; 10 | import EditListModal from './components/EditListModal'; 11 | import MainContent from './components/MainContent'; 12 | import EditTaskModal from './components/EditTaskModal'; 13 | import DeleteTaskModal from './components/DeleteTaskModal'; 14 | 15 | const App: FC = () => { 16 | const notificationMsg = useSelector((state: RootState) => state.notification.message); 17 | const listIdToDelete = useSelector((state: RootState) => state.list.listIdToDelete); 18 | const listToEdit = useSelector((state: RootState) => state.list.listToEdit); 19 | const taskToEdit = useSelector((state: RootState) => state.list.taskToEdit); 20 | const taskToDelete = useSelector((state: RootState) => state.list.taskToDelete); 21 | 22 | return ( 23 |
24 |
25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 | {listIdToDelete && } 35 | {listToEdit && } 36 | {taskToEdit && } 37 | {taskToDelete && } 38 |
39 | ); 40 | } 41 | 42 | export default App; 43 | -------------------------------------------------------------------------------- /src/components/AddNewTask.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, FormEvent } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { List, Task } from '../store/types'; 5 | import { addTask, setNotification } from '../store/actions'; 6 | 7 | interface AddNewTaskProps { 8 | list: List; 9 | } 10 | 11 | const AddNewTask: FC = ({ list }) => { 12 | const dispatch = useDispatch(); 13 | const [taskName, setTaskName] = useState(''); 14 | 15 | const changeHandler = (e: FormEvent) => { 16 | setTaskName(e.currentTarget.value); 17 | } 18 | 19 | const submitHandler = (e: FormEvent) => { 20 | e.preventDefault(); 21 | 22 | if(taskName.trim() === '') { 23 | return alert('Task name is required!'); 24 | } 25 | 26 | const newTask: Task = { 27 | name: taskName, 28 | id: `task-${new Date().getTime()}`, 29 | completed: false 30 | } 31 | 32 | dispatch(addTask(newTask, list)); 33 | dispatch(setNotification(`New task created("${newTask.name}")!`)); 34 | setTaskName(''); 35 | } 36 | 37 | return( 38 |
39 |

Add new task to selected field

40 |
41 |
42 | 43 |
44 | 45 |
46 |
47 | 48 |
49 |
50 |
51 |
52 | ); 53 | } 54 | 55 | export default AddNewTask; -------------------------------------------------------------------------------- /src/components/CreateNewList.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, FormEvent } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { List } from '../store/types'; 5 | import { addList, setNotification } from '../store/actions'; 6 | 7 | const CreateNewList: FC = () => { 8 | const dispatch = useDispatch(); 9 | const [listName, setListName] = useState(''); 10 | 11 | const inputChangeHandler = (e: FormEvent) => { 12 | setListName(e.currentTarget.value); 13 | } 14 | 15 | const submitHandler = (e: FormEvent) => { 16 | e.preventDefault(); 17 | 18 | if(listName.trim() === '') { 19 | return alert('List name is required!'); 20 | } 21 | 22 | const newList: List = { 23 | id: `list-${new Date().getTime()}`, 24 | name: listName, 25 | tasks: [] 26 | } 27 | 28 | dispatch(addList(newList)); 29 | dispatch(setNotification(`New list("${newList.name}") created!`)); 30 | setListName(''); 31 | } 32 | 33 | return( 34 |
35 |
36 |

Create New List

37 |
38 |
39 |
40 |
41 | 42 |
43 | 51 |
52 |
53 |
54 | 55 |
56 |
57 |
58 |
59 | ); 60 | } 61 | 62 | export default CreateNewList; -------------------------------------------------------------------------------- /src/components/DeleteListModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import { RootState } from '../store/store'; 5 | import { getListById, deleteList, setNotification, setListIdToDelete } from '../store/actions'; 6 | 7 | interface DeleteListModalProps { 8 | listId: string; 9 | } 10 | 11 | const DeleteListModal: FC = ({ listId }) => { 12 | const dispatch = useDispatch(); 13 | const list = useSelector((state: RootState) => state.list.listById); 14 | 15 | useEffect(() => { 16 | dispatch(getListById(listId)); 17 | }, [dispatch, listId]); 18 | 19 | const deleteListHandler = () => { 20 | dispatch(deleteList(listId)); 21 | if(list) { 22 | dispatch(setNotification(`List "${list.name}" deleted!`, 'danger')); 23 | } 24 | } 25 | 26 | const hideModalHandler = () => { 27 | dispatch(setListIdToDelete('')); 28 | } 29 | 30 | return( 31 |
32 |
33 |
34 |
35 |

Are you sure you want to delete this list ?

36 |
37 |
38 |

All tasks related to this list will be deleted

39 |
40 | {list?.tasks.length === 0 ? 41 |

No tasks in this list!

42 | : 43 |
    44 | {list?.tasks.map(task => ( 45 |
  • {task.name}
  • 46 | ))} 47 |
48 | } 49 |
50 |
51 |
52 | 53 | 54 |
55 |
56 |
57 | ); 58 | } 59 | 60 | export default DeleteListModal; -------------------------------------------------------------------------------- /src/components/DeleteTaskModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { Task, List } from '../store/types'; 5 | import { unsetTaskToDelete, deleteTask, setNotification } from '../store/actions'; 6 | 7 | interface DeleteTaskModalProps { 8 | taskToDelete: { 9 | task: Task; 10 | list: List; 11 | } 12 | } 13 | 14 | const DeleteTaskModal: FC = ({ taskToDelete: { task, list }}) => { 15 | const dispatch = useDispatch(); 16 | 17 | const closeModalHandler = () => { 18 | dispatch(unsetTaskToDelete()); 19 | } 20 | 21 | const deleteHandler = () => { 22 | dispatch(deleteTask(task, list)); 23 | dispatch(setNotification(`Task "${task.name}" deleted!`, 'danger')); 24 | } 25 | 26 | return( 27 |
28 |
29 |
30 |
31 |

Are you sure you want to delete this task ?

32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 | ); 40 | } 41 | 42 | export default DeleteTaskModal; -------------------------------------------------------------------------------- /src/components/EditListModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, FormEvent, useState } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { List } from '../store/types'; 5 | import { setListToEdit, updateList, setNotification } from '../store/actions'; 6 | 7 | interface EditListModalProps { 8 | list: List; 9 | } 10 | 11 | const EditListModal: FC = ({ list }) => { 12 | const dispatch = useDispatch(); 13 | const [listName, setListName] = useState(list.name); 14 | 15 | const submitHandler = (e: FormEvent) => { 16 | e.preventDefault(); 17 | 18 | if(listName.trim() === '') { 19 | return alert('List name is required!'); 20 | } 21 | 22 | if(listName.trim() === list.name) { 23 | return alert('List name is the same as before!'); 24 | } 25 | 26 | dispatch(updateList(list.id, listName.trim())); 27 | dispatch(setNotification(`List "${list.name}" updated!`)); 28 | } 29 | 30 | const changeHandler = (e: FormEvent) => { 31 | setListName(e.currentTarget.value); 32 | } 33 | 34 | const hideModalHandler = () => { 35 | dispatch(setListToEdit('')); 36 | } 37 | 38 | return( 39 |
40 |
41 |
42 |
43 |

Edit List

44 | 45 |
46 |
47 |
48 | 49 |
50 | 58 |
59 |
60 |
61 |
62 | 63 | 64 |
65 |
66 |
67 | ); 68 | } 69 | 70 | export default EditListModal; -------------------------------------------------------------------------------- /src/components/EditTaskModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, FormEvent, useState } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { Task, List } from '../store/types'; 5 | import { updateTask, unsetTaskToEdit, setNotification } from '../store/actions'; 6 | 7 | interface EditTaskModalProps { 8 | taskToEdit: { 9 | task: Task; 10 | list: List; 11 | } 12 | } 13 | 14 | const EditTaskModal: FC = ({ taskToEdit: { task, list }}) => { 15 | const dispatch = useDispatch(); 16 | const [taskName, setTaskName] = useState(task.name); 17 | const [taskState, setTaskState] = useState(task.completed); 18 | 19 | const closeModalHandler = () => { 20 | dispatch(unsetTaskToEdit()); 21 | } 22 | 23 | const submitHandler = (e: FormEvent) => { 24 | e.preventDefault(); 25 | 26 | if(taskName.trim() === '') { 27 | return alert('Task name is required!'); 28 | } 29 | 30 | if(taskName === task.name && taskState === task.completed) { 31 | return alert('Task name and state are the same as before!'); 32 | } 33 | 34 | dispatch(updateTask(task.id, taskName, taskState, list)); 35 | dispatch(setNotification(`Task "${task.name}" updated!`)); 36 | } 37 | 38 | const nameChangeHandler = (e: FormEvent) => { 39 | setTaskName(e.currentTarget.value); 40 | } 41 | 42 | const stateChangeHandler = (e: FormEvent) => { 43 | setTaskState(e.currentTarget.checked); 44 | } 45 | 46 | return( 47 |
48 |
49 |
50 |
51 |

Edit Task

52 | 53 |
54 |
55 |
56 | 57 |
58 | 59 |
60 |
61 |
62 | 63 | 67 |
68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 | ); 76 | } 77 | 78 | export default EditTaskModal; -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | interface HeaderProps { 4 | title: string; 5 | subtitle?: string; 6 | } 7 | 8 | const Header: FC = ({ title, subtitle }) => { 9 | return ( 10 |
11 |
12 |
13 |

{title}

14 |

{subtitle}

15 |
16 |
17 |
18 | ); 19 | } 20 | 21 | export default Header; -------------------------------------------------------------------------------- /src/components/Lists.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import { RootState } from '../store/store'; 5 | import { getLists, setListToEdit, setListIdToDelete } from '../store/actions'; 6 | import { List } from '../store/types'; 7 | 8 | const Lists: FC = () => { 9 | const dispatch = useDispatch(); 10 | const lists = useSelector((state: RootState) => state.list.lists); 11 | 12 | useEffect(() => { 13 | dispatch(getLists()); 14 | }, [dispatch]); 15 | 16 | const setListToEditHandler = (id: string) => { 17 | dispatch(setListToEdit(id)); 18 | } 19 | 20 | const setListIdToDeleteHandler = (id: string) => { 21 | dispatch(setListIdToDelete(id)); 22 | } 23 | 24 | return( 25 |
26 |

Your lists

27 |
28 | { Object.keys(lists).length === 0 29 | ? 30 |

No Lists

31 | : 32 |
33 | {Object.values(lists).map((list: List) => { 34 | return
35 |

setListToEditHandler(list.id)}>{list.name}

36 | setListIdToDeleteHandler(list.id)}> 37 | 38 | 39 |
40 | })} 41 |
42 | } 43 |
44 |
45 | ); 46 | } 47 | 48 | export default Lists; -------------------------------------------------------------------------------- /src/components/MainContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, Fragment } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | import SelectList from './SelectList'; 5 | import { RootState } from '../store/store'; 6 | import AddNewTask from './AddNewTask'; 7 | import Tasks from './Tasks'; 8 | 9 | const MainContent: FC = () => { 10 | const selectedList = useSelector((state: RootState) => state.list.selectedList); 11 | 12 | return( 13 |
14 |
15 | 16 | { 17 | selectedList && 18 | 19 | 20 |
21 | 22 |
23 | } 24 |
25 |
26 | ); 27 | } 28 | 29 | export default MainContent; -------------------------------------------------------------------------------- /src/components/Notification.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import { setNotification } from '../store/actions'; 5 | import { RootState } from '../store/store'; 6 | 7 | interface NotificationProps { 8 | msg: string; 9 | } 10 | 11 | let timeout: ReturnType; 12 | 13 | const Notification: FC = ({ msg }) => { 14 | const dispatch = useDispatch(); 15 | const type = useSelector((state: RootState) => state.notification.type); 16 | 17 | useEffect(() => { 18 | if(msg !== '') { 19 | if(timeout) { 20 | clearTimeout(timeout); 21 | } 22 | timeout = setTimeout(() => { 23 | dispatch(setNotification('')); 24 | }, 3000); 25 | } 26 | }, [dispatch, msg]); 27 | 28 | const closeNotification = () => { 29 | dispatch(setNotification('')); 30 | clearTimeout(timeout); 31 | } 32 | 33 | return( 34 |
35 | 36 |

{msg}

37 |
38 | ); 39 | } 40 | 41 | export default Notification; -------------------------------------------------------------------------------- /src/components/SelectList.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, FormEvent } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import { List } from '../store/types'; 5 | import { setSelectedList } from '../store/actions'; 6 | import { RootState } from '../store/store'; 7 | 8 | const SelectList: FC = () => { 9 | const dispatch = useDispatch(); 10 | const lists = useSelector((state: RootState) => state.list.lists); 11 | 12 | const selectChangeHandler = (e: FormEvent) => { 13 | dispatch(setSelectedList(e.currentTarget.value)); 14 | } 15 | 16 | return( 17 |
18 |

Choose a list

19 |
20 |
21 |
22 | 30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 | ); 38 | } 39 | 40 | export default SelectList; -------------------------------------------------------------------------------- /src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import CreateNewList from './CreateNewList'; 4 | import Lists from './Lists'; 5 | 6 | const Sidebar: FC = () => { 7 | return( 8 |
9 | 10 | 11 |
12 | ); 13 | } 14 | 15 | export default Sidebar; -------------------------------------------------------------------------------- /src/components/Tasks.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import { Task } from '../store/types'; 5 | import { setTaskToDelete, setTaskToEdit } from '../store/actions'; 6 | import { RootState } from '../store/store'; 7 | 8 | interface TasksProps { 9 | tasks: Task[]; 10 | } 11 | 12 | const Tasks: FC = ({ tasks }) => { 13 | const dispatch = useDispatch(); 14 | const list = useSelector((state: RootState) => state.list.selectedList!); 15 | 16 | const setTaskToEditHandler = (task: Task) => { 17 | dispatch(setTaskToEdit(task, list)); 18 | } 19 | 20 | const setTaskToDeleteHandler = (task: Task) => { 21 | dispatch(setTaskToDelete(task, list)); 22 | } 23 | 24 | const tasksTable = ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | { 35 | tasks.map((task: Task) => ( 36 | 37 | 38 | 45 | 52 | 53 | )) 54 | } 55 | 56 |
TaskEditDelete
{task.name} 39 | 44 | 46 | 51 |
57 | ); 58 | 59 | return( 60 |
61 |

List of tasks in selected list

62 | {tasks.length === 0 ?

No Tasks

: tasksTable} 63 |
64 | ); 65 | } 66 | 67 | export default Tasks; -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import '../node_modules/bulma/css/bulma.min.css'; 6 | import '../node_modules/@fortawesome/fontawesome-free/css/all.min.css'; 7 | 8 | import App from './App'; 9 | import store from './store/store'; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById('root') 18 | ); 19 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/store/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './listActions'; 2 | export * from './notificationActions'; -------------------------------------------------------------------------------- /src/store/actions/listActions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | List, 3 | ListsAction, 4 | ADD_LIST, 5 | GET_LISTS, 6 | GET_LIST_BY_ID, 7 | SET_LISTID_TO_DELETE, 8 | SET_LIST_TO_EDIT, 9 | SET_SELECTED_LIST, 10 | DELETE_LIST, 11 | UPDATE_LIST, 12 | Task, 13 | ADD_TASK, 14 | SET_TASK_TO_DELETE, 15 | UNSET_TASK_TO_DELETE, 16 | DELETE_TASK, 17 | SET_TASK_TO_EDIT, 18 | UNSET_TASK_TO_EDIT, 19 | UPDATE_TASK, 20 | } from "../types"; 21 | 22 | export const addList = (list: List): ListsAction => { 23 | return { 24 | type: ADD_LIST, 25 | payload: list, 26 | }; 27 | }; 28 | 29 | export const getLists = (): ListsAction => { 30 | return { 31 | type: GET_LISTS, 32 | }; 33 | }; 34 | 35 | export const getListById = (id: string): ListsAction => { 36 | return { 37 | type: GET_LIST_BY_ID, 38 | payload: id, 39 | }; 40 | }; 41 | 42 | export const setListIdToDelete = (id: string): ListsAction => { 43 | return { 44 | type: SET_LISTID_TO_DELETE, 45 | payload: id, 46 | }; 47 | }; 48 | 49 | export const setListToEdit = (id: string): ListsAction => { 50 | return { 51 | type: SET_LIST_TO_EDIT, 52 | payload: id, 53 | }; 54 | }; 55 | 56 | export const setSelectedList = (id: string): ListsAction => { 57 | return { 58 | type: SET_SELECTED_LIST, 59 | payload: id, 60 | }; 61 | }; 62 | 63 | export const deleteList = (id: string): ListsAction => { 64 | return { 65 | type: DELETE_LIST, 66 | payload: id, 67 | }; 68 | }; 69 | 70 | export const updateList = (id: string, name: string): ListsAction => { 71 | return { 72 | type: UPDATE_LIST, 73 | payload: { 74 | id, 75 | name, 76 | }, 77 | }; 78 | }; 79 | 80 | export const addTask = (task: Task, list: List): ListsAction => { 81 | return { 82 | type: ADD_TASK, 83 | payload: { 84 | task, 85 | list, 86 | }, 87 | }; 88 | }; 89 | 90 | export const setTaskToDelete = (task: Task, list: List): ListsAction => { 91 | return { 92 | type: SET_TASK_TO_DELETE, 93 | payload: { 94 | task, 95 | list, 96 | }, 97 | }; 98 | }; 99 | 100 | export const unsetTaskToDelete = (): ListsAction => { 101 | return { 102 | type: UNSET_TASK_TO_DELETE, 103 | }; 104 | }; 105 | 106 | export const deleteTask = (task: Task, list: List): ListsAction => { 107 | return { 108 | type: DELETE_TASK, 109 | payload: { 110 | task, 111 | list, 112 | }, 113 | }; 114 | }; 115 | 116 | export const setTaskToEdit = (task: Task, list: List): ListsAction => { 117 | return { 118 | type: SET_TASK_TO_EDIT, 119 | payload: { 120 | task, 121 | list, 122 | }, 123 | }; 124 | }; 125 | 126 | export const unsetTaskToEdit = (): ListsAction => { 127 | return { 128 | type: UNSET_TASK_TO_EDIT, 129 | }; 130 | }; 131 | 132 | export const updateTask = ( 133 | taskId: string, 134 | taskName: string, 135 | taskState: boolean, 136 | list: List 137 | ): ListsAction => { 138 | return { 139 | type: UPDATE_TASK, 140 | payload: { 141 | taskId, 142 | taskName, 143 | taskState, 144 | list, 145 | }, 146 | }; 147 | }; 148 | -------------------------------------------------------------------------------- /src/store/actions/notificationActions.ts: -------------------------------------------------------------------------------- 1 | import { NotificationAction, SET_NOTIFICATION } from "../types"; 2 | 3 | export const setNotification = (msg: string, type: string = 'success'): NotificationAction => { 4 | return { 5 | type: SET_NOTIFICATION, 6 | payload: { 7 | msg, 8 | type 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/store/reducers/listReducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ListsAction, 3 | ListState, 4 | Lists, 5 | ADD_LIST, 6 | GET_LISTS, 7 | GET_LIST_BY_ID, 8 | SET_LISTID_TO_DELETE, 9 | SET_LIST_TO_EDIT, 10 | DELETE_LIST, 11 | UPDATE_LIST, 12 | SET_SELECTED_LIST, 13 | ADD_TASK, 14 | SET_TASK_TO_DELETE, 15 | UNSET_TASK_TO_DELETE, 16 | DELETE_TASK, 17 | SET_TASK_TO_EDIT, 18 | UNSET_TASK_TO_EDIT, 19 | UPDATE_TASK, 20 | } from "../types"; 21 | 22 | const initialState: ListState = { 23 | lists: {}, 24 | listIdToDelete: "", 25 | listToEdit: null, 26 | listById: null, 27 | selectedList: null, 28 | taskToDelete: null, 29 | taskToEdit: null, 30 | }; 31 | 32 | // Helper functions 33 | const getListsFromLS = (): Lists => { 34 | if (localStorage.getItem("task_list")) { 35 | return JSON.parse(localStorage.getItem("task_list") || "{}"); 36 | } 37 | 38 | return {}; 39 | }; 40 | 41 | const saveListsToLS = (lists: Lists) => { 42 | localStorage.setItem("task_list", JSON.stringify(lists)); 43 | }; 44 | 45 | export default (state = initialState, action: ListsAction): ListState => { 46 | const listsFromLS = getListsFromLS(); 47 | 48 | switch (action.type) { 49 | case ADD_LIST: 50 | const clonedListsFromLS = { ...listsFromLS }; 51 | clonedListsFromLS[action.payload.id] = action.payload; 52 | saveListsToLS(clonedListsFromLS); 53 | return { 54 | ...state, 55 | lists: clonedListsFromLS, 56 | }; 57 | 58 | case GET_LISTS: 59 | return { 60 | ...state, 61 | lists: listsFromLS, 62 | }; 63 | 64 | case GET_LIST_BY_ID: 65 | const list = listsFromLS[action.payload]; 66 | return { 67 | ...state, 68 | listById: list, 69 | }; 70 | 71 | case SET_LISTID_TO_DELETE: 72 | return { 73 | ...state, 74 | listIdToDelete: action.payload, 75 | }; 76 | 77 | case SET_LIST_TO_EDIT: 78 | const listToEdit = listsFromLS[action.payload]; 79 | return { 80 | ...state, 81 | listToEdit, 82 | }; 83 | 84 | case DELETE_LIST: 85 | const clonedListsFromLS2 = { ...listsFromLS }; 86 | const listId = clonedListsFromLS2[action.payload].id; 87 | delete clonedListsFromLS2[action.payload]; 88 | saveListsToLS(clonedListsFromLS2); 89 | return { 90 | ...state, 91 | lists: clonedListsFromLS2, 92 | listIdToDelete: "", 93 | listById: null, 94 | selectedList: 95 | state.selectedList && listId === state.selectedList.id 96 | ? null 97 | : state.selectedList, 98 | }; 99 | 100 | case UPDATE_LIST: 101 | const clonedListsFromLS3 = { ...listsFromLS }; 102 | clonedListsFromLS3[action.payload.id].name = action.payload.name; 103 | saveListsToLS(clonedListsFromLS3); 104 | return { 105 | ...state, 106 | lists: clonedListsFromLS3, 107 | listToEdit: null, 108 | }; 109 | 110 | case SET_SELECTED_LIST: 111 | const selectedList = getListsFromLS()[action.payload]; 112 | return { 113 | ...state, 114 | selectedList: selectedList, 115 | }; 116 | 117 | case ADD_TASK: 118 | const clonedListsFromLS4 = { ...listsFromLS }; 119 | clonedListsFromLS4[action.payload.list.id].tasks.push( 120 | action.payload.task 121 | ); 122 | saveListsToLS(clonedListsFromLS4); 123 | return { 124 | ...state, 125 | lists: clonedListsFromLS4, 126 | selectedList: clonedListsFromLS4[action.payload.list.id], 127 | }; 128 | 129 | case SET_TASK_TO_DELETE: 130 | return { 131 | ...state, 132 | taskToDelete: { 133 | task: action.payload.task, 134 | list: action.payload.list, 135 | }, 136 | }; 137 | 138 | case UNSET_TASK_TO_DELETE: 139 | return { 140 | ...state, 141 | taskToDelete: null, 142 | }; 143 | 144 | case DELETE_TASK: 145 | const clonedListsFromLS5 = { ...listsFromLS }; 146 | const clonedTasks = [ 147 | ...clonedListsFromLS5[state.taskToDelete!.list.id].tasks, 148 | ]; 149 | const task = clonedTasks.find( 150 | (task) => task.id === state.taskToDelete!.task.id 151 | ); 152 | clonedTasks.splice(clonedTasks.indexOf(task!), 1); 153 | clonedListsFromLS5[state.taskToDelete!.list.id].tasks = clonedTasks; 154 | saveListsToLS(clonedListsFromLS5); 155 | return { 156 | ...state, 157 | lists: clonedListsFromLS5, 158 | selectedList: clonedListsFromLS5[state.taskToDelete!.list.id], 159 | taskToDelete: null, 160 | }; 161 | 162 | case SET_TASK_TO_EDIT: 163 | return { 164 | ...state, 165 | taskToEdit: { 166 | task: action.payload.task, 167 | list: action.payload.list, 168 | }, 169 | }; 170 | 171 | case UNSET_TASK_TO_EDIT: 172 | return { 173 | ...state, 174 | taskToEdit: null, 175 | }; 176 | 177 | case UPDATE_TASK: 178 | const clonedListsFromLS6 = { ...listsFromLS }; 179 | const clonedList = { ...clonedListsFromLS6[action.payload.list.id] }; 180 | const clonedTasks2 = [...clonedList.tasks]; 181 | const task2 = clonedTasks2.find( 182 | (task) => task.id === action.payload.taskId 183 | ); 184 | const clonedTask = { ...task2! }; 185 | clonedTask.name = action.payload.taskName; 186 | clonedTask.completed = action.payload.taskState; 187 | const updatedTasks = clonedTasks2.map((task) => 188 | task.id === clonedTask.id ? clonedTask : task 189 | ); 190 | clonedList.tasks = updatedTasks; 191 | clonedListsFromLS6[clonedList.id] = clonedList; 192 | saveListsToLS(clonedListsFromLS6); 193 | 194 | return { 195 | ...state, 196 | lists: clonedListsFromLS6, 197 | selectedList: clonedList, 198 | taskToEdit: null, 199 | }; 200 | 201 | default: 202 | return state; 203 | } 204 | }; 205 | -------------------------------------------------------------------------------- /src/store/reducers/notificationReducer.ts: -------------------------------------------------------------------------------- 1 | import { NotificationState, NotificationAction, SET_NOTIFICATION } from "../types"; 2 | 3 | const initialState: NotificationState = { 4 | message: '', 5 | type: 'success' 6 | } 7 | 8 | export default (state = initialState, action: NotificationAction): NotificationState => { 9 | switch(action.type) { 10 | case SET_NOTIFICATION: 11 | return { 12 | message: action.payload.msg, 13 | type: action.payload.type 14 | } 15 | 16 | default: 17 | return state; 18 | } 19 | } -------------------------------------------------------------------------------- /src/store/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | 4 | import listReducer from './reducers/listReducer'; 5 | import notificationReducer from './reducers/notificationReducer'; 6 | 7 | const rootReducer = combineReducers({ 8 | list: listReducer, 9 | notification: notificationReducer 10 | }); 11 | 12 | const store = createStore(rootReducer, composeWithDevTools()); 13 | 14 | export type RootState = ReturnType; 15 | 16 | export default store; -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | export const GET_LISTS = "GET_LISTS"; 2 | export const GET_LIST_BY_ID = "GET_LIST_BY_ID"; 3 | export const ADD_LIST = "ADD_LIST"; 4 | export const DELETE_LIST = "DELETE_LIST"; 5 | export const UPDATE_LIST = "UPDATE_LIST"; 6 | export const SET_LISTID_TO_DELETE = "SET_LISTID_TO_DELETE"; 7 | export const SET_LIST_TO_EDIT = "SET_LIST_TO_EDIT"; 8 | export const SET_SELECTED_LIST = "SET_SELECTED_LIST"; 9 | 10 | export const ADD_TASK = "ADD_TASK"; 11 | export const DELETE_TASK = "DELETE_TASK"; 12 | export const SET_TASK_TO_DELETE = "SET_TASK_TO_DELETE"; 13 | export const UNSET_TASK_TO_DELETE = "UNSET_TASK_TO_DELETE"; 14 | export const UPDATE_TASK = "UPDATE_TASK"; 15 | export const SET_TASK_TO_EDIT = "SET_TASK_TO_EDIT"; 16 | export const UNSET_TASK_TO_EDIT = "UNSET_TASK_TO_EDIT"; 17 | 18 | export const SET_NOTIFICATION = "SET_NOTIFICATION"; 19 | 20 | export interface Task { 21 | name: string; 22 | id: string; 23 | completed: boolean; 24 | } 25 | 26 | export interface List { 27 | name: string; 28 | id: string; 29 | tasks: Task[]; 30 | } 31 | 32 | export interface Lists { 33 | [id: string]: List; 34 | } 35 | 36 | // Actions 37 | interface AddListAction { 38 | type: typeof ADD_LIST; 39 | payload: List; 40 | } 41 | 42 | interface GetListsAction { 43 | type: typeof GET_LISTS; 44 | } 45 | 46 | interface GetListByIdAction { 47 | type: typeof GET_LIST_BY_ID; 48 | payload: string; 49 | } 50 | 51 | interface SetListIdToDeleteAction { 52 | type: typeof SET_LISTID_TO_DELETE; 53 | payload: string; 54 | } 55 | 56 | interface SetListToEditAction { 57 | type: typeof SET_LIST_TO_EDIT; 58 | payload: string; 59 | } 60 | 61 | interface DeleteListAction { 62 | type: typeof DELETE_LIST; 63 | payload: string; 64 | } 65 | 66 | interface UpdateListAction { 67 | type: typeof UPDATE_LIST; 68 | payload: { 69 | id: string; 70 | name: string; 71 | }; 72 | } 73 | 74 | interface SetSelectedListAction { 75 | type: typeof SET_SELECTED_LIST; 76 | payload: string; 77 | } 78 | 79 | interface AddTaskAction { 80 | type: typeof ADD_TASK; 81 | payload: { 82 | task: Task; 83 | list: List; 84 | }; 85 | } 86 | 87 | interface DeleteTaskAction { 88 | type: typeof DELETE_TASK; 89 | payload: { 90 | task: Task; 91 | list: List; 92 | }; 93 | } 94 | 95 | interface SetTaskToDeleteAction { 96 | type: typeof SET_TASK_TO_DELETE; 97 | payload: { 98 | task: Task; 99 | list: List; 100 | }; 101 | } 102 | 103 | interface UnsetTaskToDeleteAction { 104 | type: typeof UNSET_TASK_TO_DELETE; 105 | } 106 | 107 | interface EditTaskAction { 108 | type: typeof UPDATE_TASK; 109 | payload: { 110 | taskId: string; 111 | taskName: string; 112 | taskState: boolean; 113 | list: List; 114 | }; 115 | } 116 | 117 | interface SetTaskToEditAction { 118 | type: typeof SET_TASK_TO_EDIT; 119 | payload: { 120 | task: Task; 121 | list: List; 122 | }; 123 | } 124 | 125 | interface UnsetTaskToEditAction { 126 | type: typeof UNSET_TASK_TO_EDIT; 127 | } 128 | 129 | interface SetNotificationAction { 130 | type: typeof SET_NOTIFICATION; 131 | payload: { 132 | msg: string; 133 | type: string; 134 | }; 135 | } 136 | 137 | export type ListsAction = 138 | | AddListAction 139 | | GetListsAction 140 | | GetListByIdAction 141 | | SetListIdToDeleteAction 142 | | SetListToEditAction 143 | | DeleteListAction 144 | | UpdateListAction 145 | | SetSelectedListAction 146 | | AddTaskAction 147 | | DeleteTaskAction 148 | | SetTaskToDeleteAction 149 | | UnsetTaskToDeleteAction 150 | | EditTaskAction 151 | | SetTaskToEditAction 152 | | UnsetTaskToEditAction; 153 | 154 | export type NotificationAction = SetNotificationAction; 155 | 156 | export interface ListState { 157 | lists: Lists; 158 | listIdToDelete: string; 159 | listToEdit: List | null; 160 | listById: List | null; 161 | selectedList: List | null; 162 | taskToDelete: { 163 | task: Task; 164 | list: List; 165 | } | null; 166 | taskToEdit: { 167 | task: Task; 168 | list: List; 169 | } | null; 170 | } 171 | 172 | export interface NotificationState { 173 | message: string; 174 | type: string; 175 | } 176 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------