diff --git a/src/App.tsx b/src/App.tsx
index a399287bd..c190703e3 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,156 +1,22 @@
-/* eslint-disable jsx-a11y/control-has-associated-label */
-import React from 'react';
+import React, { useContext } from 'react';
+import { Header } from './components/Header/Header';
+import { TodoList } from './components/TodoList/TodoList';
+import { Footer } from './components/Footer/Footer';
+import { TodosContext } from './components/TodosContext/TodosContext';
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
-
-
+ {!!todos.length && }
- {/* this button should be disabled if there are no completed todos */}
-
- Clear completed
-
-
+ {!!todos.length &&
}
);
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
new file mode 100644
index 000000000..25e8b8222
--- /dev/null
+++ b/src/components/Footer/Footer.tsx
@@ -0,0 +1,50 @@
+import React, { useContext, useMemo } from 'react';
+import classNames from 'classnames';
+import { MethodsContext, TodosContext } from '../TodosContext/TodosContext';
+import { TodoStatus } from '../../types/TodoStatus';
+import { countActiveTodo } from '../../utils/countActiveTodo';
+
+export const Footer: React.FC = () => {
+ const { todos, activeFilter, setActiveFilter } = useContext(TodosContext);
+
+ const { clearCompleted } = useContext(MethodsContext);
+
+ const activeTodoCount = useMemo(() => countActiveTodo(todos), [todos]);
+
+ return (
+
+ );
+};
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
new file mode 100644
index 000000000..faeec1fc5
--- /dev/null
+++ b/src/components/Header/Header.tsx
@@ -0,0 +1,58 @@
+import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
+import classNames from 'classnames';
+import { MethodsContext, TodosContext } from '../TodosContext/TodosContext';
+import { countActiveTodo } from '../../utils/countActiveTodo';
+
+export const Header: React.FC = () => {
+ const { todos } = useContext(TodosContext);
+ const { addTodo, toggleAll } = useContext(MethodsContext);
+
+ const [newTitle, setNewTitle] = useState('');
+
+ const titleInput = useRef(null);
+
+ useEffect(() => {
+ titleInput.current?.focus();
+ }, [todos]);
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault();
+
+ const trimmed = newTitle.trim();
+
+ if (trimmed) {
+ addTodo(trimmed);
+ setNewTitle('');
+ titleInput.current?.focus();
+ }
+ }
+
+ const activeTodoCount = useMemo(() => countActiveTodo(todos), [todos]);
+
+ return (
+
+ {!!todos.length && (
+
+ )}
+
+
+
+ );
+};
diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx
new file mode 100644
index 000000000..1dd9bb583
--- /dev/null
+++ b/src/components/TodoItem/TodoItem.tsx
@@ -0,0 +1,100 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+import React, { useContext, useEffect, useRef, useState } from 'react';
+import classNames from 'classnames';
+import { MethodsContext } from '../TodosContext/TodosContext';
+import { Todo } from '../../types/Todo';
+
+type Props = {
+ todo: Todo;
+};
+
+export const TodoItem: React.FC = ({ todo }) => {
+ const { id, title, completed } = todo;
+ const { deleteTodo, toggleTodo, renameTodo } = useContext(MethodsContext);
+
+ const [isEditing, setIsEditing] = useState(false);
+ const [newTitle, setNewTitle] = useState(title);
+
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (isEditing && inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [isEditing]);
+
+ function saveChanges() {
+ const trimmed = newTitle.trim();
+
+ if (trimmed) {
+ renameTodo(todo.id, trimmed);
+ } else {
+ deleteTodo(todo.id);
+ }
+
+ setIsEditing(false);
+ }
+
+ function handleSubmitForm(event: React.FormEvent) {
+ event.preventDefault();
+
+ saveChanges();
+ }
+
+ const handleKeyUp = (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ saveChanges();
+ } else if (event.key === 'Escape') {
+ setIsEditing(false);
+ setNewTitle(title);
+ }
+ };
+
+ return (
+
+
+ toggleTodo(todo.id)}
+ />
+
+
+ {isEditing ? (
+
+ ) : (
+ <>
+ setIsEditing(true)}
+ >
+ {title}
+
+ deleteTodo(id)}
+ >
+ ×
+
+ >
+ )}
+
+ );
+};
diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx
new file mode 100644
index 000000000..fedf104c7
--- /dev/null
+++ b/src/components/TodoList/TodoList.tsx
@@ -0,0 +1,15 @@
+import { useContext } from 'react';
+import { TodoItem } from '../TodoItem/TodoItem';
+import { TodosContext } from '../TodosContext/TodosContext';
+
+export const TodoList: React.FC = () => {
+ const { filtredTodos } = useContext(TodosContext);
+
+ return (
+
+ {filtredTodos.map(todo => (
+
+ ))}
+
+ );
+};
diff --git a/src/components/TodosContext/TodosContext.tsx b/src/components/TodosContext/TodosContext.tsx
new file mode 100644
index 000000000..c4495b734
--- /dev/null
+++ b/src/components/TodosContext/TodosContext.tsx
@@ -0,0 +1,111 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import React, { useMemo, useState } from 'react';
+import { TodoStatus } from '../../types/TodoStatus';
+import { Todo } from '../../types/Todo';
+import { getFiltredTodos } from '../../utils/getFiltredTodos';
+import { useLocalStorage } from '../../hooks/useLocalStorage';
+
+interface Methods {
+ addTodo: (title: string) => void;
+ deleteTodo: (id: number) => void;
+ toggleTodo: (id: number) => void;
+ renameTodo: (id: number, title: string) => void;
+ toggleAll: () => void;
+ clearCompleted: () => void;
+}
+
+export const MethodsContext = React.createContext({
+ addTodo: () => {},
+ deleteTodo: () => {},
+ toggleTodo: () => {},
+ renameTodo: () => {},
+ toggleAll: () => {},
+ clearCompleted: () => {},
+});
+
+type Context = {
+ todos: Todo[];
+ filtredTodos: Todo[];
+ setTodos: (newTodos: Todo[]) => void;
+ activeFilter: TodoStatus;
+ setActiveFilter: (newActiveFilter: TodoStatus) => void;
+};
+
+export const TodosContext = React.createContext({
+ todos: [],
+ filtredTodos: [],
+ setTodos: () => {},
+ activeFilter: TodoStatus.All,
+ setActiveFilter: () => {},
+});
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export const TodosProvider: React.FC = ({ children }) => {
+ const [todos, setTodos] = useLocalStorage('todos', []);
+ const [activeFilter, setActiveFilter] = useState(TodoStatus.All);
+
+ const methods = useMemo(
+ () => ({
+ addTodo(title: string) {
+ const newTodo: Todo = {
+ id: Date.now(),
+ title,
+ completed: false,
+ };
+
+ setTodos([...todos, newTodo]);
+ },
+
+ deleteTodo(id: number) {
+ setTodos(todos.filter(todo => todo.id !== id));
+ },
+
+ toggleTodo(id: number) {
+ setTodos(
+ todos.map(todo =>
+ todo.id === id ? { ...todo, completed: !todo.completed } : todo,
+ ),
+ );
+ },
+
+ renameTodo(id: number, title: string) {
+ setTodos(
+ todos.map(todo => (todo.id === id ? { ...todo, title } : todo)),
+ );
+ },
+
+ toggleAll() {
+ const allCompleted = todos.every(todo => todo.completed);
+
+ setTodos(todos.map(todo => ({ ...todo, completed: !allCompleted })));
+ },
+
+ clearCompleted() {
+ setTodos(todos.filter(todo => !todo.completed));
+ },
+ }),
+ [todos],
+ );
+
+ const filtredTodos = getFiltredTodos(todos, activeFilter);
+
+ const value: Context = useMemo(
+ () => ({
+ todos,
+ filtredTodos,
+ setTodos,
+ activeFilter,
+ setActiveFilter,
+ }),
+ [todos, filtredTodos, setTodos, activeFilter],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
new file mode 100644
index 000000000..04fc40981
--- /dev/null
+++ b/src/hooks/useLocalStorage.ts
@@ -0,0 +1,33 @@
+import { useState } from 'react';
+
+export function useLocalStorage(
+ valueName: string,
+ initialValue: T,
+): [T, (v: T) => void] {
+ const [value, setValue] = useState(() => {
+ if (localStorage.getItem(valueName) === null) {
+ localStorage.setItem(valueName, JSON.stringify(initialValue));
+ }
+
+ const data = localStorage.getItem(valueName);
+
+ if (data === null) {
+ return initialValue;
+ }
+
+ try {
+ return JSON.parse(data);
+ } catch (error) {
+ localStorage.removeItem(valueName);
+
+ return initialValue;
+ }
+ });
+
+ const changeValue = (newValue: T) => {
+ localStorage.setItem(valueName, JSON.stringify(newValue));
+ setValue(newValue);
+ };
+
+ return [value, changeValue];
+}
diff --git a/src/index.tsx b/src/index.tsx
index a9689cb38..72f012c62 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/index.scss';
+import './styles/todoapp.scss';
+import './styles/todo.scss';
+import './styles/filter.scss';
import { App } from './App';
+import { TodosProvider } from './components/TodosContext/TodosContext';
const container = document.getElementById('root') as HTMLDivElement;
-createRoot(container).render( );
+createRoot(container).render(
+
+
+ ,
+);
diff --git a/src/styles/todo.scss b/src/styles/todo.scss
index 4576af434..cfb34ec2f 100644
--- a/src/styles/todo.scss
+++ b/src/styles/todo.scss
@@ -71,6 +71,7 @@
}
&__title-field {
+ box-sizing: border-box;
width: 100%;
padding: 11px 14px;
diff --git a/src/styles/todoapp.scss b/src/styles/todoapp.scss
index e289a9458..29383a1e2 100644
--- a/src/styles/todoapp.scss
+++ b/src/styles/todoapp.scss
@@ -56,6 +56,7 @@
}
&__new-todo {
+ box-sizing: border-box;
width: 100%;
padding: 16px 16px 16px 60px;
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;
+}
diff --git a/src/types/TodoStatus.ts b/src/types/TodoStatus.ts
new file mode 100644
index 000000000..207c27648
--- /dev/null
+++ b/src/types/TodoStatus.ts
@@ -0,0 +1,5 @@
+export enum TodoStatus {
+ All = 'All',
+ Active = 'Active',
+ Completed = 'Completed',
+}
diff --git a/src/utils/countActiveTodo.ts b/src/utils/countActiveTodo.ts
new file mode 100644
index 000000000..752b45804
--- /dev/null
+++ b/src/utils/countActiveTodo.ts
@@ -0,0 +1,5 @@
+import { Todo } from '../types/Todo';
+
+export const countActiveTodo = (todos: Todo[]) => {
+ return todos.reduce((count, todo) => count + Number(!todo.completed), 0);
+};
diff --git a/src/utils/getFiltredTodos.ts b/src/utils/getFiltredTodos.ts
new file mode 100644
index 000000000..d94b36c27
--- /dev/null
+++ b/src/utils/getFiltredTodos.ts
@@ -0,0 +1,15 @@
+import { Todo } from '../types/Todo';
+import { TodoStatus } from '../types/TodoStatus';
+
+export const getFiltredTodos = (todos: Todo[], filter: TodoStatus) => {
+ switch (filter) {
+ case TodoStatus.Completed:
+ return todos.filter(todo => todo.completed);
+
+ case TodoStatus.Active:
+ return todos.filter(todo => !todo.completed);
+
+ default:
+ return todos;
+ }
+};