diff --git a/src/App.tsx b/src/App.tsx
deleted file mode 100644
index a399287bd..000000000
--- a/src/App.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-/* eslint-disable jsx-a11y/control-has-associated-label */
-import React from 'react';
-
-export const App: React.FC = () => {
- 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
-
-
-
-
- );
-};
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
new file mode 100644
index 000000000..ba38e62c8
--- /dev/null
+++ b/src/components/App/App.tsx
@@ -0,0 +1,33 @@
+/* eslint-disable jsx-a11y/control-has-associated-label */
+import React, { useEffect } from 'react';
+import { Header } from '../Header/Header';
+import { TodoList } from '../TodoList/TodoList';
+import { Footer } from '../Footer/Footer';
+import { useTodoContext } from '../../context/TodoContext'
+import { getTodos } from '../../storage/localStorage';
+
+
+export const App: React.FC = () => {
+ const { todos, setTodos } = useTodoContext();
+
+ useEffect(() => {
+ setTodos(getTodos());
+ }, [setTodos]);
+
+ return (
+
+
todos
+
+
+
+ {todos.length > 0 && (
+ <>
+
+
+ >
+ )}
+
+
+
+ );
+};
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
new file mode 100644
index 000000000..988b8e24c
--- /dev/null
+++ b/src/components/Footer/Footer.tsx
@@ -0,0 +1,60 @@
+import { useTodoContext } from '../../context/TodoContext';
+import { FilterType } from '../../types/FilterTypes';
+import { FilterTypes } from '../../types/FilterTypes';
+import classNames from 'classnames';
+
+export const Footer: React.FC = () => {
+ const { todos, setTodos, filter, setFilter } = useTodoContext();
+
+ const activeTodosCount = todos.filter(todo => !todo.completed).length;
+
+ const handleFilterChange = (newFilter: FilterType) => {
+ setFilter(newFilter);
+ };
+
+ const clearCompleted = () => {
+ const todosCompleted = todos.filter(todo => !todo.completed);
+
+ setTodos(todosCompleted);
+ localStorage.setItem('todos', JSON.stringify(todosCompleted));
+
+ const inputField = document.querySelector(".todoapp__new-todo") as HTMLInputElement;
+ inputField!.focus();
+ };
+
+ return (
+
+ )
+}
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
new file mode 100644
index 000000000..77c064cb4
--- /dev/null
+++ b/src/components/Header/Header.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { useTodoContext } from '../../context/TodoContext';
+import { Todo } from '../../types/Todo';
+import classNames from 'classnames';
+
+
+export const Header: React.FC = () => {
+ const { todos, setTodos, title, setTitle } = useTodoContext();
+
+ const addTodo = (text: string) => {
+ const newTodo: Todo = { id: Date.now(), title: text, completed: false};
+
+ setTodos([...todos, newTodo]);
+ localStorage.setItem('todos', JSON.stringify([...todos, newTodo]));
+ }
+
+ const handleSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+
+ if (!title.trim()) {
+ return;
+ }
+
+ addTodo(title.trim());
+ setTitle('');
+ };
+
+ const toogleAllTodos = () => {
+ const allCompleted = todos.every(todo => todo.completed);
+
+ const updatedTodos = todos.map(todo => ({
+ ...todo,
+ completed: !allCompleted,
+ }));
+
+ setTodos(updatedTodos);
+ localStorage.setItem('todos', JSON.stringify(updatedTodos));
+ }
+
+ return (
+
+ {/* this button should have `active` class only if all todos are completed */}
+ {todos.length > 0 && (
+ todo.completed),
+ })}
+ data-cy="ToggleAllButton"
+ onClick={toogleAllTodos}
+ />
+ )}
+ {/* Add a todo on form submit */}
+
+
+ )
+}
diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx
new file mode 100644
index 000000000..f89b07000
--- /dev/null
+++ b/src/components/TodoItem/TodoItem.tsx
@@ -0,0 +1,132 @@
+import React, { useState, useRef } from 'react';
+import { Todo } from '../../types/Todo';
+import classNames from 'classnames';
+import { useTodoContext } from '../../context/TodoContext';
+
+interface Props {
+ todo: Todo;
+}
+
+export const TodoItem: React.FC = ({ todo }) => {
+ const { todos, setTodos, newTitle, setNewTitle } = useTodoContext();
+ const [isEditForm, setIsEditForm] = useState(false);
+ const editInputRef = useRef(null);
+
+ const handleDoubleClick = () => {
+ setIsEditForm(true);
+ setNewTitle(todo.title);
+ setTimeout(() => {
+ editInputRef.current?.focus();
+ }, 0);
+ }
+
+ const deleteTodo = (id: number) => {
+ const filteredTodos = todos.filter(t => t.id !== id);
+
+ setTodos(filteredTodos);
+ localStorage.setItem('todos', JSON.stringify(filteredTodos));
+
+ const inputField = document.querySelector(".todoapp__new-todo") as HTMLInputElement;
+ inputField!.focus();
+ }
+
+ const editTodo = (title: string, id: number) => {
+ const updatedTodos = todos.map(t => t.id === id ? { ...t, title } : t);
+
+ setTodos(updatedTodos);
+ localStorage.setItem('todos', JSON.stringify(updatedTodos));
+ };
+
+ const handleKeyUp = (e: React.KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ setIsEditForm(false);
+ }
+ };
+
+ const editTitle = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const form = new FormData(e.currentTarget);
+ const newEditTitle = form.get('TodoTitleField') as string;
+
+ if (!newEditTitle.trim()) {
+ deleteTodo(todo.id);
+ } else if (newEditTitle !== todo.title) {
+ editTodo(newEditTitle.trim(), todo.id);
+ }
+
+ setTimeout(() => {
+ setIsEditForm(false);
+ }, 100);
+ };
+
+ const handleTitleBlur = (e: React.ChangeEvent) => {
+ const newTitleBlur = e.target.value.trim();
+
+ if (!newTitleBlur) {
+ deleteTodo(todo.id);
+ return;
+ }
+
+ if (newTitleBlur !== todo.title) {
+ editTodo(newTitleBlur, todo.id);
+ }
+
+ setIsEditForm(false);
+ };
+
+ const toggleTodoStatus = (id: number) => {
+ const updatedTodos = todos.map(t =>
+ t.id === id ? { ...t, completed: !t.completed } : t
+ );
+
+ setTodos(updatedTodos);
+ localStorage.setItem('todos', JSON.stringify(updatedTodos));
+ }
+
+ return (
+
+
+ toggleTodoStatus(todo.id)}
+ />
+
+
+ {isEditForm ? (
+
+ ) : (
+
+ {todo.title}
+
+ )}
+
+
+ {/* Remove button appears only on hover */}
+ {!isEditForm && (
+ deleteTodo(todo.id)} >
+ ×
+
+ )}
+
+ )
+}
diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx
new file mode 100644
index 000000000..43d32956e
--- /dev/null
+++ b/src/components/TodoList/TodoList.tsx
@@ -0,0 +1,33 @@
+import { useTodoContext } from "../../context/TodoContext"
+import { TodoItem } from "../TodoItem/TodoItem"
+
+
+enum FilterT {
+ All = 'all',
+ Active = 'active',
+ Completed = 'completed',
+}
+
+export const TodoList: React.FC = () => {
+ const { todos, filter } = useTodoContext();
+
+ const filteredTodos = todos.filter(todo => {
+ switch (filter) {
+ case FilterT.Active:
+ return !todo.completed;
+ case FilterT.Completed:
+ return todo.completed;
+ case FilterT.All:
+ default:
+ return true;
+ }
+ });
+
+ return (
+
+ {filteredTodos.map(todo => (
+
+ ))}
+
+ )
+}
diff --git a/src/context/TodoContext.tsx b/src/context/TodoContext.tsx
new file mode 100644
index 000000000..0500c4aa3
--- /dev/null
+++ b/src/context/TodoContext.tsx
@@ -0,0 +1,57 @@
+import React, { useContext, useState } from 'react';
+import { Todo } from '../types/Todo';
+import { FilterType } from '../types/FilterTypes';
+
+interface TodoContextType {
+ todos: Todo[];
+ setTodos: (todos: Todo[]) => void;
+ title: string;
+ setTitle: (title: string) => void;
+ filter: 'all' | 'active' | 'completed';
+ setFilter: (filter: 'all' | 'active' | 'completed') => void;
+ newTitle: string;
+ setNewTitle: (editTitle: string) => void;
+}
+
+export const TodoContext = React.createContext({
+ todos: [],
+ setTodos: () => {},
+ title: '',
+ setTitle: () => {},
+ filter: 'all',
+ setFilter: () => {},
+ newTitle: '',
+ setNewTitle: () => {},
+});
+
+type Props = {
+ children: React.ReactNode;
+}
+
+export const TodoProvider: React.FC = ({ children }) => {
+ const [ todos, setTodos ] = useState([]);
+ const [ title, setTitle ] = useState('');
+ const [ filter, setFilter ] = useState('all');
+ const [ newTitle, setNewTitle ] = useState('');
+
+ const value: TodoContextType = {
+ todos,
+ setTodos,
+ title,
+ setTitle,
+ filter,
+ setFilter,
+ newTitle,
+ setNewTitle,
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const useTodoContext = (): TodoContextType => {
+ return useContext(TodoContext)
+}
diff --git a/src/index.tsx b/src/index.tsx
index a9689cb38..3652baf3c 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/todo-list.scss';
+import './styles/filters.scss';
+import './styles/todoapp.scss';
-import { App } from './App';
+import { App } from './components/App/App';
+import { TodoProvider } from './context/TodoContext';
const container = document.getElementById('root') as HTMLDivElement;
-createRoot(container).render( );
+createRoot(container).render(
+
+
+
+);
diff --git a/src/storage/localStorage.ts b/src/storage/localStorage.ts
new file mode 100644
index 000000000..ede2f037d
--- /dev/null
+++ b/src/storage/localStorage.ts
@@ -0,0 +1,15 @@
+const STORAGE_KEY = 'todos';
+
+export const getTodos = () => {
+ const value = localStorage.getItem(STORAGE_KEY);
+
+ if (!value) {
+ localStorage.setItem('todos', JSON.stringify([]));
+
+ return [];
+ }
+
+ const parsedValue = JSON.parse(value);
+
+ return Array.isArray(parsedValue) ? parsedValue : [];
+}
diff --git a/src/styles/filters.css b/src/styles/filters.scss
similarity index 100%
rename from src/styles/filters.css
rename to src/styles/filters.scss
diff --git a/src/styles/todo-list.css b/src/styles/todo-list.scss
similarity index 99%
rename from src/styles/todo-list.css
rename to src/styles/todo-list.scss
index 4576af434..542362d66 100644
--- a/src/styles/todo-list.css
+++ b/src/styles/todo-list.scss
@@ -71,7 +71,7 @@
}
&__title-field {
- width: 100%;
+ width: 94%;
padding: 11px 14px;
font-size: inherit;
diff --git a/src/styles/todoapp.scss b/src/styles/todoapp.scss
index e289a9458..e3983c143 100644
--- a/src/styles/todoapp.scss
+++ b/src/styles/todoapp.scss
@@ -56,7 +56,7 @@
}
&__new-todo {
- width: 100%;
+ width: 85%;
padding: 16px 16px 16px 60px;
font-size: 24px;
diff --git a/src/types/FilterTypes.ts b/src/types/FilterTypes.ts
new file mode 100644
index 000000000..0985e7081
--- /dev/null
+++ b/src/types/FilterTypes.ts
@@ -0,0 +1,9 @@
+export const FilterTypes = {
+ ALL: 'all',
+ ACTIVE: 'active',
+ COMPLETED: 'completed',
+} as const;
+
+export type FilterType = typeof FilterTypes[keyof typeof FilterTypes];
+
+
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;
+}