From 35bfcd89ea7a7fe0ca578050c26f7e0a68b3768d Mon Sep 17 00:00:00 2001 From: Rostyslav Date: Fri, 18 Oct 2024 09:49:31 +0300 Subject: [PATCH 1/4] Initial commit --- src/App.tsx | 93 +++++++++++++++---- src/api/todos.ts | 24 +++++ src/components/ErrorMessage/ErrorMessage.tsx | 33 +++++++ src/components/ErrorMessage/index.ts | 1 + src/components/Footer/Footer.tsx | 58 ++++++++++++ src/components/Footer/index.ts | 1 + src/components/Header/Header.tsx | 97 ++++++++++++++++++++ src/components/Header/index.ts | 1 + src/components/TodoItem/TodoItem.tsx | 64 +++++++++++++ src/components/TodoItem/index.ts | 1 + src/components/TodoList/TodoList.tsx | 30 ++++++ src/components/TodoList/index.ts | 1 + src/components/index.ts | 5 + src/constants/constants.ts | 1 + src/types/Errors.ts | 8 ++ src/types/FilterBy.ts | 5 + src/types/Todo.ts | 6 ++ src/utils/fetchClient.ts | 46 ++++++++++ src/utils/getFilteredTodos.ts | 17 ++++ src/utils/handleDeleteTodos.ts | 29 ++++++ src/utils/handleError.ts | 12 +++ src/utils/handleFetchTodos.ts | 18 ++++ 22 files changed, 532 insertions(+), 19 deletions(-) create mode 100644 src/api/todos.ts create mode 100644 src/components/ErrorMessage/ErrorMessage.tsx create mode 100644 src/components/ErrorMessage/index.ts create mode 100644 src/components/Footer/Footer.tsx create mode 100644 src/components/Footer/index.ts create mode 100644 src/components/Header/Header.tsx create mode 100644 src/components/Header/index.ts create mode 100644 src/components/TodoItem/TodoItem.tsx create mode 100644 src/components/TodoItem/index.ts create mode 100644 src/components/TodoList/TodoList.tsx create mode 100644 src/components/TodoList/index.ts create mode 100644 src/components/index.ts create mode 100644 src/constants/constants.ts create mode 100644 src/types/Errors.ts create mode 100644 src/types/FilterBy.ts create mode 100644 src/types/Todo.ts create mode 100644 src/utils/fetchClient.ts create mode 100644 src/utils/getFilteredTodos.ts create mode 100644 src/utils/handleDeleteTodos.ts create mode 100644 src/utils/handleError.ts create mode 100644 src/utils/handleFetchTodos.ts diff --git a/src/App.tsx b/src/App.tsx index 81e011f43..747648041 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,81 @@ -/* eslint-disable max-len */ +/* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; -import { UserWarning } from './UserWarning'; +import { FC, useEffect, useMemo, useState } from 'react'; -const USER_ID = 0; +import { Todo } from './types/Todo'; +import { Errors } from './types/Errors'; +import { FilterBy } from './types/FilterBy'; -export const App: React.FC = () => { - if (!USER_ID) { - return ; - } +import { getFilteredTodos } from './utils/getFilteredTodos'; +import { handleFetchTodos } from './utils/handleFetchTodos'; +import { handleDeleteTodos } from './utils/handleDeleteTodos'; + +import { Header, TodoList, Footer, ErrorMessage, TodoItem } from './components'; + +export const App: FC = () => { + const [todos, setTodos] = useState([]); + const [tempTodo, setTempTodo] = useState(null); + const [error, setError] = useState(Errors.DEFAULT); + const [idsForDelete, setIdsForDelete] = useState([]); + const [selectedFilter, setSelectedFilter] = useState(FilterBy.ALL); + + const completedTodosId = useMemo(() => { + return getFilteredTodos(todos, FilterBy.COMPLETED).map(todo => todo.id); + }, [todos]); + + const numberOfActiveTodos = useMemo(() => { + return getFilteredTodos(todos, FilterBy.ACTIVE).length; + }, [todos]); + + const filteredTodos = useMemo(() => { + return getFilteredTodos(todos, selectedFilter); + }, [todos, selectedFilter]); + + useEffect(() => { + if (idsForDelete.length) { + handleDeleteTodos(idsForDelete, setTodos, setIdsForDelete, setError); + } + }, [idsForDelete]); + + useEffect(() => { + handleFetchTodos(setTodos, setError); + }, []); return ( -
-

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

- -

Styles are already copied

-
+
+

todos

+ +
+
+ + + + {tempTodo && ( + + )} + + {!!todos.length && ( +
+ )} +
+ + +
); }; diff --git a/src/api/todos.ts b/src/api/todos.ts new file mode 100644 index 000000000..448448f12 --- /dev/null +++ b/src/api/todos.ts @@ -0,0 +1,24 @@ +import { Todo } from '../types/Todo'; + +import { client } from '../utils/fetchClient'; + +import { USER_ID } from '../constants/constants'; + +export const getTodos = (): Promise => { + return client.get(`/todos?userId=${USER_ID}`); +}; + +export const addTodo = (newTodo: Omit): Promise => { + return client.post('/todos', newTodo); +}; + +export const deleteTodo = (id: number): Promise => { + return client.delete(`/todos/${id}`); +}; + +export const updateTodo = ( + id: number, + updates: Partial, +): Promise => { + return client.patch(`/todos/${id}`, updates); +}; diff --git a/src/components/ErrorMessage/ErrorMessage.tsx b/src/components/ErrorMessage/ErrorMessage.tsx new file mode 100644 index 000000000..2c32b2121 --- /dev/null +++ b/src/components/ErrorMessage/ErrorMessage.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react'; +import cn from 'classnames'; + +import { Errors } from '../../types/Errors'; +import { handleError } from '../../utils/handleError'; + +interface Props { + error: Errors; + setError: (error: Errors) => void; +} + +export const ErrorMessage: FC = ({ error, setError }) => { + const handleClose = () => { + handleError(Errors.DEFAULT, setError); + }; + + return ( +
+
+ ); +}; diff --git a/src/components/ErrorMessage/index.ts b/src/components/ErrorMessage/index.ts new file mode 100644 index 000000000..3dee9621e --- /dev/null +++ b/src/components/ErrorMessage/index.ts @@ -0,0 +1 @@ +export * from './ErrorMessage'; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 000000000..9f37e530d --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,58 @@ +import { FC } from 'react'; +import cn from 'classnames'; + +import { FilterBy } from '../../types/FilterBy'; + +interface Props { + selectedFilter: FilterBy; + completedTodosId: number[]; + numberOfActiveTodos: number; + setSelectedFilter: (filter: FilterBy) => void; + setIdsForDelete: (ids: number[]) => void; +} + +const Footer: FC = ({ + selectedFilter, + completedTodosId, + numberOfActiveTodos, + setSelectedFilter, + setIdsForDelete, +}) => { + const filters = Object.values(FilterBy); + + return ( +
+ + {numberOfActiveTodos} items left + + + + + +
+ ); +}; + +export default Footer; diff --git a/src/components/Footer/index.ts b/src/components/Footer/index.ts new file mode 100644 index 000000000..da94c2936 --- /dev/null +++ b/src/components/Footer/index.ts @@ -0,0 +1 @@ +export { default as Footer } from './Footer'; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 000000000..51a8ebe13 --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,97 @@ +import { ChangeEvent, FC, FormEvent, useEffect, useRef, useState } from 'react'; +import cn from 'classnames'; + +import { Todo } from '../../types/Todo'; +import { Errors } from '../../types/Errors'; + +import { addTodo } from '../../api/todos'; +import { handleError } from '../../utils/handleError'; +import { USER_ID } from '../../constants/constants'; + +interface Props { + todos: Todo[]; + tempTodo: Todo | null; + setTodos: (updateTodos: (todos: Todo[]) => Todo[]) => void; + setError: (error: Errors) => void; + setTempTodo: (todo: Todo | null) => void; +} + +const Header: FC = ({ + todos, + tempTodo, + setTodos, + setError, + setTempTodo, +}) => { + const [title, setTitle] = useState(''); + const inputRef = useRef(null); + + const handleChangeTitle = (event: ChangeEvent) => { + setTitle(event.target.value.trimStart()); + }; + + const handleFormSubmit = async (event: FormEvent) => { + event.preventDefault(); + + const formattedTitle = title.trim(); + + if (!formattedTitle) { + handleError(Errors.TITLE_ERROR, setError); + + return; + } + + const newTodo: Omit = { + userId: USER_ID, + title: formattedTitle, + completed: false, + }; + + const tmpTodo: Todo = { + id: 0, + ...newTodo, + }; + + setTempTodo(tmpTodo); + + try { + const todo = await addTodo(newTodo); + + setTodos(currentTodos => [...currentTodos, todo]); + setTitle(''); + setTempTodo(null); + } catch { + setTempTodo(null); + handleError(Errors.ADD_TODO, setError); + } + }; + + useEffect(() => { + inputRef.current?.focus(); + }, [todos, tempTodo]); + + return ( +
+
+ ); +}; + +export default Header; diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts new file mode 100644 index 000000000..5653319de --- /dev/null +++ b/src/components/Header/index.ts @@ -0,0 +1 @@ +export { default as Header } from './Header'; diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx new file mode 100644 index 000000000..09ccbe489 --- /dev/null +++ b/src/components/TodoItem/TodoItem.tsx @@ -0,0 +1,64 @@ +import { FC } from 'react'; +import cn from 'classnames'; + +import { Todo } from '../../types/Todo'; + +interface Props { + todo: Todo; + idsForDelete?: number[]; + setIdsForDelete: (prevIds: (ids: number[]) => number[]) => void; +} + +export const TodoItem: FC = ({ + todo, + idsForDelete, + setIdsForDelete, +}) => { + const { id, title, completed } = todo; + + const handleDelete = () => { + setIdsForDelete(currentIds => [...currentIds, id]); + }; + + return ( +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + + + {title} + + + + +
+
+
+
+
+ ); +}; diff --git a/src/components/TodoItem/index.ts b/src/components/TodoItem/index.ts new file mode 100644 index 000000000..21f4abac3 --- /dev/null +++ b/src/components/TodoItem/index.ts @@ -0,0 +1 @@ +export * from './TodoItem'; diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx new file mode 100644 index 000000000..b6e66a4bc --- /dev/null +++ b/src/components/TodoList/TodoList.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react'; + +import { Todo } from '../../types/Todo'; + +import { TodoItem } from '../TodoItem'; + +interface Props { + todos: Todo[]; + idsForDelete: number[]; + setIdsForDelete: (prevIds: (ids: number[]) => number[]) => void; +} + +export const TodoList: FC = ({ + todos, + idsForDelete, + setIdsForDelete, +}) => { + return ( +
+ {todos.map(todo => ( + + ))} +
+ ); +}; diff --git a/src/components/TodoList/index.ts b/src/components/TodoList/index.ts new file mode 100644 index 000000000..f239f4345 --- /dev/null +++ b/src/components/TodoList/index.ts @@ -0,0 +1 @@ +export * from './TodoList'; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 000000000..788fc6d24 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,5 @@ +export * from './Header'; +export * from './TodoList'; +export * from './TodoItem'; +export * from './Footer'; +export * from './ErrorMessage'; diff --git a/src/constants/constants.ts b/src/constants/constants.ts new file mode 100644 index 000000000..38e4f9b87 --- /dev/null +++ b/src/constants/constants.ts @@ -0,0 +1 @@ +export const USER_ID = 1443; diff --git a/src/types/Errors.ts b/src/types/Errors.ts new file mode 100644 index 000000000..5d5de3b87 --- /dev/null +++ b/src/types/Errors.ts @@ -0,0 +1,8 @@ +export enum Errors { + DEFAULT = '', + LOAD_ERROR = 'Unable to load todos', + TITLE_ERROR = 'Title should not be empty', + ADD_TODO = 'Unable to add a todo', + DELETE_TODO = 'Unable to delete a todo', + UPDATE_TODO = 'Unable to update a todo', +} diff --git a/src/types/FilterBy.ts b/src/types/FilterBy.ts new file mode 100644 index 000000000..f2dfad262 --- /dev/null +++ b/src/types/FilterBy.ts @@ -0,0 +1,5 @@ +export enum FilterBy { + ALL = 'All', + ACTIVE = 'Active', + COMPLETED = 'Completed', +} diff --git a/src/types/Todo.ts b/src/types/Todo.ts new file mode 100644 index 000000000..3f52a5fdd --- /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/fetchClient.ts b/src/utils/fetchClient.ts new file mode 100644 index 000000000..708ac4c17 --- /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', + }; + } + + // DON'T change the delay it is required for tests + 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'), +}; diff --git a/src/utils/getFilteredTodos.ts b/src/utils/getFilteredTodos.ts new file mode 100644 index 000000000..1a9019aaf --- /dev/null +++ b/src/utils/getFilteredTodos.ts @@ -0,0 +1,17 @@ +import { Todo } from '../types/Todo'; +import { FilterBy } from '../types/FilterBy'; + +export const getFilteredTodos = (todos: Todo[], filter: FilterBy) => { + return todos.filter(todo => { + switch (filter) { + case FilterBy.ACTIVE: + return !todo.completed; + + case FilterBy.COMPLETED: + return todo.completed; + + default: + return true; + } + }); +}; diff --git a/src/utils/handleDeleteTodos.ts b/src/utils/handleDeleteTodos.ts new file mode 100644 index 000000000..a26e89a3e --- /dev/null +++ b/src/utils/handleDeleteTodos.ts @@ -0,0 +1,29 @@ +import { Dispatch, SetStateAction } from 'react'; + +import { Todo } from '../types/Todo'; +import { Errors } from '../types/Errors'; + +import { deleteTodo } from '../api/todos'; +import { handleError } from './handleError'; + +export const handleDeleteTodos = ( + idsForDelete: number[], + setTodos: Dispatch>, + setIdsForDelete: Dispatch>, + setError: Dispatch>, +) => { + Promise.allSettled( + idsForDelete.map(id => + deleteTodo(id) + .then(() => { + setTodos(currentTodos => currentTodos.filter(todo => todo.id !== id)); + }) + .catch(() => { + handleError(Errors.DELETE_TODO, setError); + }) + .finally(() => { + setIdsForDelete([]); + }), + ), + ); +}; diff --git a/src/utils/handleError.ts b/src/utils/handleError.ts new file mode 100644 index 000000000..a9756da24 --- /dev/null +++ b/src/utils/handleError.ts @@ -0,0 +1,12 @@ +import { Errors } from '../types/Errors'; + +export const handleError = ( + error: Errors, + setError: (error: Errors) => void, +) => { + setError(error); + + setTimeout(() => { + setError(Errors.DEFAULT); + }, 3000); +}; diff --git a/src/utils/handleFetchTodos.ts b/src/utils/handleFetchTodos.ts new file mode 100644 index 000000000..8fd04f730 --- /dev/null +++ b/src/utils/handleFetchTodos.ts @@ -0,0 +1,18 @@ +import { Todo } from '../types/Todo'; +import { Errors } from '../types/Errors'; + +import { getTodos } from '../api/todos'; +import { handleError } from './handleError'; + +export const handleFetchTodos = async ( + setTodos: (todos: Todo[]) => void, + setError: (error: Errors) => void, +) => { + try { + const todos = await getTodos(); + + setTodos(todos); + } catch { + handleError(Errors.LOAD_ERROR, setError); + } +}; From ce109faf85f4bfef297aab4b9fd8955407a7c865 Mon Sep 17 00:00:00 2001 From: Rostyslav Date: Sat, 19 Oct 2024 21:39:17 +0300 Subject: [PATCH 2/4] Solution --- src/App.tsx | 30 ++++++- src/components/Header/Header.tsx | 18 +++- src/components/TodoItem/TodoItem.tsx | 118 +++++++++++++++++++++++---- src/components/TodoList/TodoList.tsx | 9 ++ src/utils/handleUpdateTodos.ts | 33 ++++++++ 5 files changed, 191 insertions(+), 17 deletions(-) create mode 100644 src/utils/handleUpdateTodos.ts diff --git a/src/App.tsx b/src/App.tsx index 747648041..6db11be64 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import { handleFetchTodos } from './utils/handleFetchTodos'; import { handleDeleteTodos } from './utils/handleDeleteTodos'; import { Header, TodoList, Footer, ErrorMessage, TodoItem } from './components'; +import { handleUpdateTodos } from './utils/handleUpdateTodos'; export const App: FC = () => { const [todos, setTodos] = useState([]); @@ -19,6 +20,10 @@ export const App: FC = () => { const [idsForDelete, setIdsForDelete] = useState([]); const [selectedFilter, setSelectedFilter] = useState(FilterBy.ALL); + const [idsForUpdate, setIdsForUpdate] = useState([]); + + const [newTodoData, setNewTodoData] = useState>({}); + const completedTodosId = useMemo(() => { return getFilteredTodos(todos, FilterBy.COMPLETED).map(todo => todo.id); }, [todos]); @@ -37,6 +42,18 @@ export const App: FC = () => { } }, [idsForDelete]); + useEffect(() => { + if (idsForUpdate.length) { + handleUpdateTodos( + idsForUpdate, + newTodoData, + setTodos, + setIdsForUpdate, + setError, + ); + } + }, [idsForUpdate, newTodoData]); + useEffect(() => { handleFetchTodos(setTodos, setError); }, []); @@ -49,19 +66,30 @@ export const App: FC = () => {
{tempTodo && ( - + )} {!!todos.length && ( diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 51a8ebe13..ea6897f29 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -11,17 +11,23 @@ import { USER_ID } from '../../constants/constants'; interface Props { todos: Todo[]; tempTodo: Todo | null; + numberOfActiveTodos: number; setTodos: (updateTodos: (todos: Todo[]) => Todo[]) => void; setError: (error: Errors) => void; setTempTodo: (todo: Todo | null) => void; + setIdsForUpdate: (prevIds: (ids: number[]) => number[]) => void; + setNewTodoData: (newData: Partial) => void; } const Header: FC = ({ todos, tempTodo, + numberOfActiveTodos, setTodos, setError, setTempTodo, + setIdsForUpdate, + setNewTodoData, }) => { const [title, setTitle] = useState(''); const inputRef = useRef(null); @@ -66,6 +72,13 @@ const Header: FC = ({ } }; + const handleToggleAll = () => { + todos.map(todo => { + setIdsForUpdate(currentIds => [...currentIds, todo.id]); + setNewTodoData({ completed: !todo.completed }); + }); + }; + useEffect(() => { inputRef.current?.focus(); }, [todos, tempTodo]); @@ -74,8 +87,11 @@ const Header: FC = ({
+ + + ) : ( + + + + )}
diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx index b6e66a4bc..6e5a490d5 100644 --- a/src/components/TodoList/TodoList.tsx +++ b/src/components/TodoList/TodoList.tsx @@ -7,13 +7,19 @@ import { TodoItem } from '../TodoItem'; interface Props { todos: Todo[]; idsForDelete: number[]; + idsForUpdate: number[]; setIdsForDelete: (prevIds: (ids: number[]) => number[]) => void; + setIdsForUpdate: (prevIds: (ids: number[]) => number[]) => void; + setNewTodoData: (newTodoData: Partial) => void; } export const TodoList: FC = ({ todos, idsForDelete, + idsForUpdate, setIdsForDelete, + setIdsForUpdate, + setNewTodoData, }) => { return (
@@ -22,7 +28,10 @@ export const TodoList: FC = ({ key={todo.id} todo={todo} idsForDelete={idsForDelete} + idsForUpdate={idsForUpdate} setIdsForDelete={setIdsForDelete} + setIdsForUpdate={setIdsForUpdate} + setNewTodoData={setNewTodoData} /> ))}
diff --git a/src/utils/handleUpdateTodos.ts b/src/utils/handleUpdateTodos.ts new file mode 100644 index 000000000..d84c3de95 --- /dev/null +++ b/src/utils/handleUpdateTodos.ts @@ -0,0 +1,33 @@ +import { Dispatch, SetStateAction } from 'react'; + +import { Todo } from '../types/Todo'; +import { Errors } from '../types/Errors'; + +import { updateTodo } from '../api/todos'; +import { handleError } from './handleError'; + +export const handleUpdateTodos = ( + idsForUpdate: number[], + newData: Partial, + setTodos: Dispatch>, + setIdsForUpdate: Dispatch>, + setError: Dispatch>, +) => { + Promise.allSettled( + idsForUpdate.map(id => + updateTodo(id, newData) + .then(updatedTodo => { + setTodos(currentTodos => + currentTodos.map(todo => + todo.id !== updatedTodo.id ? todo : updatedTodo, + ), + ); + }) + .catch(() => { + handleError(Errors.UPDATE_TODO, setError); + }), + ), + ).finally(() => { + setIdsForUpdate([]); + }); +}; From 65fbff2178629372a6fc0b54dea0ed3091eac09b Mon Sep 17 00:00:00 2001 From: Rostyslav Date: Tue, 22 Oct 2024 13:14:28 +0300 Subject: [PATCH 3/4] Fixes --- src/App.tsx | 9 +++++---- src/components/Header/Header.tsx | 5 ++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 6db11be64..ba84c8c42 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,13 +16,14 @@ import { handleUpdateTodos } from './utils/handleUpdateTodos'; export const App: FC = () => { const [todos, setTodos] = useState([]); const [tempTodo, setTempTodo] = useState(null); - const [error, setError] = useState(Errors.DEFAULT); - const [idsForDelete, setIdsForDelete] = useState([]); - const [selectedFilter, setSelectedFilter] = useState(FilterBy.ALL); + const [newTodoData, setNewTodoData] = useState>({}); + const [idsForDelete, setIdsForDelete] = useState([]); const [idsForUpdate, setIdsForUpdate] = useState([]); - const [newTodoData, setNewTodoData] = useState>({}); + const [error, setError] = useState(Errors.DEFAULT); + + const [selectedFilter, setSelectedFilter] = useState(FilterBy.ALL); const completedTodosId = useMemo(() => { return getFilteredTodos(todos, FilterBy.COMPLETED).map(todo => todo.id); diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index ea6897f29..8431b79fb 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -6,6 +6,7 @@ import { Errors } from '../../types/Errors'; import { addTodo } from '../../api/todos'; import { handleError } from '../../utils/handleError'; + import { USER_ID } from '../../constants/constants'; interface Props { @@ -73,9 +74,11 @@ const Header: FC = ({ }; const handleToggleAll = () => { + const isAllTodosCompleted = todos.every(todo => todo.completed); + todos.map(todo => { setIdsForUpdate(currentIds => [...currentIds, todo.id]); - setNewTodoData({ completed: !todo.completed }); + setNewTodoData({ completed: isAllTodosCompleted ? false : true }); }); }; From b8e256882882c9873ac4365fe9a323f6e05f6355 Mon Sep 17 00:00:00 2001 From: Rostyslav Date: Tue, 22 Oct 2024 14:23:32 +0300 Subject: [PATCH 4/4] Final solutions --- src/components/TodoItem/TodoItem.tsx | 34 +++++++++++++++------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx index eeb082665..6c64a9209 100644 --- a/src/components/TodoItem/TodoItem.tsx +++ b/src/components/TodoItem/TodoItem.tsx @@ -78,6 +78,9 @@ export const TodoItem: FC = ({ [title, handleTitleSave], ); + const isLoaderShow = + !id || idsForDelete?.includes(id) || idsForUpdate?.includes(id); + useEffect(() => { if (isEditing) { document.addEventListener('keyup', handleKeyUp); @@ -107,7 +110,20 @@ export const TodoItem: FC = ({ /> - {!isEditing ? ( + {isEditing ? ( +
+ +
+ ) : ( <> {title} @@ -122,26 +138,12 @@ export const TodoItem: FC = ({ × - ) : ( -
- -
)}