diff --git a/src/App.tsx b/src/App.tsx
index a399287bd..4ac424509 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,156 +1,20 @@
-/* eslint-disable jsx-a11y/control-has-associated-label */
-import React from 'react';
+import React, { useContext } from 'react';
+import { TodoList } from './components/TodoList';
+import { TodoHeader } from './components/TodoHeader';
+import { TodoFooter } from './components/TodoFooter';
+import { TodosContext } from './context';
export const App: React.FC = () => {
+ const { todos } = useContext(TodosContext);
+
return (
todos
-
- {/* this button should have `active` class only if all todos are completed */}
-
-
- {/* Add a todo on form submit */}
-
-
-
-
-
- {/* Hide the footer if there are no todos */}
-
-
- 3 items left
-
-
- {/* Active link should have the 'selected' class */}
-
-
- All
-
-
-
- Active
-
-
-
- Completed
-
-
-
- {/* this button should be disabled if there are no completed todos */}
-
- Clear completed
-
-
+
+
+ {todos.length > 0 &&
}
);
diff --git a/src/components/TodoFooter/TodoFooter.tsx b/src/components/TodoFooter/TodoFooter.tsx
new file mode 100644
index 000000000..461abd951
--- /dev/null
+++ b/src/components/TodoFooter/TodoFooter.tsx
@@ -0,0 +1,57 @@
+import React, { useContext } from 'react';
+import { TodosContext } from '../../context';
+import { Filter } from '../../enums/Enums';
+import classNames from 'classnames';
+
+export const TodoFooter: React.FC = () => {
+ const { todos, activeTodos, filter, setFilter, deleteHandler } =
+ useContext(TodosContext);
+
+ const deleteCompleted = () => {
+ const idsToDelete: number[] = [];
+
+ todos.forEach(todo => {
+ const { completed, id } = todo;
+
+ if (completed) {
+ idsToDelete.push(id);
+ }
+ });
+
+ deleteHandler(idsToDelete);
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/TodoFooter/index.ts b/src/components/TodoFooter/index.ts
new file mode 100644
index 000000000..544d07114
--- /dev/null
+++ b/src/components/TodoFooter/index.ts
@@ -0,0 +1 @@
+export * from './TodoFooter';
diff --git a/src/components/TodoHeader/TodoHeader.tsx b/src/components/TodoHeader/TodoHeader.tsx
new file mode 100644
index 000000000..cedb26993
--- /dev/null
+++ b/src/components/TodoHeader/TodoHeader.tsx
@@ -0,0 +1,59 @@
+import React, { useContext } from 'react';
+import { TodosContext } from '../../context';
+import classNames from 'classnames';
+import { myLocalStorage } from '../../localStorage';
+import { Names } from '../../enums/Enums';
+
+export const TodoHeader: React.FC = () => {
+ const {
+ todos,
+ activeTodos,
+ value,
+ headerInputRef,
+ setTodos,
+ setValue,
+ onSubmit,
+ } = useContext(TodosContext);
+
+ const toogleAll = () => {
+ const updatedTodos = todos.map(todo => {
+ const { completed } = todo;
+
+ if (activeTodos === 0 && completed) {
+ return { ...todo, completed: !completed };
+ }
+
+ return { ...todo, completed: true };
+ });
+
+ setTodos(updatedTodos);
+ myLocalStorage.setItem(Names.todos, JSON.stringify(updatedTodos));
+ };
+
+ return (
+
+ {todos.length > 0 && (
+
+ )}
+
+
+
+ );
+};
diff --git a/src/components/TodoHeader/index.ts b/src/components/TodoHeader/index.ts
new file mode 100644
index 000000000..c4db4bc40
--- /dev/null
+++ b/src/components/TodoHeader/index.ts
@@ -0,0 +1 @@
+export * from './TodoHeader';
diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx
new file mode 100644
index 000000000..146647958
--- /dev/null
+++ b/src/components/TodoItem/TodoItem.tsx
@@ -0,0 +1,86 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+import classNames from 'classnames';
+import React from 'react';
+
+interface Props {
+ id: number;
+ title: string;
+ editableTitle: string;
+ isTodoChecked: boolean;
+ editableTodoById: number;
+ todoInputRef: React.MutableRefObject;
+ setEditableTitle: (value: string) => void;
+ editHandler: (id: number, title: string) => void;
+ deleteHandler: (ids: number[]) => void;
+ toogleHandler: (id: number) => void;
+ onSubmit: (event: React.FormEvent, id: number) => void;
+}
+
+export const TodoItem: React.FC = React.memo(
+ ({
+ id,
+ title,
+ editableTitle,
+ editableTodoById,
+ isTodoChecked,
+ todoInputRef,
+ setEditableTitle,
+ editHandler,
+ deleteHandler,
+ toogleHandler,
+ onSubmit,
+ }) => {
+ return (
+
+
+ toogleHandler(id)}
+ checked={isTodoChecked}
+ />
+
+
+ {editableTodoById === id ? (
+
+ ) : (
+ editHandler(id, title)}
+ >
+ {title || 'Todo is being saved now'}
+
+ )}
+
+ {editableTodoById !== id && (
+ deleteHandler([id])}
+ >
+ ×
+
+ )}
+
+ );
+ },
+);
+
+TodoItem.displayName = 'TodoItem';
diff --git a/src/components/TodoItem/index.ts b/src/components/TodoItem/index.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx
new file mode 100644
index 000000000..d32b09927
--- /dev/null
+++ b/src/components/TodoList/TodoList.tsx
@@ -0,0 +1,95 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+import React, { useContext, useEffect, useRef, useState } from 'react';
+import { TodosContext } from '../../context';
+import { Filter, Names } from '../../enums/Enums';
+import { myLocalStorage } from '../../localStorage';
+import { TodoItem } from '../TodoItem/TodoItem';
+
+export const TodoList: React.FC = () => {
+ const { todos, filter, setTodos, toogleHandler, deleteHandler } =
+ useContext(TodosContext);
+
+ const [editableTodoById, setEditableTodoById] = useState(0);
+ const [editableTitle, setEditableTitle] = useState('');
+ const todoInputRef = useRef(null);
+
+ const filteredTodos = todos.filter(todo => {
+ switch (filter) {
+ case Filter.active:
+ return !todo.completed;
+ case Filter.completed:
+ return todo.completed;
+ default:
+ return todo;
+ }
+ });
+
+ const editHandler = (id: number, value: string) => {
+ setEditableTodoById(id);
+ setEditableTitle(value);
+ };
+
+ const onSubmit = (event: React.FormEvent, id: number) => {
+ event.preventDefault();
+ const trimmedTitle = editableTitle.trim();
+
+ if (trimmedTitle.length > 0) {
+ const updatedTodos = todos.map(todo => {
+ if (todo.id === id) {
+ return { ...todo, title: trimmedTitle };
+ }
+
+ return todo;
+ });
+
+ setTodos(updatedTodos);
+ myLocalStorage.setItem(Names.todos, JSON.stringify(updatedTodos));
+ } else {
+ deleteHandler([id]);
+ }
+
+ setEditableTodoById(0);
+ setEditableTitle('');
+ };
+
+ const onKeyUpHandler = (key: React.KeyboardEvent) => {
+ if (key.code === 'Escape') {
+ setEditableTodoById(0);
+ setEditableTitle('');
+ }
+ };
+
+ useEffect(() => {
+ todoInputRef.current?.focus();
+ }, [editableTodoById]);
+
+ return (
+ onKeyUpHandler(event)}
+ >
+ {filteredTodos.map(todo => {
+ const { id, title } = todo;
+ const isTodoChecked = todo.completed;
+
+ return (
+
+ );
+ })}
+
+ );
+};
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/context/TodosContext.tsx b/src/context/TodosContext.tsx
new file mode 100644
index 000000000..9abfed70c
--- /dev/null
+++ b/src/context/TodosContext.tsx
@@ -0,0 +1,130 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+import React, {
+ createContext,
+ memo,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import { Todo } from '../types/Todo';
+import { myLocalStorage } from '../localStorage';
+import { Names } from '../enums/Enums';
+
+interface TodosContextType {
+ todos: Todo[];
+ activeTodos: number;
+ value: string;
+ filter: string;
+ headerInputRef: React.MutableRefObject;
+ setTodos: (todos: Todo[]) => void;
+ setValue: (value: string) => void;
+ onSubmit: (event: React.FormEvent) => void;
+ setFilter: (value: string) => void;
+ toogleHandler: (id: number) => void;
+ deleteHandler: (ids: number[]) => void;
+}
+
+export const TodosContext = createContext({
+ todos: [],
+ activeTodos: 0,
+ value: '',
+ filter: '',
+ headerInputRef: {
+ current: null,
+ },
+ setTodos: () => {},
+ setValue: () => {},
+ onSubmit: () => {},
+ setFilter: () => {},
+ toogleHandler: () => {},
+ deleteHandler: () => {},
+});
+
+interface Props {
+ children: React.ReactNode;
+}
+
+export const TodosProvider: React.FC = memo(({ children }) => {
+ const [todos, setTodos] = useState([]);
+ const [value, setValue] = useState('');
+ const [filter, setFilter] = useState('All');
+ const headerInputRef = useRef(null);
+
+ const onSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+ const todoId = +new Date();
+ const todo = {
+ id: todoId,
+ title: value.trim(),
+ completed: false,
+ };
+
+ myLocalStorage.setItem(Names.todos, JSON.stringify([...todos, todo]));
+ setTodos(currentTodos => [...currentTodos, todo]);
+
+ setValue('');
+ };
+
+ const toogleHandler = (id: number) => {
+ const updatedTodos = todos.map(todo => {
+ const { completed } = todo;
+
+ if (todo.id === id) {
+ return { ...todo, completed: !completed };
+ }
+
+ return todo;
+ });
+
+ setTodos(updatedTodos);
+ myLocalStorage.setItem(Names.todos, JSON.stringify(updatedTodos));
+ };
+
+ const deleteHandler = (ids: number[]) => {
+ const updatedTodos = todos.filter(todo => !ids.includes(todo.id));
+
+ setTodos(updatedTodos);
+ myLocalStorage.setItem(Names.todos, JSON.stringify(updatedTodos));
+ headerInputRef.current?.focus();
+ };
+
+ const activeTodos = todos.filter(_todo => !_todo.completed).length;
+
+ useEffect(() => {
+ const getTodos = myLocalStorage.getItem(Names.todos);
+
+ if (!getTodos) {
+ myLocalStorage.setItem(Names.todos, JSON.stringify([]));
+ } else {
+ setTodos(JSON.parse(getTodos as string));
+ }
+
+ headerInputRef.current?.focus();
+ }, []);
+
+ const contextValue = useMemo(
+ () => ({
+ todos,
+ activeTodos,
+ value,
+ filter,
+ headerInputRef,
+ setTodos,
+ setValue,
+ onSubmit,
+ setFilter,
+ toogleHandler,
+ deleteHandler,
+ }),
+ [todos, value, filter],
+ );
+
+ return (
+
+ {children}
+
+ );
+});
+
+TodosProvider.displayName = 'TodosProvider';
diff --git a/src/context/index.ts b/src/context/index.ts
new file mode 100644
index 000000000..b9ab89200
--- /dev/null
+++ b/src/context/index.ts
@@ -0,0 +1 @@
+export * from './TodosContext';
diff --git a/src/enums/Enums.ts b/src/enums/Enums.ts
new file mode 100644
index 000000000..eef71c709
--- /dev/null
+++ b/src/enums/Enums.ts
@@ -0,0 +1,9 @@
+export enum Filter {
+ all = 'All',
+ active = 'Active',
+ completed = 'Completed',
+}
+
+export enum Names {
+ todos = 'todos',
+}
diff --git a/src/index.tsx b/src/index.tsx
index a9689cb38..a9b8be212 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,11 +1,17 @@
import { createRoot } from 'react-dom/client';
-import './styles/index.css';
-import './styles/todo-list.css';
-import './styles/filters.css';
+import './styles/filter.scss';
+import './styles/index.scss';
+import './styles/todo.scss';
+import './styles/todoapp.scss';
import { App } from './App';
+import { TodosProvider } from './context';
const container = document.getElementById('root') as HTMLDivElement;
-createRoot(container).render( );
+createRoot(container).render(
+
+
+ ,
+);
diff --git a/src/localStorage/index.ts b/src/localStorage/index.ts
new file mode 100644
index 000000000..3fcbe130e
--- /dev/null
+++ b/src/localStorage/index.ts
@@ -0,0 +1 @@
+export * from './localStorage';
diff --git a/src/localStorage/localStorage.ts b/src/localStorage/localStorage.ts
new file mode 100644
index 000000000..92aeed0d3
--- /dev/null
+++ b/src/localStorage/localStorage.ts
@@ -0,0 +1 @@
+export const myLocalStorage = window.localStorage;
diff --git a/src/styles/todo.scss b/src/styles/todo.scss
index 4576af434..fd5f6aa10 100644
--- a/src/styles/todo.scss
+++ b/src/styles/todo.scss
@@ -73,6 +73,7 @@
&__title-field {
width: 100%;
padding: 11px 14px;
+ box-sizing: border-box;
font-size: inherit;
line-height: inherit;
diff --git a/src/styles/todoapp.scss b/src/styles/todoapp.scss
index e289a9458..cf79aa03b 100644
--- a/src/styles/todoapp.scss
+++ b/src/styles/todoapp.scss
@@ -58,6 +58,7 @@
&__new-todo {
width: 100%;
padding: 16px 16px 16px 60px;
+ box-sizing: border-box;
font-size: 24px;
line-height: 1.4em;
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;
+}