diff --git a/src/App.tsx b/src/App.tsx
index a399287bd..2c9170f2a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,156 +1,29 @@
-/* eslint-disable jsx-a11y/control-has-associated-label */
-import React from 'react';
+import React, { useContext, useEffect } from 'react';
+import { Header } from './components/Header';
+import { TodosList } from './components/Todos';
+import { Footer } from './components/Footer';
+import { StateContext } from './context/TodosContext';
export const App: React.FC = () => {
+ const { todos } = useContext(StateContext);
+
+ useEffect(() => {
+ if (!localStorage.getItem('todos')) {
+ localStorage.setItem('todos', JSON.stringify([]));
+ }
+ }, []);
+
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/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
new file mode 100644
index 000000000..1d74476ef
--- /dev/null
+++ b/src/components/Footer/Footer.tsx
@@ -0,0 +1,81 @@
+import React, { useContext, useMemo } from 'react';
+import { DispatchContext, StateContext } from '../../context/TodosContext';
+import { TodosType } from '../../types/TodosInterface';
+import { FilterEnum } from '../../types/FilterEnum';
+import classNames from 'classnames';
+
+export const Footer: React.FC = () => {
+ const { todos, filter } = useContext(StateContext);
+ const dispatch = useContext(DispatchContext);
+
+ const activeTodos = useMemo(() => {
+ return [...todos].filter((currentTodo: TodosType) => {
+ return currentTodo.completed === false;
+ });
+ }, [todos]);
+
+ const completedTodos = useMemo(() => {
+ return [...todos].some((currentTodo: TodosType) => {
+ return currentTodo.completed === true;
+ });
+ }, [todos]);
+
+ return (
+
+ );
+};
diff --git a/src/components/Footer/index.ts b/src/components/Footer/index.ts
new file mode 100644
index 000000000..65e2506fa
--- /dev/null
+++ b/src/components/Footer/index.ts
@@ -0,0 +1 @@
+export { Footer } from './Footer';
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
new file mode 100644
index 000000000..c10227cef
--- /dev/null
+++ b/src/components/Header/Header.tsx
@@ -0,0 +1,72 @@
+import React, { useContext, useEffect, useRef, useState } from 'react';
+import { DispatchContext, StateContext } from '../../context/TodosContext';
+import classNames from 'classnames';
+import { TodosType } from '../../types/TodosInterface';
+
+export const Header: React.FC = () => {
+ const { todos } = useContext(StateContext);
+ const dispatch = useContext(DispatchContext);
+
+ const [title, setTitle] = useState('');
+ const fieldFocus = useRef(null);
+
+ const allTodosCopleted = [...todos].every(
+ (currentTodos: TodosType) => currentTodos.completed,
+ );
+
+ const handleTitle = (event: React.ChangeEvent) => {
+ setTitle(event.target.value);
+ };
+
+ const onSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+
+ if (title.trim().length === 0) {
+ return;
+ }
+
+ dispatch({ type: 'add', title: title });
+ setTitle('');
+ };
+
+ useEffect(() => {
+ if (fieldFocus.current) {
+ fieldFocus.current.focus();
+ }
+ }, []);
+
+ useEffect(() => {
+ if (fieldFocus.current) {
+ fieldFocus.current.focus();
+ }
+ }, [todos]);
+
+ return (
+
+ {/* this button should have `active` class only if all todos are completed */}
+ {todos.length > 0 && (
+ dispatch({ type: 'completeAll' })}
+ />
+ )}
+
+ {/* Add a todo on form submit */}
+
+
+ );
+};
diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts
new file mode 100644
index 000000000..29429dc97
--- /dev/null
+++ b/src/components/Header/index.ts
@@ -0,0 +1 @@
+export { Header } from './Header';
diff --git a/src/components/Todos/TodosList.tsx b/src/components/Todos/TodosList.tsx
new file mode 100644
index 000000000..defaa36c4
--- /dev/null
+++ b/src/components/Todos/TodosList.tsx
@@ -0,0 +1,122 @@
+import React, { useContext, useEffect, useMemo, useState } from 'react';
+import { DispatchContext, StateContext } from '../../context/TodosContext';
+import classNames from 'classnames';
+import { TodosType } from '../../types/TodosInterface';
+
+export const TodosList: React.FC = () => {
+ const { todos, filter } = useContext(StateContext);
+ const dispatch = useContext(DispatchContext);
+
+ const [title, setTitle] = useState('');
+ const [updateTodo, setUpdateTodo] = useState(null);
+
+ const handleTitle = (event: React.ChangeEvent) => {
+ setTitle(event.target.value);
+ };
+
+ const onDelete = (id: number) => {
+ dispatch({ type: 'delete', todoID: id });
+ };
+
+ const handleSubmit = () => {
+ const trimmedTitle = title.trim();
+
+ if (trimmedTitle === '') {
+ return onDelete(updateTodo?.id as number);
+ }
+
+ dispatch({
+ type: 'update',
+ currentTodo: updateTodo as TodosType,
+ newTitle: trimmedTitle,
+ });
+
+ setUpdateTodo(null);
+ };
+
+ const onEscape = (event: React.KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setUpdateTodo(null);
+ }
+ };
+
+ const visibleTodos = useMemo(() => {
+ if (filter === 'active') {
+ return [...todos].filter(
+ (currentTodos: TodosType) => currentTodos.completed === false,
+ );
+ }
+
+ if (filter === 'completed') {
+ return [...todos].filter(
+ (currentTodos: TodosType) => currentTodos.completed === true,
+ );
+ }
+
+ return todos;
+ }, [todos, filter]);
+
+ useEffect(() => {
+ if (updateTodo) {
+ setTitle(updateTodo?.title);
+ }
+ }, [updateTodo]);
+
+ return (
+
+ );
+};
diff --git a/src/components/Todos/index.ts b/src/components/Todos/index.ts
new file mode 100644
index 000000000..7b27cff16
--- /dev/null
+++ b/src/components/Todos/index.ts
@@ -0,0 +1 @@
+export { TodosList } from './TodosList';
diff --git a/src/context/TodosContext.tsx b/src/context/TodosContext.tsx
new file mode 100644
index 000000000..a93d9e40c
--- /dev/null
+++ b/src/context/TodosContext.tsx
@@ -0,0 +1,189 @@
+import React, { useReducer } from 'react';
+import { Action } from '../types/ActionType';
+import { TodosType } from '../types/TodosInterface';
+import { FilterEnum } from '../types/FilterEnum';
+
+interface State {
+ todos: TodosType[];
+ filter: FilterEnum;
+}
+
+function getInitialTodos() {
+ const storage = localStorage.getItem('todos');
+
+ if (!storage) {
+ return [];
+ }
+
+ return JSON.parse(storage);
+}
+
+const initialState: State = {
+ todos: getInitialTodos(),
+ filter: FilterEnum.ALL,
+};
+
+export const StateContext = React.createContext(initialState);
+export const DispatchContext = React.createContext>(
+ () => {},
+);
+
+function addTodo(state: State, title: string) {
+ const newTodos: TodosType = {
+ id: +new Date(),
+ title: title.trim(),
+ completed: false,
+ };
+
+ localStorage.setItem('todos', JSON.stringify([...state.todos, newTodos]));
+
+ return [...state.todos, newTodos];
+}
+
+function deleteTodo(state: State, todosID: number) {
+ const newTodos = state.todos.filter(
+ (currentTodo: TodosType) => currentTodo.id !== todosID,
+ );
+
+ localStorage.setItem('todos', JSON.stringify(newTodos));
+
+ return newTodos;
+}
+
+function updateTodos(state: State, updateTodo: TodosType, newTitle: string) {
+ const index = [...state.todos].findIndex((currentTodo: TodosType) => {
+ return currentTodo.id === updateTodo.id;
+ });
+
+ const newTodos: TodosType = {
+ id: updateTodo.id,
+ title: newTitle,
+ completed: updateTodo.completed,
+ };
+
+ state.todos.splice(index, 1, newTodos);
+
+ localStorage.setItem('todos', JSON.stringify(state.todos));
+
+ return state.todos;
+}
+
+function completeTodos(state: State, todosID: number) {
+ const newTodos = [...state.todos].map((currentTodos: TodosType) => {
+ return currentTodos.id === todosID
+ ? { ...currentTodos, completed: !currentTodos.completed }
+ : currentTodos;
+ });
+
+ localStorage.setItem('todos', JSON.stringify(newTodos));
+
+ return newTodos;
+}
+
+function deleteCopleted(state: State) {
+ const newTodos = [...state.todos].filter(
+ (currentTodos: TodosType) => currentTodos.completed === false,
+ );
+
+ localStorage.setItem('todos', JSON.stringify(newTodos));
+
+ return newTodos;
+}
+
+function completeAll(state: State) {
+ const isEveryActive = [...state.todos].some(
+ (currentTodos: TodosType) => currentTodos.completed === false,
+ );
+
+ const isEveryCompleted = [...state.todos].some(
+ (currentTodos: TodosType) => currentTodos.completed === true,
+ );
+
+ if (isEveryActive) {
+ const everyCompletedTodos = [...state.todos].map(
+ (currentTodos: TodosType) => {
+ return { ...currentTodos, completed: true };
+ },
+ );
+
+ localStorage.setItem('todos', JSON.stringify(everyCompletedTodos));
+
+ return everyCompletedTodos;
+ }
+
+ if (isEveryCompleted) {
+ const everyactiveTodos = [...state.todos].map((currentTodos: TodosType) => {
+ return { ...currentTodos, completed: false };
+ });
+
+ localStorage.setItem('todos', JSON.stringify(everyactiveTodos));
+
+ return everyactiveTodos;
+ }
+
+ const newTodos = [...state.todos].map((currentTodos: TodosType) => {
+ return currentTodos.completed === false
+ ? { ...currentTodos, completed: true }
+ : { ...currentTodos };
+ });
+
+ localStorage.setItem('todos', JSON.stringify(newTodos));
+
+ return newTodos;
+}
+
+function TodosReducer(state: State, action: Action): State {
+ switch (action.type) {
+ case 'add':
+ return {
+ ...state,
+ todos: addTodo(state, action.title),
+ };
+ case 'delete':
+ return {
+ ...state,
+ todos: deleteTodo(state, action.todoID),
+ };
+ case 'update':
+ return {
+ ...state,
+ todos: updateTodos(state, action.currentTodo, action.newTitle),
+ };
+ case 'complete':
+ return {
+ ...state,
+ todos: completeTodos(state, action.todoID),
+ };
+ case 'filter':
+ return {
+ ...state,
+ filter: action.filterType,
+ };
+ case 'deleteCompleted':
+ return {
+ ...state,
+ todos: deleteCopleted(state),
+ };
+ case 'completeAll':
+ return {
+ ...state,
+ todos: completeAll(state),
+ };
+ default:
+ return state;
+ }
+}
+
+interface Props {
+ children: React.ReactNode;
+}
+
+export const GlobalTodosContext: React.FC = ({ children }) => {
+ const [state, dispatch] = useReducer(TodosReducer, initialState);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/index.tsx b/src/index.tsx
index b2c38a17a..e9e15570d 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -3,7 +3,12 @@ import { createRoot } from 'react-dom/client';
import './styles/index.scss';
import { App } from './App';
+import { GlobalTodosContext } from './context/TodosContext';
const container = document.getElementById('root') as HTMLDivElement;
-createRoot(container).render( );
+createRoot(container).render(
+
+
+ ,
+);
diff --git a/src/styles/todo-list.scss b/src/styles/todo-list.scss
index 4576af434..cfb34ec2f 100644
--- a/src/styles/todo-list.scss
+++ b/src/styles/todo-list.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/ActionType.ts b/src/types/ActionType.ts
new file mode 100644
index 000000000..39be04465
--- /dev/null
+++ b/src/types/ActionType.ts
@@ -0,0 +1,11 @@
+import { FilterEnum } from './FilterEnum';
+import { TodosType } from './TodosInterface';
+
+export type Action =
+ | { type: 'add'; title: string }
+ | { type: 'update'; currentTodo: TodosType; newTitle: string }
+ | { type: 'delete'; todoID: number }
+ | { type: 'complete'; todoID: number }
+ | { type: 'deleteCompleted' }
+ | { type: 'completeAll' }
+ | { type: 'filter'; filterType: FilterEnum };
diff --git a/src/types/FilterEnum.ts b/src/types/FilterEnum.ts
new file mode 100644
index 000000000..9537f05ee
--- /dev/null
+++ b/src/types/FilterEnum.ts
@@ -0,0 +1,5 @@
+export enum FilterEnum {
+ ALL = 'all',
+ ACTIVE = 'active',
+ COMPLETED = 'completed',
+}
diff --git a/src/types/TodosInterface.ts b/src/types/TodosInterface.ts
new file mode 100644
index 000000000..7917594e7
--- /dev/null
+++ b/src/types/TodosInterface.ts
@@ -0,0 +1,5 @@
+export interface TodosType {
+ id: number;
+ title: string;
+ completed: boolean;
+}