From 5e5c506514ad451ad286b8357bc55b748d2ebf56 Mon Sep 17 00:00:00 2001 From: taniabarkovskya Date: Wed, 18 Dec 2024 21:40:09 +0200 Subject: [PATCH 1/5] solution --- src/App.tsx | 183 ++++++++++++++++-- src/api/todos.ts | 20 ++ .../ErrorNotification/ErrorNotification.tsx | 43 ++++ src/components/ErrorNotification/index.tsx | 1 + src/components/Footer/Footer.tsx | 58 ++++++ src/components/Footer/index.tsx | 1 + src/components/Header/Header.tsx | 106 ++++++++++ src/components/Header/index.tsx | 1 + .../TodoComponent/TodoComponent.tsx | 146 ++++++++++++++ src/components/TodoComponent/index.tsx | 1 + src/components/TodoItem/TodoItem.tsx | 43 ++++ src/components/TodoItem/index.tsx | 1 + src/components/TodoList/TodoList.tsx | 46 +++++ src/components/TodoList/index.tsx | 1 + src/types/ErrorTypes.ts | 8 + src/types/Filters.ts | 5 + src/types/FiltersType.ts | 5 + src/types/Todo.ts | 6 + src/utils/fetchClient.ts | 46 +++++ 19 files changed, 707 insertions(+), 14 deletions(-) create mode 100644 src/api/todos.ts create mode 100644 src/components/ErrorNotification/ErrorNotification.tsx create mode 100644 src/components/ErrorNotification/index.tsx create mode 100644 src/components/Footer/Footer.tsx create mode 100644 src/components/Footer/index.tsx create mode 100644 src/components/Header/Header.tsx create mode 100644 src/components/Header/index.tsx create mode 100644 src/components/TodoComponent/TodoComponent.tsx create mode 100644 src/components/TodoComponent/index.tsx create mode 100644 src/components/TodoItem/TodoItem.tsx create mode 100644 src/components/TodoItem/index.tsx create mode 100644 src/components/TodoList/TodoList.tsx create mode 100644 src/components/TodoList/index.tsx create mode 100644 src/types/ErrorTypes.ts create mode 100644 src/types/Filters.ts create mode 100644 src/types/FiltersType.ts create mode 100644 src/types/Todo.ts create mode 100644 src/utils/fetchClient.ts diff --git a/src/App.tsx b/src/App.tsx index 81e011f432..0364ab2da4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,181 @@ /* 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 React, { useEffect, useMemo, useState } from 'react'; import { UserWarning } from './UserWarning'; - -const USER_ID = 0; +import { + createTodo, + deleteTodo, + getTodos, + updateTodo, + USER_ID, +} from './api/todos'; +import { Todo } from './types/Todo'; +import { ErrorNotification } from './components/ErrorNotification'; +import { FiltersType } from './types/FiltersType'; +import { TodoList } from './components/TodoList'; +import { Footer } from './components/Footer'; +import { Header } from './components/Header'; +import { ErrorTypes } from './types/ErrorTypes'; export const App: React.FC = () => { + const [todos, setTodos] = useState([]); + const [errorTodos, setErrorTodos] = useState(ErrorTypes.NoErrors); + const [status, setStatus] = useState(FiltersType.All); + const [isLoading, setIsLoading] = useState(true); + const [loadingTodosIds, setLoadingTodosIds] = useState([]); + + const [tempTodo, setTempTodo] = useState(null); + + const handleErrorClose = () => { + setErrorTodos(ErrorTypes.NoErrors); + }; + + const visibleTodos = todos?.filter(todo => { + switch (status) { + case FiltersType.Completed: + return todo.completed; + case FiltersType.Active: + return !todo.completed; + default: + return true; + } + }); + + const activeTodosCount = useMemo( + () => todos.filter(todo => !todo.completed).length, + [todos], + ); + + const completedTodos = useMemo( + () => todos.filter(todo => todo.completed), + [todos], + ); + const completedTodosCount = completedTodos.length; + + useEffect(() => { + setIsLoading(true); + getTodos() + .then(data => setTodos(data)) + .catch(() => { + setErrorTodos(ErrorTypes.Loading); + }) + .finally(() => { + setIsLoading(false); + }); + }, []); + + const onAddTodo = async (newTodo: Omit): Promise => { + try { + const createdTodo = await createTodo(newTodo); + + setTodos(currentTodos => [...currentTodos, createdTodo]); + } catch (error) { + setErrorTodos(ErrorTypes.Add); + throw error; + } + }; + + const onDeleteTodo = async (todoId: number) => { + setLoadingTodosIds(currentIds => [...currentIds, todoId]); + try { + await deleteTodo(todoId); + setTodos(currentTodos => currentTodos.filter(todo => todo.id !== todoId)); + } catch (error) { + setErrorTodos(ErrorTypes.Delete); + throw error; + } finally { + setLoadingTodosIds(currentIds => currentIds.filter(id => id !== todoId)); + } + }; + + const onDeleteAllCompleted = () => { + completedTodos.forEach(completedTodo => { + onDeleteTodo(completedTodo.id); + }); + }; + + const onUpdateTodo = async (newTodo: Todo) => { + setLoadingTodosIds(currentIds => [...currentIds, newTodo.id]); + try { + const updatedTodo = await updateTodo(newTodo); + + setTodos(currentTodos => + currentTodos.map(todo => { + return todo.id === updatedTodo.id ? updatedTodo : todo; + }), + ); + } catch (error) { + setErrorTodos(ErrorTypes.Update); + throw error; + } finally { + setLoadingTodosIds(currentIds => + currentIds.filter(id => id !== newTodo.id), + ); + } + }; + + const onUpdateToggleAll = () => { + todos.forEach(todo => { + if (todos.length === completedTodos.length) { + onUpdateTodo({ ...todo, completed: false }); + + return; + } + + if (todo.completed === false) { + onUpdateTodo({ ...todo, completed: true }); + + return; + } + }); + }; + if (!USER_ID) { return ; } return ( -
-

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

- -

Styles are already copied

-
+
+

todos

+ +
+
+ + {todos?.length && ( + <> + + +
+ + )} +
+ + +
); }; diff --git a/src/api/todos.ts b/src/api/todos.ts new file mode 100644 index 0000000000..957a9b7e49 --- /dev/null +++ b/src/api/todos.ts @@ -0,0 +1,20 @@ +import { Todo } from '../types/Todo'; +import { client } from '../utils/fetchClient'; + +export const USER_ID = 2153; + +export const getTodos = () => { + return client.get(`/todos?userId=${USER_ID}`); +}; + +export const deleteTodo = (todoId: number) => { + return client.delete(`/todos/${todoId}`); +}; + +export const createTodo = (newTodo: Omit) => { + return client.post(`/todos`, newTodo); +}; + +export const updateTodo = ({ id, title, userId, completed }: Todo) => { + return client.patch(`/todos/${id}`, { id, title, userId, completed }); +}; diff --git a/src/components/ErrorNotification/ErrorNotification.tsx b/src/components/ErrorNotification/ErrorNotification.tsx new file mode 100644 index 0000000000..f8026bc0ef --- /dev/null +++ b/src/components/ErrorNotification/ErrorNotification.tsx @@ -0,0 +1,43 @@ +import React, { useEffect } from 'react'; +import cn from 'classnames'; +import { ErrorTypes } from '../../types/ErrorTypes'; + +type Props = { + error: string; + handleErrorClose: () => void; +}; + +export const ErrorNotification: React.FC = props => { + const { error, handleErrorClose } = props; + + useEffect(() => { + if (error === ErrorTypes.NoErrors) { + return; + } + + const timeoutId = setTimeout(() => { + handleErrorClose(); + }, 3000); + + return () => { + clearTimeout(timeoutId); + }; + }, [error]); + + return ( +
+
+ ); +}; diff --git a/src/components/ErrorNotification/index.tsx b/src/components/ErrorNotification/index.tsx new file mode 100644 index 0000000000..8cb4787920 --- /dev/null +++ b/src/components/ErrorNotification/index.tsx @@ -0,0 +1 @@ +export * from './ErrorNotification'; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 0000000000..c6ebc94310 --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import cn from 'classnames'; +import { FiltersType } from '../../types/FiltersType'; +import { Dispatch, SetStateAction } from 'react'; + +type Props = { + activeCount: number | undefined; + completedCount: number | undefined; + status: FiltersType; + setStatus: Dispatch>; + onDeleteAllCompleted: () => void; +}; + +export const Footer: React.FC = props => { + const { + activeCount, + completedCount, + status, + setStatus, + onDeleteAllCompleted, + } = props; + + return ( + + ); +}; diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx new file mode 100644 index 0000000000..ddcc5a9cd1 --- /dev/null +++ b/src/components/Footer/index.tsx @@ -0,0 +1 @@ +export * from './Footer'; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 0000000000..9272c195ff --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,106 @@ +/* eslint-disable max-len */ +import React, { + Dispatch, + SetStateAction, + useEffect, + useRef, + useState, +} from 'react'; +import cn from 'classnames'; +import { USER_ID } from '../../api/todos'; +import { Todo } from '../../types/Todo'; +import { ErrorTypes } from '../../types/ErrorTypes'; + +type Props = { + todos: Todo[]; + completedTodosCount: number; + onAddTodo: (newTodo: Omit) => Promise; + setErrorTodos: Dispatch>; + tempTodo: Todo | null; + setTempTodo: Dispatch>; + onUpdateToggleAll: () => void; +}; + +export const Header: React.FC = props => { + const { + todos, + completedTodosCount, + onAddTodo, + setErrorTodos, + tempTodo, + setTempTodo, + onUpdateToggleAll, + } = props; + + const [todoTitle, setTodoTitle] = useState(''); + const normalizedTitle = todoTitle.trim(); + + const inputNameRef = useRef(null); + + useEffect(() => { + if (inputNameRef.current) { + inputNameRef.current.focus(); + } + }, [todos, tempTodo]); + + const onResetTitle = () => { + setTodoTitle(''); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!normalizedTitle) { + setErrorTodos(ErrorTypes.EmptyTitle); + + return; + } + + setTempTodo({ + id: 0, + title: normalizedTitle, + userId: USER_ID, + completed: false, + }); + + try { + await onAddTodo({ + title: normalizedTitle, + userId: USER_ID, + completed: false, + }); + onResetTitle(); + } catch (error) { + } finally { + setTempTodo(null); + } + }; + + return ( +
+ {todos.length > 0 && ( +
+ ); +}; diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx new file mode 100644 index 0000000000..266dec8a1b --- /dev/null +++ b/src/components/Header/index.tsx @@ -0,0 +1 @@ +export * from './Header'; diff --git a/src/components/TodoComponent/TodoComponent.tsx b/src/components/TodoComponent/TodoComponent.tsx new file mode 100644 index 0000000000..0c40609d21 --- /dev/null +++ b/src/components/TodoComponent/TodoComponent.tsx @@ -0,0 +1,146 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +import React, { + Dispatch, + SetStateAction, + useEffect, + useRef, + useState, +} from 'react'; +import cn from 'classnames'; + +import { Todo } from '../../types/Todo'; + +type Props = { + todo: Todo; + onDeleteTodo: (id: number) => Promise; + onUpdateTodo: (newTodo: Todo) => Promise; + isDataLoading: boolean; + isDeleting: boolean; + editingTodoId: number | null; + setEditingTodoId: Dispatch>; +}; + +export const TodoComponent: React.FC = props => { + const { + todo, + onDeleteTodo, + onUpdateTodo, + isDataLoading, + isDeleting, + editingTodoId, + setEditingTodoId, + } = props; + + const [newTitle, setNewTitle] = useState(todo.title); + + const inputNameRef = useRef(null); + + useEffect(() => { + if (inputNameRef.current) { + inputNameRef.current.focus(); + } + }, [editingTodoId]); + + const handleToggle = async (newTodo: Todo) => { + try { + await onUpdateTodo({ ...newTodo, completed: !newTodo.completed }); + } catch (error) {} + }; + + const handleEdit = async (newTodo: Todo) => { + try { + if (newTitle === newTodo.title) { + setEditingTodoId(null); + + return; + } + + if (newTitle.trim() === '') { + await onDeleteTodo(newTodo.id); + setEditingTodoId(null); + + return; + } + + await onUpdateTodo({ + ...newTodo, + id: newTodo.id, + title: newTitle.trim(), + }); + setEditingTodoId(null); + } catch (error) { + } finally { + // setEditingTodoId(null); + } + }; + + return ( +
+ + + {editingTodoId === todo.id ? ( +
{ + event.preventDefault(); + handleEdit(todo); + }} + onBlur={() => handleEdit(todo)} + > + { + setNewTitle(event.target.value); + }} + onKeyUp={event => { + if (event.key === 'Escape') { + setEditingTodoId(null); + } + }} + /> +
+ ) : ( + <> + setEditingTodoId(todo.id)} + > + {todo.title} + + + + )} + +
+
+
+
+
+ ); +}; diff --git a/src/components/TodoComponent/index.tsx b/src/components/TodoComponent/index.tsx new file mode 100644 index 0000000000..11cf5d69d2 --- /dev/null +++ b/src/components/TodoComponent/index.tsx @@ -0,0 +1 @@ +export * from './TodoComponent'; diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx new file mode 100644 index 0000000000..3f05075578 --- /dev/null +++ b/src/components/TodoItem/TodoItem.tsx @@ -0,0 +1,43 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +import React from 'react'; +import cn from 'classnames'; + +import { Todo } from '../../types/Todo'; + +type Props = { + todo: Todo; + onDeleteTodo: (id: number) => void; +}; + +export const TodoItem: React.FC = props => { + const { todo, onDeleteTodo } = props; + + return ( +
+ + + + {todo.title} + + +
+
+
+
+
+ ); +}; diff --git a/src/components/TodoItem/index.tsx b/src/components/TodoItem/index.tsx new file mode 100644 index 0000000000..21f4abac39 --- /dev/null +++ b/src/components/TodoItem/index.tsx @@ -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 0000000000..e6b2129833 --- /dev/null +++ b/src/components/TodoList/TodoList.tsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; + +import { Todo } from '../../types/Todo'; +import { TodoComponent } from '../TodoComponent'; +import { TodoItem } from '../TodoItem'; + +type Props = { + todos: Todo[]; + onDeleteTodo: (id: number) => Promise; + onUpdateTodo: (newTodo: Todo) => Promise; + isDataLoading: boolean; + loadingTodosIds: number[]; + tempTodo: Todo | null; +}; + +export const TodoList: React.FC = props => { + const { + todos, + onDeleteTodo, + onUpdateTodo, + isDataLoading, + loadingTodosIds, + tempTodo, + } = props; + + const [editingTodoId, setEditingTodoId] = useState(null); + + return ( +
+ {todos?.map(todo => ( + + ))} + + {tempTodo && } +
+ ); +}; diff --git a/src/components/TodoList/index.tsx b/src/components/TodoList/index.tsx new file mode 100644 index 0000000000..f239f43459 --- /dev/null +++ b/src/components/TodoList/index.tsx @@ -0,0 +1 @@ +export * from './TodoList'; diff --git a/src/types/ErrorTypes.ts b/src/types/ErrorTypes.ts new file mode 100644 index 0000000000..23a270163a --- /dev/null +++ b/src/types/ErrorTypes.ts @@ -0,0 +1,8 @@ +export enum ErrorTypes { + NoErrors = '', + Loading = 'Unable to load todos', + EmptyTitle = 'Title should not be empty', + Add = 'Unable to add a todo', + Delete = 'Unable to delete a todo', + Update = 'Unable to update a todo', +} diff --git a/src/types/Filters.ts b/src/types/Filters.ts new file mode 100644 index 0000000000..6786d4f5b3 --- /dev/null +++ b/src/types/Filters.ts @@ -0,0 +1,5 @@ +export enum Filters { + All = 'all', + Active = 'active', + Completed = 'completed', +} diff --git a/src/types/FiltersType.ts b/src/types/FiltersType.ts new file mode 100644 index 0000000000..5952d3260c --- /dev/null +++ b/src/types/FiltersType.ts @@ -0,0 +1,5 @@ +export enum FiltersType { + All = 'All', + Active = 'Active', + Completed = 'Completed', +} 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/fetchClient.ts b/src/utils/fetchClient.ts new file mode 100644 index 0000000000..708ac4c17b --- /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'), +}; From f254b3dc04ee1f928158cb51788eb01d5d9e0028 Mon Sep 17 00:00:00 2001 From: taniabarkovskya Date: Thu, 19 Dec 2024 13:07:20 +0200 Subject: [PATCH 2/5] fix solution --- src/App.tsx | 6 ++--- .../ErrorNotification/ErrorNotification.tsx | 2 +- .../TodoComponent/TodoComponent.tsx | 24 +++++++++---------- src/components/TodoItem/index.tsx | 1 - src/components/TodoList/TodoList.tsx | 6 ++--- .../TodoItem.tsx => TodoTemp/TodoTemp.tsx} | 12 +++------- src/components/TodoTemp/index.tsx | 1 + 7 files changed, 23 insertions(+), 29 deletions(-) delete mode 100644 src/components/TodoItem/index.tsx rename src/components/{TodoItem/TodoItem.tsx => TodoTemp/TodoTemp.tsx} (74%) create mode 100644 src/components/TodoTemp/index.tsx diff --git a/src/App.tsx b/src/App.tsx index 0364ab2da4..54788110e6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ /* eslint-disable max-len */ /* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { UserWarning } from './UserWarning'; import { createTodo, @@ -27,9 +27,9 @@ export const App: React.FC = () => { const [tempTodo, setTempTodo] = useState(null); - const handleErrorClose = () => { + const handleErrorClose = useCallback(() => { setErrorTodos(ErrorTypes.NoErrors); - }; + }, []); const visibleTodos = todos?.filter(todo => { switch (status) { diff --git a/src/components/ErrorNotification/ErrorNotification.tsx b/src/components/ErrorNotification/ErrorNotification.tsx index f8026bc0ef..7a418e8f45 100644 --- a/src/components/ErrorNotification/ErrorNotification.tsx +++ b/src/components/ErrorNotification/ErrorNotification.tsx @@ -22,7 +22,7 @@ export const ErrorNotification: React.FC = props => { return () => { clearTimeout(timeoutId); }; - }, [error]); + }, [error, handleErrorClose]); return (
Promise; onUpdateTodo: (newTodo: Todo) => Promise; isDataLoading: boolean; - isDeleting: boolean; + isTodoLoading: boolean; editingTodoId: number | null; setEditingTodoId: Dispatch>; }; @@ -26,7 +26,7 @@ export const TodoComponent: React.FC = props => { onDeleteTodo, onUpdateTodo, isDataLoading, - isDeleting, + isTodoLoading, editingTodoId, setEditingTodoId, } = props; @@ -64,13 +64,16 @@ export const TodoComponent: React.FC = props => { await onUpdateTodo({ ...newTodo, - id: newTodo.id, title: newTitle.trim(), }); setEditingTodoId(null); - } catch (error) { - } finally { - // setEditingTodoId(null); + } catch (error) {} + }; + + const handleEscape = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + setEditingTodoId(null); + setNewTitle(todo.title); } }; @@ -100,16 +103,13 @@ export const TodoComponent: React.FC = props => { type="text" data-cy="TodoTitleField" className="todo__title-field" + placeholder="Empty todo will be deleted" ref={inputNameRef} value={newTitle} onChange={event => { setNewTitle(event.target.value); }} - onKeyUp={event => { - if (event.key === 'Escape') { - setEditingTodoId(null); - } - }} + onKeyUp={handleEscape} /> ) : ( @@ -135,7 +135,7 @@ export const TodoComponent: React.FC = props => {
diff --git a/src/components/TodoItem/index.tsx b/src/components/TodoItem/index.tsx deleted file mode 100644 index 21f4abac39..0000000000 --- a/src/components/TodoItem/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './TodoItem'; diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx index e6b2129833..d40f4f8e99 100644 --- a/src/components/TodoList/TodoList.tsx +++ b/src/components/TodoList/TodoList.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Todo } from '../../types/Todo'; import { TodoComponent } from '../TodoComponent'; -import { TodoItem } from '../TodoItem'; +import { TodoTemp } from '../TodoTemp'; type Props = { todos: Todo[]; @@ -34,13 +34,13 @@ export const TodoList: React.FC = props => { onDeleteTodo={onDeleteTodo} onUpdateTodo={onUpdateTodo} isDataLoading={isDataLoading} - isDeleting={loadingTodosIds.includes(todo.id)} + isTodoLoading={loadingTodosIds.includes(todo.id)} editingTodoId={editingTodoId} setEditingTodoId={setEditingTodoId} /> ))} - {tempTodo && } + {tempTodo && } ); }; diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoTemp/TodoTemp.tsx similarity index 74% rename from src/components/TodoItem/TodoItem.tsx rename to src/components/TodoTemp/TodoTemp.tsx index 3f05075578..0ae1d4e051 100644 --- a/src/components/TodoItem/TodoItem.tsx +++ b/src/components/TodoTemp/TodoTemp.tsx @@ -6,11 +6,10 @@ import { Todo } from '../../types/Todo'; type Props = { todo: Todo; - onDeleteTodo: (id: number) => void; }; -export const TodoItem: React.FC = props => { - const { todo, onDeleteTodo } = props; +export const TodoTemp: React.FC = props => { + const { todo } = props; return (
@@ -26,12 +25,7 @@ export const TodoItem: React.FC = props => { {todo.title} -
diff --git a/src/components/TodoTemp/index.tsx b/src/components/TodoTemp/index.tsx new file mode 100644 index 0000000000..c85ea5b4dd --- /dev/null +++ b/src/components/TodoTemp/index.tsx @@ -0,0 +1 @@ +export * from './TodoTemp'; From 85f0eee88451dcd27a3c1588a303a04459932483 Mon Sep 17 00:00:00 2001 From: taniabarkovskya Date: Thu, 19 Dec 2024 17:49:42 +0200 Subject: [PATCH 3/5] fix problems from part 2 --- src/App.tsx | 40 ++++++++----------- .../ErrorNotification/ErrorNotification.tsx | 4 +- src/components/Footer/Footer.tsx | 10 ++--- src/components/Header/Header.tsx | 20 +++++----- src/types/{ErrorTypes.ts => ErrorType.ts} | 2 +- src/types/{FiltersType.ts => FilterType.ts} | 2 +- src/types/Filters.ts | 5 --- 7 files changed, 36 insertions(+), 47 deletions(-) rename src/types/{ErrorTypes.ts => ErrorType.ts} (89%) rename src/types/{FiltersType.ts => FilterType.ts} (71%) delete mode 100644 src/types/Filters.ts diff --git a/src/App.tsx b/src/App.tsx index 54788110e6..e363dc6f4e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ /* eslint-disable max-len */ /* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { UserWarning } from './UserWarning'; import { createTodo, @@ -12,53 +12,47 @@ import { } from './api/todos'; import { Todo } from './types/Todo'; import { ErrorNotification } from './components/ErrorNotification'; -import { FiltersType } from './types/FiltersType'; +import { FilterType } from './types/FilterType'; import { TodoList } from './components/TodoList'; import { Footer } from './components/Footer'; import { Header } from './components/Header'; -import { ErrorTypes } from './types/ErrorTypes'; +import { ErrorType } from './types/ErrorType'; export const App: React.FC = () => { const [todos, setTodos] = useState([]); - const [errorTodos, setErrorTodos] = useState(ErrorTypes.NoErrors); - const [status, setStatus] = useState(FiltersType.All); + const [errorTodos, setErrorTodos] = useState(ErrorType.NoErrors); + const [status, setStatus] = useState(FilterType.All); const [isLoading, setIsLoading] = useState(true); const [loadingTodosIds, setLoadingTodosIds] = useState([]); const [tempTodo, setTempTodo] = useState(null); const handleErrorClose = useCallback(() => { - setErrorTodos(ErrorTypes.NoErrors); + setErrorTodos(ErrorType.NoErrors); }, []); - const visibleTodos = todos?.filter(todo => { + const visibleTodos = todos.filter(todo => { switch (status) { - case FiltersType.Completed: + case FilterType.Completed: return todo.completed; - case FiltersType.Active: + case FilterType.Active: return !todo.completed; default: return true; } }); - const activeTodosCount = useMemo( - () => todos.filter(todo => !todo.completed).length, - [todos], - ); + const activeTodosCount = todos.filter(todo => !todo.completed).length; - const completedTodos = useMemo( - () => todos.filter(todo => todo.completed), - [todos], - ); + const completedTodos = todos.filter(todo => todo.completed); const completedTodosCount = completedTodos.length; useEffect(() => { setIsLoading(true); getTodos() - .then(data => setTodos(data)) + .then(setTodos) .catch(() => { - setErrorTodos(ErrorTypes.Loading); + setErrorTodos(ErrorType.Loading); }) .finally(() => { setIsLoading(false); @@ -71,7 +65,7 @@ export const App: React.FC = () => { setTodos(currentTodos => [...currentTodos, createdTodo]); } catch (error) { - setErrorTodos(ErrorTypes.Add); + setErrorTodos(ErrorType.Add); throw error; } }; @@ -82,7 +76,7 @@ export const App: React.FC = () => { await deleteTodo(todoId); setTodos(currentTodos => currentTodos.filter(todo => todo.id !== todoId)); } catch (error) { - setErrorTodos(ErrorTypes.Delete); + setErrorTodos(ErrorType.Delete); throw error; } finally { setLoadingTodosIds(currentIds => currentIds.filter(id => id !== todoId)); @@ -106,7 +100,7 @@ export const App: React.FC = () => { }), ); } catch (error) { - setErrorTodos(ErrorTypes.Update); + setErrorTodos(ErrorType.Update); throw error; } finally { setLoadingTodosIds(currentIds => @@ -150,7 +144,7 @@ export const App: React.FC = () => { onUpdateToggleAll={onUpdateToggleAll} /> - {todos?.length && ( + {todos.length > 0 && ( <> = props => { const { error, handleErrorClose } = props; useEffect(() => { - if (error === ErrorTypes.NoErrors) { + if (error === ErrorType.NoErrors) { return; } diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index c6ebc94310..2df581007d 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -1,13 +1,13 @@ import React from 'react'; import cn from 'classnames'; -import { FiltersType } from '../../types/FiltersType'; +import { FilterType } from '../../types/FilterType'; import { Dispatch, SetStateAction } from 'react'; type Props = { activeCount: number | undefined; completedCount: number | undefined; - status: FiltersType; - setStatus: Dispatch>; + status: FilterType; + setStatus: Dispatch>; onDeleteAllCompleted: () => void; }; @@ -27,10 +27,10 @@ export const Footer: React.FC = props => {