From c941e34faccc27f0bafd904aac4c4dbcbbab8188 Mon Sep 17 00:00:00 2001 From: Maksym Date: Sun, 21 Jun 2026 18:40:41 +0300 Subject: [PATCH 1/3] solution --- src/App.tsx | 297 ++++++++++++++++++++++------------------------- src/api/todos.ts | 2 +- 2 files changed, 139 insertions(+), 160 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index bf9c75980..de57e9a6b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,54 @@ +/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { UserWarning } from './UserWarning'; -import { USER_ID } from './api/todos'; +import { getTodos, USER_ID } from './api/todos'; +import { Todo } from './types/Todo'; export const App: React.FC = () => { + const [todos, setTodos] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [errorMessage, setErrorMessage] = useState(''); + const [filter, setFilter] = useState('all'); + if (!USER_ID) { return ; } + useEffect(() => { + const fetchTodos = async () => { + setIsLoading(true); + + try { + const data = await getTodos(); + + setTodos(data); + } catch (error) { + setErrorMessage('Unable to load todos'); + setTimeout(() => { + setErrorMessage(''); + }, 3000); + } finally { + setIsLoading(false); + } + }; + + fetchTodos(); + }, []); + + const visibleTodos = todos.filter(todo => { + if (filter === 'completed') { + return todo.completed; + } + + if (filter === 'active') { + return !todo.completed; + } + + return true; + }); + return (

todos

@@ -33,171 +73,110 @@ export const App: React.FC = () => { -
- {/* This is a completed todo */} -
- - - - Completed Todo - - - {/* Remove button appears only on hover */} - - - {/* overlay will cover the todo while it is being deleted or updated */} -
-
-
-
-
- - {/* This todo is an active todo */} -
- - - - Not Completed Todo + {todos.length > 0 && ( +
+ {visibleTodos.map(todo => ( +
+ + + + {todo.title} + + + +
+
+
+
+
+ ))} +
+ )} + + {todos.length > 0 && ( +
+ + {todos.filter(todo => !todo.completed).length} items left - -
-
-
-
-
- - {/* This todo is being edited */} -
- - - {/* This form is shown instead of the title and remove button */} -
- -
- -
-
-
-
-
- - {/* This todo is in loadind state */} -
- - - - Todo is being saved now - - - - - {/* 'is-active' class puts this modal on top of the todo */} -
-
-
-
-
-
- - {/* Hide the footer if there are no todos */} - + Clear completed + + + )}
- {/* DON'T use conditional rendering to hide the notification */} - {/* Add the 'hidden' class to hide the message smoothly */}
-
); diff --git a/src/api/todos.ts b/src/api/todos.ts index 8a0b90aa0..2f0dc57dd 100644 --- a/src/api/todos.ts +++ b/src/api/todos.ts @@ -1,7 +1,7 @@ import { Todo } from '../types/Todo'; import { client } from '../utils/fetchClient'; -export const USER_ID = 0; +export const USER_ID = 4327; export const getTodos = () => { return client.get(`/todos?userId=${USER_ID}`); From 97182b3fdd10bb86c2ebb30732f6947e05f46e58 Mon Sep 17 00:00:00 2001 From: Maksym Date: Sun, 21 Jun 2026 20:10:01 +0300 Subject: [PATCH 2/3] app splitted into smaller components --- src/App.tsx | 133 +++------------------------ src/components/ErrorNotification.tsx | 24 +++++ src/components/Footer.tsx | 64 +++++++++++++ src/components/Header.tsx | 20 ++++ src/components/TodoItem.tsx | 37 ++++++++ src/components/TodoList.tsx | 16 ++++ 6 files changed, 173 insertions(+), 121 deletions(-) create mode 100644 src/components/ErrorNotification.tsx create mode 100644 src/components/Footer.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/TodoItem.tsx create mode 100644 src/components/TodoList.tsx diff --git a/src/App.tsx b/src/App.tsx index de57e9a6b..edd6a6d89 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,10 +5,14 @@ import React, { useEffect, useState } from 'react'; import { UserWarning } from './UserWarning'; import { getTodos, USER_ID } from './api/todos'; import { Todo } from './types/Todo'; +import { ErrorNotification } from './components/ErrorNotification'; +import { Header } from './components/Header'; +import { Footer } from './components/Footer'; +import { TodoList } from './components/TodoList'; export const App: React.FC = () => { const [todos, setTodos] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const [, setIsLoading] = useState(true); const [errorMessage, setErrorMessage] = useState(''); const [filter, setFilter] = useState('all'); @@ -52,132 +56,19 @@ export const App: React.FC = () => { return (

todos

+
-
- {/* this button should have `active` class only if all todos are completed */} -
- - {todos.length > 0 && ( -
- {visibleTodos.map(todo => ( -
- - - - {todo.title} - - - -
-
-
-
-
- ))} -
- )} + {todos.length > 0 && } {todos.length > 0 && ( - +
)}
- -
-
+
); }; diff --git a/src/components/ErrorNotification.tsx b/src/components/ErrorNotification.tsx new file mode 100644 index 000000000..bfa7dab9b --- /dev/null +++ b/src/components/ErrorNotification.tsx @@ -0,0 +1,24 @@ +interface Props { + errorMessage: string; + setErrorMessage: (value: string) => void; +} + +export const ErrorNotification: React.FC = ({ + errorMessage, + setErrorMessage, +}) => { + return ( +
+
+ ); +}; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 000000000..d075e0b5e --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,64 @@ +import { Todo } from '../types/Todo'; + +interface Props { + todos: Todo[]; + filter: string; + setFilter: (filter: string) => void; +} + +export const Footer: React.FC = ({ todos, filter, setFilter }) => { + return ( + + ); +}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 000000000..5bdb0f85c --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,20 @@ +export const Header: React.FC = () => { + return ( +
+
+ ); +}; diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx new file mode 100644 index 000000000..8c3db3031 --- /dev/null +++ b/src/components/TodoItem.tsx @@ -0,0 +1,37 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +import { Todo } from '../types/Todo'; + +interface Props { + todo: Todo; +} + +export const TodoItem: React.FC = ({ todo }) => { + return ( +
+ + + + {todo.title} + + + +
+
+
+
+
+ ); +}; diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx new file mode 100644 index 000000000..6d143f62e --- /dev/null +++ b/src/components/TodoList.tsx @@ -0,0 +1,16 @@ +import { Todo } from '../types/Todo'; +import { TodoItem } from './TodoItem'; + +interface Props { + visibleTodos: Todo[]; +} + +export const TodoList: React.FC = ({ visibleTodos }) => { + return ( +
+ {visibleTodos.map(todo => ( + + ))} +
+ ); +}; From 60f58e5730ca3348d2cf1faa2d927cefd26bb65c Mon Sep 17 00:00:00 2001 From: Maksym Date: Mon, 22 Jun 2026 14:40:11 +0300 Subject: [PATCH 3/3] added requested changes --- src/App.tsx | 6 ++-- src/api/todos.ts | 2 +- src/components/Footer.tsx | 71 +++++++++++++++++-------------------- src/components/TodoItem.tsx | 2 +- src/components/TodoList.tsx | 2 +- src/types/Todo.ts | 6 ---- src/types/Types.ts | 16 +++++++++ 7 files changed, 55 insertions(+), 50 deletions(-) delete mode 100644 src/types/Todo.ts create mode 100644 src/types/Types.ts diff --git a/src/App.tsx b/src/App.tsx index edd6a6d89..a3984dfe6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import { UserWarning } from './UserWarning'; import { getTodos, USER_ID } from './api/todos'; -import { Todo } from './types/Todo'; +import { ErrorMessage, FilterType, Todo } from './types/Types'; import { ErrorNotification } from './components/ErrorNotification'; import { Header } from './components/Header'; import { Footer } from './components/Footer'; @@ -14,7 +14,7 @@ export const App: React.FC = () => { const [todos, setTodos] = useState([]); const [, setIsLoading] = useState(true); const [errorMessage, setErrorMessage] = useState(''); - const [filter, setFilter] = useState('all'); + const [filter, setFilter] = useState(FilterType.ALL); if (!USER_ID) { return ; @@ -29,7 +29,7 @@ export const App: React.FC = () => { setTodos(data); } catch (error) { - setErrorMessage('Unable to load todos'); + setErrorMessage(ErrorMessage.LOAD); setTimeout(() => { setErrorMessage(''); }, 3000); diff --git a/src/api/todos.ts b/src/api/todos.ts index 2f0dc57dd..20046ba7b 100644 --- a/src/api/todos.ts +++ b/src/api/todos.ts @@ -1,4 +1,4 @@ -import { Todo } from '../types/Todo'; +import { Todo } from '../types/Types'; import { client } from '../utils/fetchClient'; export const USER_ID = 4327; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index d075e0b5e..f97512671 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,12 +1,28 @@ -import { Todo } from '../types/Todo'; +import { FilterType, Todo } from '../types/Types'; interface Props { todos: Todo[]; - filter: string; - setFilter: (filter: string) => void; + filter: FilterType; + setFilter: (filter: FilterType) => void; } export const Footer: React.FC = ({ todos, filter, setFilter }) => { + const filterLinks = [ + { type: FilterType.ALL, title: 'All', cy: 'FilterLinkAll', href: '#/' }, + { + type: FilterType.ACTIVE, + title: 'Active', + cy: 'FilterLinkActive', + href: '#/active', + }, + { + type: FilterType.COMPLETED, + title: 'Completed', + cy: 'FilterLinkCompleted', + href: '#/completed', + }, + ]; + return (