From 0cc9fe94e5f1d09f7b1b8c8c73671e0cf891565b Mon Sep 17 00:00:00 2001 From: Olha Momot Date: Tue, 26 Sep 2023 00:32:40 +0300 Subject: [PATCH 1/5] implemented todo update feature --- src/App.tsx | 138 ++++++++++++++++++++++++++---- src/api/todos.ts | 18 ++++ src/components/Footer.tsx | 70 +++++++++++++++ src/components/Form.tsx | 77 +++++++++++++++++ src/components/TodoItem.tsx | 147 ++++++++++++++++++++++++++++++++ src/components/TodoList.tsx | 25 ++++++ src/components/TodoProvider.tsx | 117 +++++++++++++++++++++++++ src/index.tsx | 7 +- src/styles/todo.scss | 63 ++------------ src/styles/todoapp.scss | 5 -- src/types/Todo.ts | 6 ++ src/utils/constants.tsx | 1 + src/utils/fetchClient.ts | 46 ++++++++++ 13 files changed, 643 insertions(+), 77 deletions(-) create mode 100644 src/api/todos.ts create mode 100644 src/components/Footer.tsx create mode 100644 src/components/Form.tsx create mode 100644 src/components/TodoItem.tsx create mode 100644 src/components/TodoList.tsx create mode 100644 src/components/TodoProvider.tsx create mode 100644 src/types/Todo.ts create mode 100644 src/utils/constants.tsx create mode 100644 src/utils/fetchClient.ts diff --git a/src/App.tsx b/src/App.tsx index 5749bdf784..f478ad0356 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,130 @@ -/* eslint-disable max-len */ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; -import { UserWarning } from './UserWarning'; +import React, { + useContext, useEffect, useMemo, useState, +} from 'react'; +import classNames from 'classnames'; -const USER_ID = 0; +import { TodoList } from './components/TodoList'; +import { TodoContext } from './components/TodoProvider'; +import { Form } from './components/Form'; +import { Todo } from './types/Todo'; +import { Footer } from './components/Footer'; + +enum FilterOption { + All = 'All', + Active = 'Active', + Completed = 'Completed', +} export const App: React.FC = () => { - if (!USER_ID) { - return ; - } + const [filter, setFilter] = useState(FilterOption.All); + const [tempTodo, setTempTodo] = useState(null); + + const { + todos, + setErrorMessage, + errorMessage, + updateTodoHandler, + } = useContext(TodoContext); + + const filteredTodos = useMemo(() => { + return todos.filter(({ completed }) => { + switch (filter) { + case FilterOption.Active: + return !completed; + case FilterOption.Completed: + return completed; + case FilterOption.All: + default: + return true; + } + }); + }, [filter, todos]); + + const activeTodos = useMemo(() => { + return todos.filter(({ completed }) => !completed); + }, [todos]); + + useEffect(() => { + let timer: NodeJS.Timeout | undefined; + + if (errorMessage) { + timer = setTimeout(() => { + setErrorMessage(''); + }, 3000); + } + + return () => { + if (timer) { + clearTimeout(timer); + } + }; + }, [errorMessage]); + + const onToggleAll = async () => { + if (activeTodos.length) { + activeTodos.forEach( + currentTodo => updateTodoHandler(currentTodo, { completed: true }), + ); + } else { + todos.forEach( + currentTodo => updateTodoHandler(currentTodo, { completed: false }), + ); + } + }; return ( -
-

- Copy all you need from the prev task: -
- React Todo App - Add and Delete -

- -

Styles are already copied

-
+
+

todos

+ +
+
+ {!!todos.length + && ( +
+ + {!!filteredTodos.length + && } + + {!!todos.length + && ( +
+ )} +
+ +
+
+
); }; diff --git a/src/api/todos.ts b/src/api/todos.ts new file mode 100644 index 0000000000..b0a0712cd2 --- /dev/null +++ b/src/api/todos.ts @@ -0,0 +1,18 @@ +import { Todo } from '../types/Todo'; +import { client } from '../utils/fetchClient'; + +export const getTodos = (userId: number) => { + return client.get(`/todos?userId=${userId}`); +}; + +export const addTodo = (newTodo: Omit) => { + return client.post('/todos', newTodo); +}; + +export const deleteTodo = (todoId: number) => { + return client.delete(`/todos/${todoId}`); +}; + +export const updateTodo = (todoId: number, newTodo: Omit) => { + return client.patch(`/todos/${todoId}`, newTodo); +}; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000000..f7eeb22a60 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,70 @@ +import classNames from 'classnames'; +import React, { useContext, useMemo } from 'react'; + +import { TodoContext } from './TodoProvider'; +import { Todo } from '../types/Todo'; + +type Props = { + activeTodos: Todo[], + filter: string, + FilterOption: { [key: string]: string }, + setFilter: (filter: string) => void, +}; + +export const Footer: React.FC = ({ + activeTodos, + filter, + FilterOption, + setFilter, +}) => { + const { todos, deleteTodoHandler } = useContext(TodoContext); + + const completedTodos = useMemo(() => { + return todos.filter(({ completed }) => completed === true); + }, [todos]); + + const handleDeleteCompletedTodos = () => { + completedTodos.forEach(({ id }) => { + deleteTodoHandler(id); + }); + }; + + const isClearButtonInvisible = completedTodos.length === 0; + + return ( + + ); +}; diff --git a/src/components/Form.tsx b/src/components/Form.tsx new file mode 100644 index 0000000000..cc8356f5a8 --- /dev/null +++ b/src/components/Form.tsx @@ -0,0 +1,77 @@ +import React, { + useContext, useEffect, useRef, useState, +} from 'react'; +import { TodoContext } from './TodoProvider'; +import { Todo } from '../types/Todo'; +import { USER_ID } from '../utils/constants'; + +type Props = { + setTempTodo: (todo: Todo | null) => void; +}; + +export const Form: React.FC = ({ setTempTodo }) => { + const [title, setTitle] = useState(''); + const [titleError, setTitleError] = useState(false); + const [isSubmiting, setIsSubmiting] = useState(false); + const todoTitleField = useRef(null); + const { + setErrorMessage, + addTodoHandler, + } = useContext(TodoContext); + + useEffect(() => { + if (todoTitleField.current) { + todoTitleField.current.focus(); + } + }); + + const handleTitleChange = (event: React.ChangeEvent) => { + setTitle(event?.target.value); + + if (titleError) { + setTitleError(false); + } + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + setIsSubmiting(true); + const newTitle = title.trim(); + + if (!newTitle) { + setErrorMessage('Title should not be empty'); + setIsSubmiting(false); + + return; + } + + const newTodo = { + title: newTitle, + userId: USER_ID, + completed: false, + }; + + setTempTodo({ id: 0, ...newTodo }); + + addTodoHandler(newTodo, () => { + setTitle(''); + setTempTodo(null); + }); + setIsSubmiting(false); + }; + + return ( + + + + ); +}; diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx new file mode 100644 index 0000000000..422e3e59d1 --- /dev/null +++ b/src/components/TodoItem.tsx @@ -0,0 +1,147 @@ +import classNames from 'classnames'; +import React, { + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { Todo } from '../types/Todo'; +import { TodoContext } from './TodoProvider'; + +type Props = { + todo: Todo, +}; + +export const TodoItem: React.FC = ( + { todo }: Props, +) => { + const [isItemLoading, setIsItemLoading] = useState(todo.id === 0); + const [isEditing, setIsEditing] = useState(false); + const [todoTitle, setTodoTitle] = useState(todo.title); + const todoTitleField = useRef(null); + const { + isLoadingMap, + deleteTodoHandler, + updateTodoHandler, + } = useContext(TodoContext); + + const handleCheckboxChange = async () => { + setIsItemLoading(true); + await updateTodoHandler(todo, { completed: !todo.completed }); + setIsItemLoading(false); + }; + + const handleTodoTitleChange = ( + event: React.ChangeEvent, + ) => { + setTodoTitle(event.target.value); + }; + + const handleTodoDoubleClick = () => { + setIsEditing(true); + }; + + const handleTodoSave = async (event: React.ChangeEvent) => { + event.preventDefault(); + setIsItemLoading(true); + + if (todoTitle !== todo.title) { + await updateTodoHandler(todo, { title: todoTitle }); + } else if (todoTitle === todo.title) { + setIsEditing(false); + + return; + } else { + await deleteTodoHandler(todo.id); + } + + setIsEditing(false); + setIsItemLoading(false); + }; + + const onTodoDelete = async () => { + setIsItemLoading(true); + await deleteTodoHandler(todo.id); + setIsItemLoading(false); + }; + + useEffect(() => { + if (isEditing && todoTitleField.current) { + todoTitleField.current.focus(); + } + }, [isEditing]); + + document.addEventListener('keyup', (e) => { + if (e.key === 'Escape') { + setIsEditing(false); + } + }); + + return ( +
+ + + {isEditing + ? ( +
+ +
+ ) : ( + <> + + {todo.title} + + + + )} + +
+
+
+
+
+ ); +}; diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx new file mode 100644 index 0000000000..d9f279c289 --- /dev/null +++ b/src/components/TodoList.tsx @@ -0,0 +1,25 @@ +import { TodoItem } from './TodoItem'; +import { Todo } from '../types/Todo'; + +type Props = { + todos: Todo[]; + tempTodo: Todo | null; +}; + +export const TodoList: React.FC = ({ todos, tempTodo }) => { + return ( +
+ {todos.map(todo => { + return ( + + ); + })} + + {tempTodo + && } +
+ ); +}; diff --git a/src/components/TodoProvider.tsx b/src/components/TodoProvider.tsx new file mode 100644 index 0000000000..6f63fda756 --- /dev/null +++ b/src/components/TodoProvider.tsx @@ -0,0 +1,117 @@ +import React, { createContext, useEffect, useState } from 'react'; + +import { Todo } from '../types/Todo'; +import { + addTodo, + deleteTodo, + getTodos, + updateTodo, +} from '../api/todos'; +import { USER_ID } from '../utils/constants'; + +interface TodoContextProps { + todos: Todo[]; + addTodoHandler: (todo: Omit, onSuccess: () => void) => void; + deleteTodoHandler: (todoId: number) => Promise; + updateTodoHandler: (todo: Todo, property: Partial) => Promise; + errorMessage: string; + setErrorMessage: (str: string) => void; + isLoadingMap: {}; +} + +export const TodoContext = createContext({ + todos: [], + addTodoHandler: () => { }, + deleteTodoHandler: async () => { }, + updateTodoHandler: async () => { }, + errorMessage: '', + setErrorMessage: () => { }, + isLoadingMap: {}, +}); + +type TodoProviderProps = { + children: React.ReactNode; +}; + +export const TodoProvider: React.FC = ({ children }) => { + const [todos, setTodos] = useState([]); + const [errorMessage, setErrorMessage] = useState(''); + const [isLoadingMap, setIsLoadingMap] + = useState<{ [key: number]: boolean } | {}>({}); + + const addTodoHandler = async ( + newTodo: Omit, onSuccess: () => void, + ) => { + try { + const createdTodo = await addTodo(newTodo); + + setTodos((currentTodos) => [...currentTodos, createdTodo]); + onSuccess(); + } catch (error) { + setErrorMessage('Unable to add todo'); + } + }; + + const updateTodoHandler = async ( + todo: Todo, + propertiesToUpdate: Partial, + ) => { + try { + const updatedTodo = await updateTodo(todo.id, { + ...todo, + ...propertiesToUpdate, + }); + + setTodos((currentTodos: Todo[]) => { + return currentTodos.map((currentTodo) => { + return currentTodo.id === updatedTodo.id ? updatedTodo : currentTodo; + }) as Todo[]; + }); + } catch (error) { + setErrorMessage('Unable to update todo'); + // throw new Error(); + } + }; + + const deleteTodoHandler = async (todoId: number) => { + setIsLoadingMap(prevLoadingMap => ({ + ...prevLoadingMap, + [todoId]: true, + })); + + try { + await deleteTodo(todoId); + setTodos(currentTodos => currentTodos.filter(({ id }) => id !== todoId)); + } catch { + setErrorMessage('Unable to delete a todo'); + // throw new Error(); + } + + setIsLoadingMap(prevLoadingMap => ({ + ...prevLoadingMap, + [todoId]: false, + })); + }; + + useEffect(() => { + getTodos(USER_ID) + .then(setTodos) + .catch(() => setErrorMessage('Unable to load todos')); + }, []); + + return ( + + {children} + + ); +}; diff --git a/src/index.tsx b/src/index.tsx index 7de19e0c70..7e81c03da4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,11 @@ import '@fortawesome/fontawesome-free/css/all.css'; import './styles/index.scss'; import { App } from './App'; +import { TodoProvider } from './components/TodoProvider'; createRoot(document.getElementById('root') as HTMLDivElement) - .render(); + .render( + + + , + ); diff --git a/src/styles/todo.scss b/src/styles/todo.scss index c7f93ff6b9..fc99714ef2 100644 --- a/src/styles/todo.scss +++ b/src/styles/todo.scss @@ -8,7 +8,7 @@ font-size: 24px; line-height: 1.4em; border-bottom: 1px solid #ededed; - + &:last-child { border-bottom: 0; } @@ -30,7 +30,7 @@ &__title { padding: 12px 15px; - + word-break: break-all; transition: color 0.4s; } @@ -56,7 +56,7 @@ border: 0; background: none; cursor: pointer; - + transform: translateY(-2px); opacity: 0; transition: color 0.2s ease-out; @@ -65,7 +65,7 @@ color: #af5b5e; } } - + &:hover &__remove { opacity: 1; } @@ -73,13 +73,13 @@ &__title-field { width: 100%; padding: 11px 14px; - + font-size: inherit; line-height: inherit; font-family: inherit; font-weight: inherit; color: inherit; - + border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); @@ -92,58 +92,11 @@ .overlay { position: absolute; - top: 0; left: 0; right: 0; - height: 58px; + bottom: 0; + top: 0; opacity: 0.5; } } - -.item-enter { - max-height: 0; -} - -.item-enter-active { - overflow: hidden; - max-height: 58px; - transition: max-height 0.3s ease-in-out; -} - -.item-exit { - max-height: 58px; -} - -.item-exit-active { - overflow: hidden; - max-height: 0; - transition: max-height 0.3s ease-in-out; -} - -.temp-item-enter { - max-height: 0; -} - -.temp-item-enter-active { - overflow: hidden; - max-height: 58px; - transition: max-height 0.3s ease-in-out; -} - -.temp-item-exit { - max-height: 58px; -} - -.temp-item-exit-active { - transform: translateY(-58px); - max-height: 0; - opacity: 0; - transition: 0.3s ease-in-out; - transition-property: opacity, max-height, transform; -} - -.has-error .temp-item-exit-active { - transform: translateY(0); - overflow: hidden; -} diff --git a/src/styles/todoapp.scss b/src/styles/todoapp.scss index 9095f1847f..836166156b 100644 --- a/src/styles/todoapp.scss +++ b/src/styles/todoapp.scss @@ -121,7 +121,6 @@ appearance: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - transition: opacity 0.3s; &:hover { text-decoration: underline; @@ -130,9 +129,5 @@ &:active { text-decoration: none; } - - &:disabled { - visibility: hidden; - } } } diff --git a/src/types/Todo.ts b/src/types/Todo.ts new file mode 100644 index 0000000000..3f52a5fdde --- /dev/null +++ b/src/types/Todo.ts @@ -0,0 +1,6 @@ +export interface Todo { + id: number; + userId: number; + title: string; + completed: boolean; +} diff --git a/src/utils/constants.tsx b/src/utils/constants.tsx new file mode 100644 index 0000000000..1aa2c9b5c0 --- /dev/null +++ b/src/utils/constants.tsx @@ -0,0 +1 @@ +export const USER_ID = 11503; diff --git a/src/utils/fetchClient.ts b/src/utils/fetchClient.ts new file mode 100644 index 0000000000..ca588ab63a --- /dev/null +++ b/src/utils/fetchClient.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +const BASE_URL = 'https://mate.academy/students-api'; + +// returns a promise resolved after a given delay +function wait(delay: number) { + return new Promise(resolve => { + setTimeout(resolve, delay); + }); +} + +// To have autocompletion and avoid mistypes +type RequestMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE'; + +function request( + url: string, + method: RequestMethod = 'GET', + data: any = null, // we can send any data to the server +): Promise { + const options: RequestInit = { method }; + + if (data) { + // We add body and Content-Type only for the requests with data + options.body = JSON.stringify(data); + options.headers = { + 'Content-Type': 'application/json; charset=UTF-8', + }; + } + + // we wait for testing purpose to see loaders + return wait(100) + .then(() => fetch(BASE_URL + url, options)) + .then(response => { + if (!response.ok) { + throw new Error(); + } + + return response.json(); + }); +} + +export const client = { + get: (url: string) => request(url), + post: (url: string, data: any) => request(url, 'POST', data), + patch: (url: string, data: any) => request(url, 'PATCH', data), + delete: (url: string) => request(url, 'DELETE'), +}; From 6c3ebfd0188269d3a0f9161576dbebc723303a59 Mon Sep 17 00:00:00 2001 From: Olha Momot Date: Tue, 26 Sep 2023 10:30:01 +0300 Subject: [PATCH 2/5] fixed updating empty todo --- README.md | 2 +- src/App.tsx | 1 - src/components/Form.tsx | 14 ++++++++++---- src/components/TodoItem.tsx | 2 +- src/components/TodoProvider.tsx | 11 +++++------ 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index af7dae81f6..dc1212e0b6 100644 --- a/README.md +++ b/README.md @@ -41,4 +41,4 @@ Implement the ability to edit a todo title on double click: - Implement a solution following the [React task guideline](https://github.com/mate-academy/react_task-guideline#react-tasks-guideline). - Use the [React TypeScript cheat sheet](https://mate-academy.github.io/fe-program/js/extra/react-typescript). -- Replace `` with your Github username in the [DEMO LINK](https://.github.io/react_todo-app-with-api/) and add it to the PR description. +- Replace `` with your Github username in the [DEMO LINK](https://OlhaMomot.github.io/react_todo-app-with-api/) and add it to the PR description. diff --git a/src/App.tsx b/src/App.tsx index f478ad0356..419b550d2a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -123,7 +123,6 @@ export const App: React.FC = () => { onClick={() => setErrorMessage('')} /> {errorMessage} - {/* Unable to update a todo */}
); diff --git a/src/components/Form.tsx b/src/components/Form.tsx index cc8356f5a8..91b140deb1 100644 --- a/src/components/Form.tsx +++ b/src/components/Form.tsx @@ -33,7 +33,7 @@ export const Form: React.FC = ({ setTempTodo }) => { } }; - const handleSubmit = (event: React.FormEvent) => { + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); setIsSubmiting(true); const newTitle = title.trim(); @@ -53,11 +53,17 @@ export const Form: React.FC = ({ setTempTodo }) => { setTempTodo({ id: 0, ...newTodo }); - addTodoHandler(newTodo, () => { + try { + await addTodoHandler(newTodo); + + setIsSubmiting(false); + setTitle(''); setTempTodo(null); - }); - setIsSubmiting(false); + } catch (error) { + setIsSubmiting(false); + setTempTodo(null); + } }; return ( diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx index 422e3e59d1..7608ce2d9c 100644 --- a/src/components/TodoItem.tsx +++ b/src/components/TodoItem.tsx @@ -45,7 +45,7 @@ export const TodoItem: React.FC = ( event.preventDefault(); setIsItemLoading(true); - if (todoTitle !== todo.title) { + if (todoTitle !== todo.title && todoTitle !== '') { await updateTodoHandler(todo, { title: todoTitle }); } else if (todoTitle === todo.title) { setIsEditing(false); diff --git a/src/components/TodoProvider.tsx b/src/components/TodoProvider.tsx index 6f63fda756..076d8668a0 100644 --- a/src/components/TodoProvider.tsx +++ b/src/components/TodoProvider.tsx @@ -11,7 +11,7 @@ import { USER_ID } from '../utils/constants'; interface TodoContextProps { todos: Todo[]; - addTodoHandler: (todo: Omit, onSuccess: () => void) => void; + addTodoHandler: (todo: Omit) => Promise; deleteTodoHandler: (todoId: number) => Promise; updateTodoHandler: (todo: Todo, property: Partial) => Promise; errorMessage: string; @@ -21,7 +21,7 @@ interface TodoContextProps { export const TodoContext = createContext({ todos: [], - addTodoHandler: () => { }, + addTodoHandler: async () => { }, deleteTodoHandler: async () => { }, updateTodoHandler: async () => { }, errorMessage: '', @@ -40,15 +40,15 @@ export const TodoProvider: React.FC = ({ children }) => { = useState<{ [key: number]: boolean } | {}>({}); const addTodoHandler = async ( - newTodo: Omit, onSuccess: () => void, + newTodo: Omit, ) => { try { const createdTodo = await addTodo(newTodo); setTodos((currentTodos) => [...currentTodos, createdTodo]); - onSuccess(); } catch (error) { - setErrorMessage('Unable to add todo'); + setErrorMessage('Unable to add a todo'); + throw new Error(); } }; @@ -84,7 +84,6 @@ export const TodoProvider: React.FC = ({ children }) => { setTodos(currentTodos => currentTodos.filter(({ id }) => id !== todoId)); } catch { setErrorMessage('Unable to delete a todo'); - // throw new Error(); } setIsLoadingMap(prevLoadingMap => ({ From c7bd252b935c239709dc45a24c1fc3668c9c457d Mon Sep 17 00:00:00 2001 From: Olha Momot Date: Thu, 28 Sep 2023 15:26:59 +0300 Subject: [PATCH 3/5] created header and errorNotification components, useTodo hook and fixed other problems --- src/App.tsx | 88 ++++---------------- src/components/ErrorNotification.tsx | 49 +++++++++++ src/components/Footer.tsx | 18 ++-- src/components/Form.tsx | 83 ------------------- src/components/Header.tsx | 119 +++++++++++++++++++++++++++ src/components/TodoItem.tsx | 22 ++--- src/components/TodoList.tsx | 5 +- src/components/TodoProvider.tsx | 33 ++++---- src/hooks/useTodo.tsx | 24 ++++++ src/types/FilterOptions.tsx | 5 ++ 10 files changed, 254 insertions(+), 192 deletions(-) create mode 100644 src/components/ErrorNotification.tsx delete mode 100644 src/components/Form.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/hooks/useTodo.tsx create mode 100644 src/types/FilterOptions.tsx diff --git a/src/App.tsx b/src/App.tsx index 419b550d2a..3989d131cc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,15 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ import React, { - useContext, useEffect, useMemo, useState, + useMemo, + useState, } from 'react'; -import classNames from 'classnames'; - import { TodoList } from './components/TodoList'; -import { TodoContext } from './components/TodoProvider'; -import { Form } from './components/Form'; +import { Header } from './components/Header'; import { Todo } from './types/Todo'; import { Footer } from './components/Footer'; - -enum FilterOption { - All = 'All', - Active = 'Active', - Completed = 'Completed', -} +import { ErrorNotification } from './components/ErrorNotification'; +import { FilterOption } from './types/FilterOptions'; +import { useTodo } from './hooks/useTodo'; export const App: React.FC = () => { const [filter, setFilter] = useState(FilterOption.All); @@ -24,8 +19,7 @@ export const App: React.FC = () => { todos, setErrorMessage, errorMessage, - updateTodoHandler, - } = useContext(TodoContext); + } = useTodo(); const filteredTodos = useMemo(() => { return todos.filter(({ completed }) => { @@ -45,55 +39,16 @@ export const App: React.FC = () => { return todos.filter(({ completed }) => !completed); }, [todos]); - useEffect(() => { - let timer: NodeJS.Timeout | undefined; - - if (errorMessage) { - timer = setTimeout(() => { - setErrorMessage(''); - }, 3000); - } - - return () => { - if (timer) { - clearTimeout(timer); - } - }; - }, [errorMessage]); - - const onToggleAll = async () => { - if (activeTodos.length) { - activeTodos.forEach( - currentTodo => updateTodoHandler(currentTodo, { completed: true }), - ); - } else { - todos.forEach( - currentTodo => updateTodoHandler(currentTodo, { completed: false }), - ); - } - }; - return (

todos

-
- {!!todos.length - && ( -
+
{!!filteredTodos.length && } @@ -103,27 +58,14 @@ export const App: React.FC = () => {
)}
- -
-
+
); }; diff --git a/src/components/ErrorNotification.tsx b/src/components/ErrorNotification.tsx new file mode 100644 index 0000000000..02117d95c6 --- /dev/null +++ b/src/components/ErrorNotification.tsx @@ -0,0 +1,49 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ +import classNames from 'classnames'; +import { useEffect } from 'react'; + +type Props = { + errorMessage: string; + setErrorMessage: (message: string) => void; +}; + +export const ErrorNotification: React.FC = ({ + errorMessage, + setErrorMessage, +}) => { + useEffect(() => { + let timer: NodeJS.Timeout | undefined; + + if (errorMessage) { + timer = setTimeout(() => { + setErrorMessage(''); + }, 3000); + } + + return () => { + if (timer) { + clearTimeout(timer); + } + }; + }, [errorMessage]); + + return ( +
+
+ ); +}; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index f7eeb22a60..e3ec64249e 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,32 +1,30 @@ import classNames from 'classnames'; -import React, { useContext, useMemo } from 'react'; +import React, { useMemo } from 'react'; -import { TodoContext } from './TodoProvider'; import { Todo } from '../types/Todo'; +import { FilterOption } from '../types/FilterOptions'; +import { useTodo } from '../hooks/useTodo'; type Props = { activeTodos: Todo[], filter: string, - FilterOption: { [key: string]: string }, setFilter: (filter: string) => void, }; export const Footer: React.FC = ({ activeTodos, filter, - FilterOption, setFilter, }) => { - const { todos, deleteTodoHandler } = useContext(TodoContext); + const { todos, deleteTodoHandler } = useTodo(); const completedTodos = useMemo(() => { return todos.filter(({ completed }) => completed === true); }, [todos]); const handleDeleteCompletedTodos = () => { - completedTodos.forEach(({ id }) => { - deleteTodoHandler(id); - }); + Promise.all(completedTodos + .map(({ id }) => deleteTodoHandler(id))); }; const isClearButtonInvisible = completedTodos.length === 0; @@ -34,7 +32,9 @@ export const Footer: React.FC = ({ return (