diff --git a/src/App.tsx b/src/App.tsx
index 20e932bab..f25ec0ce3 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,93 +1,10 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
-
-export const App: React.FC = () => {
- return (
-
-
-
-
-
-
-
- );
-};
+import { TodosProvider } from './TodosContext';
+import { TodoApp } from './components/TodoApp';
+
+export const App: React.FC = () => (
+
+
+
+);
diff --git a/src/TodosContext.tsx b/src/TodosContext.tsx
new file mode 100644
index 000000000..3a6d985fa
--- /dev/null
+++ b/src/TodosContext.tsx
@@ -0,0 +1,58 @@
+import React, { useState } from 'react';
+import { Option } from './types/Option';
+import { useLocalStorage } from './hooks/useLocalStorage';
+import { Todo } from './types/Todo';
+import { Context } from './types/Context';
+
+const initialValue: Context = {
+ todos: [],
+ setTodos: () => { },
+ filter: Option.All,
+ setFilter: () => { },
+ visibleTodos: [],
+ isToggleCheckedAll: false,
+ setIsToggleCheckedAll: () => {},
+};
+
+export const TodosContext = React.createContext(initialValue);
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export const TodosProvider: React.FC = ({ children }) => {
+ const [todos, setTodos] = useLocalStorage('todos', []);
+ const [filter, setFilter] = useState(Option.All);
+ const [isToggleCheckedAll, setIsToggleCheckedAll]
+ = useState(todos.every(todo => todo.completed));
+
+ const visibleTodos = todos.filter(todo => {
+ switch (filter) {
+ case Option.Active:
+ return !todo.completed;
+
+ case Option.Completed:
+ return todo.completed;
+
+ case Option.All:
+ default:
+ return true;
+ }
+ });
+
+ const context: Context = {
+ todos,
+ setTodos,
+ filter,
+ setFilter,
+ visibleTodos,
+ isToggleCheckedAll,
+ setIsToggleCheckedAll,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
new file mode 100644
index 000000000..b1369df04
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -0,0 +1,76 @@
+import React, { useContext } from 'react';
+import cn from 'classnames';
+import { TodosContext } from '../TodosContext';
+import { Option } from '../types/Option';
+
+export const Footer: React.FC = () => {
+ const {
+ todos,
+ setTodos,
+ filter,
+ setFilter,
+ } = useContext(TodosContext);
+
+ const TotalUncompletedTodos = todos.filter(todo => !todo.completed);
+
+ const isCompletedTodos = todos.some(todo => todo.completed);
+
+ const cleanCompletedTodos = () => {
+ setTodos(todos.filter(todo => !todo.completed));
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
new file mode 100644
index 000000000..77289ce60
--- /dev/null
+++ b/src/components/Header.tsx
@@ -0,0 +1,45 @@
+import React, { useCallback, useContext, useState } from 'react';
+import { TodosContext } from '../TodosContext';
+import { Todo } from '../types/Todo';
+
+export const Header: React.FC = () => {
+ const { todos, setTodos, setIsToggleCheckedAll } = useContext(TodosContext);
+ const [title, setTitle] = useState('');
+
+ const handleSubmit = useCallback((
+ event: React.FormEvent,
+ ) => {
+ event.preventDefault();
+
+ if (!title.trim()) {
+ return;
+ }
+
+ const newTodo: Todo = {
+ id: +new Date(),
+ title: title.trim(),
+ completed: false,
+ };
+
+ setTodos([...todos, newTodo]);
+ setTitle('');
+ setIsToggleCheckedAll(false);
+ }, [setIsToggleCheckedAll, setTodos, title, todos]);
+
+ return (
+
+ );
+};
diff --git a/src/components/Main.tsx b/src/components/Main.tsx
new file mode 100644
index 000000000..ebdbf30b1
--- /dev/null
+++ b/src/components/Main.tsx
@@ -0,0 +1,41 @@
+/* eslint-disable jsx-a11y/control-has-associated-label */
+import React, { useContext } from 'react';
+import { TodosContext } from '../TodosContext';
+import { TodoList } from './TodoList';
+
+export const Main: React.FC = () => {
+ const {
+ todos,
+ setTodos,
+ visibleTodos,
+ isToggleCheckedAll,
+ setIsToggleCheckedAll,
+ } = useContext(TodosContext);
+
+ const checkAllTodos = () => {
+ const newTodos = todos.map(todo => (
+ isToggleCheckedAll
+ ? { ...todo, completed: false }
+ : { ...todo, completed: true }
+ ));
+
+ setTodos(newTodos);
+ setIsToggleCheckedAll(!isToggleCheckedAll);
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/TodoApp.tsx b/src/components/TodoApp.tsx
new file mode 100644
index 000000000..515fe98f5
--- /dev/null
+++ b/src/components/TodoApp.tsx
@@ -0,0 +1,23 @@
+import React, { useContext } from 'react';
+import { Header } from './Header';
+import { Main } from './Main';
+import { Footer } from './Footer';
+import { TodosContext } from '../TodosContext';
+
+export const TodoApp: React.FC = () => {
+ const { todos } = useContext(TodosContext);
+
+ return (
+
+
+
+ {!!todos.length && (
+ <>
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx
new file mode 100644
index 000000000..bf4c170ef
--- /dev/null
+++ b/src/components/TodoItem.tsx
@@ -0,0 +1,121 @@
+import React, {
+ useContext,
+ useState,
+ useRef,
+ useEffect,
+} from 'react';
+import cn from 'classnames';
+import { Todo } from '../types/Todo';
+import { TodosContext } from '../TodosContext';
+
+type Props = {
+ item: Todo,
+};
+export const TodoItem: React.FC = ({ item }) => {
+ const { todos, setTodos, setIsToggleCheckedAll } = useContext(TodosContext);
+ const [isEditing, setIsEditing] = useState(false);
+ const [title, setTitle] = useState(item.title);
+
+ const selectedTodo = useRef(null);
+
+ useEffect(() => {
+ if (selectedTodo.current) {
+ selectedTodo.current.focus();
+ }
+ }, [isEditing]);
+
+ const removeTodo = (id: number) => {
+ const newTodos = todos.filter(todo => todo.id !== id);
+
+ setTodos(newTodos);
+ setIsToggleCheckedAll(newTodos.every(todo => todo.completed));
+ };
+
+ const toggleTodo = (id: number) => {
+ const newTodos = todos.map(todo => (
+ todo.id === id
+ ? { ...todo, completed: !todo.completed }
+ : todo
+ ));
+
+ setTodos(newTodos);
+ setIsToggleCheckedAll(newTodos.every(todo => todo.completed));
+ };
+
+ const handleDoubleClick = (
+ event: React.MouseEvent,
+ ) => {
+ event.preventDefault();
+ setIsEditing(true);
+ };
+
+ const handleBlur = () => {
+ setIsEditing(false);
+
+ if (!title.trim()) {
+ removeTodo(item.id);
+
+ return;
+ }
+
+ setTodos(todos.map(todo => (
+ todo.id === item.id
+ ? { ...todo, title: title.trim() }
+ : todo
+ )));
+ };
+
+ const handleKeyUp = (event: React.KeyboardEvent) => {
+ switch (event.key) {
+ case 'Escape':
+ setIsEditing(false);
+ setTitle(item.title);
+ break;
+
+ case 'Enter':
+ handleBlur();
+ break;
+
+ default:
+ break;
+ }
+ };
+
+ return (
+
+
+ toggleTodo(item.id)}
+ />
+
+
+ setTitle(event.target.value)}
+ ref={selectedTodo}
+ onKeyUp={handleKeyUp}
+ onBlur={handleBlur}
+ />
+
+ );
+};
diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx
new file mode 100644
index 000000000..d512095fc
--- /dev/null
+++ b/src/components/TodoList.tsx
@@ -0,0 +1,16 @@
+/* eslint-disable jsx-a11y/control-has-associated-label */
+import React from 'react';
+import { Todo } from '../types/Todo';
+import { TodoItem } from './TodoItem';
+
+type Props = {
+ items: Todo[],
+};
+
+export const TodoList: React.FC = ({ items }) => (
+
+ {items.map(item => (
+
+ ))}
+
+);
diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
new file mode 100644
index 000000000..d4069dfe6
--- /dev/null
+++ b/src/hooks/useLocalStorage.ts
@@ -0,0 +1,29 @@
+import { useState } from 'react';
+
+export function useLocalStorage(
+ key: string,
+ startValue: T,
+): [T, (v: T) => void] {
+ const [value, setValue] = useState(() => {
+ const data = localStorage.getItem(key);
+
+ if (data === null) {
+ return startValue;
+ }
+
+ try {
+ return JSON.parse(data);
+ } catch (e) {
+ localStorage.removeItem(key);
+
+ return startValue;
+ }
+ });
+
+ const save = (newValue: T) => {
+ localStorage.setItem(key, JSON.stringify(newValue));
+ setValue(newValue);
+ };
+
+ return [value, save];
+}
diff --git a/src/types/Context.ts b/src/types/Context.ts
new file mode 100644
index 000000000..577f4b434
--- /dev/null
+++ b/src/types/Context.ts
@@ -0,0 +1,12 @@
+import { Option } from './Option';
+import { Todo } from './Todo';
+
+export interface Context {
+ todos: Todo[];
+ setTodos: (v: Todo[]) => void;
+ filter: Option;
+ setFilter: (v: Option) => void;
+ visibleTodos: Todo[];
+ isToggleCheckedAll: boolean;
+ setIsToggleCheckedAll: (v: boolean) => void;
+}
diff --git a/src/types/Option.ts b/src/types/Option.ts
new file mode 100644
index 000000000..1e64b6482
--- /dev/null
+++ b/src/types/Option.ts
@@ -0,0 +1,5 @@
+export enum Option {
+ All = 'all',
+ Active = 'active',
+ Completed = 'completed',
+}
diff --git a/src/types/Todo.ts b/src/types/Todo.ts
new file mode 100644
index 000000000..f9e06b381
--- /dev/null
+++ b/src/types/Todo.ts
@@ -0,0 +1,5 @@
+export interface Todo {
+ id: number;
+ title: string;
+ completed: boolean;
+}