├── .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 |
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 |
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 |
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 |
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 |
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 |
33 |
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 |
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 |
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 |
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 |
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 | Task |
29 | Edit |
30 | Delete |
31 |
32 |
33 |
34 | {
35 | tasks.map((task: Task) => (
36 |
37 | {task.name} |
38 |
39 |
44 | |
45 |
46 |
51 | |
52 |
53 | ))
54 | }
55 |
56 |
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 |
--------------------------------------------------------------------------------