diff --git a/src/App.tsx b/src/App.tsx
index a399287bd..833f1efdd 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,5 +1,7 @@
-/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
+import { Header } from './components/Header';
+import { TodoList } from './components/TodoList';
+import { Footer } from './components/Footer';
export const App: React.FC = () => {
return (
@@ -7,150 +9,9 @@ export const App: React.FC = () => {
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/GlobalProvider.tsx b/src/GlobalProvider.tsx
new file mode 100644
index 000000000..46f7a9d90
--- /dev/null
+++ b/src/GlobalProvider.tsx
@@ -0,0 +1,62 @@
+import React, { useEffect } from 'react';
+
+import { Action } from './types/Action';
+import { Filter } from './types/Filter';
+import { State } from './types/State';
+
+const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case 'setFilter':
+ return { ...state, filter: action.payload };
+
+ case 'addTodo':
+ return { ...state, todos: [...state.todos, action.payload] };
+
+ case 'deleteTodo':
+ return {
+ ...state,
+ todos: state.todos.filter(todo => todo.id !== action.payload),
+ };
+
+ case 'updateTodo':
+ return {
+ ...state,
+ todos: state.todos.map(todo =>
+ todo.id === action.payload.id ? action.payload : todo,
+ ),
+ };
+
+ default:
+ return state;
+ }
+};
+
+const loadedTodos = localStorage.getItem('todos');
+const initialState: State = {
+ todos: loadedTodos ? JSON.parse(loadedTodos) : [],
+ filter: Filter.All,
+};
+
+const StateContext = React.createContext(initialState);
+const DispatchContext = React.createContext>(() => {});
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export const GlobalProvider: React.FC = ({ children }) => {
+ const [state, dispatch] = React.useReducer(reducer, initialState);
+
+ useEffect(() => {
+ localStorage.setItem('todos', JSON.stringify(state.todos));
+ }, [state.todos]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useDispatch = () => React.useContext(DispatchContext);
+export const useGlobalState = () => React.useContext(StateContext);
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
new file mode 100644
index 000000000..4c9dbe2eb
--- /dev/null
+++ b/src/components/Footer/Footer.tsx
@@ -0,0 +1,62 @@
+import classNames from 'classnames';
+import { useDispatch, useGlobalState } from '../../GlobalProvider';
+import { Filter } from '../../types/Filter';
+import { useMemo } from 'react';
+
+export const Footer = () => {
+ const { todos, filter } = useGlobalState();
+ const dispatch = useDispatch();
+
+ const activeTodosCount = useMemo(
+ () => todos.filter(todo => !todo.completed).length,
+ [todos],
+ );
+
+ const deleteCompleted = () => {
+ todos.forEach(todo => {
+ if (todo.completed) {
+ dispatch({ type: 'deleteTodo', payload: todo.id });
+ }
+ });
+ };
+
+ if (!todos.length) {
+ return null;
+ }
+
+ return (
+
+ );
+};
diff --git a/src/components/Footer/index.ts b/src/components/Footer/index.ts
new file mode 100644
index 000000000..ddcc5a9cd
--- /dev/null
+++ b/src/components/Footer/index.ts
@@ -0,0 +1 @@
+export * from './Footer';
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
new file mode 100644
index 000000000..6a3a20ba2
--- /dev/null
+++ b/src/components/Header/Header.tsx
@@ -0,0 +1,89 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { useDispatch, useGlobalState } from '../../GlobalProvider';
+import classNames from 'classnames';
+import { Todo } from '../../types/Todo';
+
+export const Header = () => {
+ const [title, setTitle] = useState('');
+ const inputRef = useRef(null);
+
+ const { todos } = useGlobalState();
+ const dispatch = useDispatch();
+
+ const areAllTodosCompleted = useMemo(() => {
+ return todos.filter(todo => todo.completed).length === todos.length;
+ }, [todos]);
+
+ useEffect(() => inputRef.current?.focus(), [todos.length]);
+
+ const handleToggleAllButtonClick = () => {
+ let todosToChange = [];
+
+ if (areAllTodosCompleted) {
+ todosToChange = [...todos];
+ } else {
+ todosToChange = todos.filter(todo => !todo.completed);
+ }
+
+ todosToChange.forEach(todo => {
+ const { id, title: todoTitle, completed } = todo;
+
+ dispatch({
+ type: 'updateTodo',
+ payload: { id, title: todoTitle, completed: !completed },
+ });
+ });
+ };
+
+ const handleTitleChange = (event: React.ChangeEvent) => {
+ setTitle(event.target.value);
+ };
+
+ const handleFormSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+
+ const trimmedTitle = title.trim();
+
+ if (!trimmedTitle) {
+ return;
+ }
+
+ const newTodo: Todo = {
+ title: trimmedTitle,
+ id: +new Date(),
+ completed: false,
+ };
+
+ dispatch({ type: 'addTodo', payload: newTodo });
+
+ setTitle('');
+ };
+
+ return (
+
+ {!!todos.length && (
+
+ )}
+
+
+
+ );
+};
diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts
new file mode 100644
index 000000000..266dec8a1
--- /dev/null
+++ b/src/components/Header/index.ts
@@ -0,0 +1 @@
+export * from './Header';
diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx
new file mode 100644
index 000000000..3e26d06c8
--- /dev/null
+++ b/src/components/TodoItem/TodoItem.tsx
@@ -0,0 +1,111 @@
+import React, { useState } from 'react';
+import classNames from 'classnames';
+
+import { Todo } from '../../types/Todo';
+import { useDispatch } from '../../GlobalProvider';
+
+type Props = {
+ todo: Todo;
+};
+
+export const TodoItem: React.FC = ({ todo }) => {
+ const { id, title, completed } = todo;
+
+ const [isEditing, setIsEditing] = useState(false);
+ const [editedTitle, setEditedTitle] = useState(title);
+ const dispatch = useDispatch();
+
+ const handleEditedTitleChange = (
+ event: React.ChangeEvent,
+ ) => {
+ setEditedTitle(event.target.value);
+ };
+
+ const saveChanges = () => {
+ const trimmedEditedTitle = editedTitle.trim();
+
+ if (!trimmedEditedTitle) {
+ dispatch({ type: 'deleteTodo', payload: todo.id });
+ setIsEditing(false);
+
+ return;
+ }
+
+ dispatch({
+ type: 'updateTodo',
+ payload: { id, completed, title: trimmedEditedTitle },
+ });
+ setIsEditing(false);
+ };
+
+ const handleEditSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+ saveChanges();
+ };
+
+ const handleEscapeKeyUp = (event: React.KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setIsEditing(false);
+ }
+ };
+
+ return (
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+
+ dispatch({
+ type: 'updateTodo',
+ payload: { id, title, completed: !completed },
+ })
+ }
+ />
+
+
+ {!isEditing && (
+ <>
+ setIsEditing(true)}
+ >
+ {title}
+
+
+ dispatch({ type: 'deleteTodo', payload: id })}
+ >
+ ×
+
+ >
+ )}
+
+ {isEditing && (
+
+ )}
+
+ );
+};
diff --git a/src/components/TodoItem/index.ts b/src/components/TodoItem/index.ts
new file mode 100644
index 000000000..21f4abac3
--- /dev/null
+++ b/src/components/TodoItem/index.ts
@@ -0,0 +1 @@
+export * from './TodoItem';
diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx
new file mode 100644
index 000000000..bb88f6a86
--- /dev/null
+++ b/src/components/TodoList/TodoList.tsx
@@ -0,0 +1,25 @@
+import { useMemo } from 'react';
+import { useGlobalState } from '../../GlobalProvider';
+import { TodoItem } from '../TodoItem';
+import { filterTodos } from '../../utils/filterTodos';
+
+export const TodoList = () => {
+ const { todos, filter } = useGlobalState();
+
+ const visibleTodos = useMemo(
+ () => filterTodos(todos, filter),
+ [filter, todos],
+ );
+
+ if (!todos.length) {
+ return null;
+ }
+
+ return (
+
+ {visibleTodos.map(todo => (
+
+ ))}
+
+ );
+};
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/index.tsx b/src/index.tsx
index a9689cb38..7918c9dee 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,11 +1,14 @@
import { createRoot } from 'react-dom/client';
-import './styles/index.css';
-import './styles/todo-list.css';
-import './styles/filters.css';
+import './styles/index.scss';
import { App } from './App';
+import { GlobalProvider } from './GlobalProvider';
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/Action.ts b/src/types/Action.ts
new file mode 100644
index 000000000..80caec8e1
--- /dev/null
+++ b/src/types/Action.ts
@@ -0,0 +1,8 @@
+import { Filter } from './Filter';
+import { Todo } from './Todo';
+
+export type Action =
+ | { type: 'setFilter'; payload: Filter }
+ | { type: 'addTodo'; payload: Todo }
+ | { type: 'deleteTodo'; payload: number }
+ | { type: 'updateTodo'; payload: Todo };
diff --git a/src/types/Filter.ts b/src/types/Filter.ts
new file mode 100644
index 000000000..401d5c5e8
--- /dev/null
+++ b/src/types/Filter.ts
@@ -0,0 +1,5 @@
+export enum Filter {
+ All = 'All',
+ Completed = 'Completed',
+ Active = 'Active',
+}
diff --git a/src/types/State.ts b/src/types/State.ts
new file mode 100644
index 000000000..2990f7672
--- /dev/null
+++ b/src/types/State.ts
@@ -0,0 +1,7 @@
+import { Filter } from './Filter';
+import { Todo } from './Todo';
+
+export type State = {
+ todos: Todo[];
+ filter: Filter;
+};
diff --git a/src/types/Todo.ts b/src/types/Todo.ts
new file mode 100644
index 000000000..d94ea1bff
--- /dev/null
+++ b/src/types/Todo.ts
@@ -0,0 +1,5 @@
+export type Todo = {
+ id: number;
+ title: string;
+ completed: boolean;
+};
diff --git a/src/utils/filterTodos.ts b/src/utils/filterTodos.ts
new file mode 100644
index 000000000..53bc3e4ea
--- /dev/null
+++ b/src/utils/filterTodos.ts
@@ -0,0 +1,13 @@
+import { Filter } from '../types/Filter';
+import { Todo } from '../types/Todo';
+
+export const filterTodos = (todos: Todo[], filterBy: Filter): Todo[] => {
+ switch (filterBy) {
+ case Filter.Completed:
+ return todos.filter(todo => todo.completed);
+ case Filter.Active:
+ return todos.filter(todo => !todo.completed);
+ default:
+ return todos;
+ }
+};