diff --git a/src/App.tsx b/src/App.tsx index bf9c75980..a3984dfe6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,204 +1,74 @@ +/* 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 { ErrorMessage, FilterType, Todo } from './types/Types'; +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 [, setIsLoading] = useState(true); + const [errorMessage, setErrorMessage] = useState(''); + const [filter, setFilter] = useState(FilterType.ALL); + if (!USER_ID) { return ; } - return ( -
-

todos

- -
-
- {/* this button should have `active` class only if all todos are completed */} -
- -
- {/* 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 */} -
- + useEffect(() => { + const fetchTodos = async () => { + setIsLoading(true); - - Not Completed Todo - - + try { + const data = await getTodos(); -
-
-
-
-
+ setTodos(data); + } catch (error) { + setErrorMessage(ErrorMessage.LOAD); + setTimeout(() => { + setErrorMessage(''); + }, 3000); + } finally { + setIsLoading(false); + } + }; - {/* This todo is being edited */} -
- + fetchTodos(); + }, []); - {/* This form is shown instead of the title and remove button */} -
- -
+ const visibleTodos = todos.filter(todo => { + if (filter === 'completed') { + return todo.completed; + } -
-
-
-
-
+ if (filter === 'active') { + return !todo.completed; + } - {/* This todo is in loadind state */} -
- + return true; + }); - - Todo is being saved now - - - - - {/* 'is-active' class puts this modal on top of the todo */} -
-
-
-
-
-
- - {/* Hide the footer if there are no todos */} -
- - 3 items left - - - {/* Active link should have the 'selected' class */} - + return ( +
+

todos

+
- {/* this button should be disabled if there are no completed todos */} - -
-
+
+ {todos.length > 0 && } - {/* 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..20046ba7b 100644 --- a/src/api/todos.ts +++ b/src/api/todos.ts @@ -1,7 +1,7 @@ -import { Todo } from '../types/Todo'; +import { Todo } from '../types/Types'; 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}`); 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..f97512671 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,59 @@ +import { FilterType, Todo } from '../types/Types'; + +interface Props { + todos: Todo[]; + 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 ( + + ); +}; 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..35c732665 --- /dev/null +++ b/src/components/TodoItem.tsx @@ -0,0 +1,37 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +import { Todo } from '../types/Types'; + +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..4f6b8c5f6 --- /dev/null +++ b/src/components/TodoList.tsx @@ -0,0 +1,16 @@ +import { Todo } from '../types/Types'; +import { TodoItem } from './TodoItem'; + +interface Props { + visibleTodos: Todo[]; +} + +export const TodoList: React.FC = ({ visibleTodos }) => { + return ( +
+ {visibleTodos.map(todo => ( + + ))} +
+ ); +}; diff --git a/src/types/Todo.ts b/src/types/Todo.ts deleted file mode 100644 index 3f52a5fdd..000000000 --- a/src/types/Todo.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface Todo { - id: number; - userId: number; - title: string; - completed: boolean; -} diff --git a/src/types/Types.ts b/src/types/Types.ts new file mode 100644 index 000000000..21592abe6 --- /dev/null +++ b/src/types/Types.ts @@ -0,0 +1,16 @@ +export interface Todo { + id: number; + userId: number; + title: string; + completed: boolean; +} + +export enum FilterType { + ALL = 'all', + ACTIVE = 'active', + COMPLETED = 'completed', +} + +export enum ErrorMessage { + LOAD = 'Unable to load todos', +}