diff --git a/src/App.tsx b/src/App.tsx
index a399287bd..34c89378b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,156 +1,40 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
-import React from 'react';
+// #region imports
+import React, { useContext, useState } from 'react';
+import { Footer } from './components/Footer';
+import { Header } from './components/Header';
+import { TodoList } from './components/TodoList';
+import { TodosContext } from './components/TodosContext';
+import { getFilteredTodos } from './services/getFilteredTodos';
+import { FilterStatus } from './types/FilterStatus';
+// #endregion
export const App: React.FC = () => {
+ const { todos } = useContext(TodosContext);
+ const sortedTodos = {
+ active: todos.filter(({ completed }) => !completed),
+ completed: todos.filter(({ completed }) => completed),
+ };
+ const [filterStatus, setFilterStatus] = useState(FilterStatus.All);
+
+ const filteredTodos = getFilteredTodos(todos, sortedTodos, filterStatus);
+
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.tsx b/src/components/Footer.tsx
new file mode 100644
index 000000000..891e18f7d
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -0,0 +1,66 @@
+// #region imports
+import cn from 'classnames';
+import { useContext } from 'react';
+import { FilterStatus } from '../types/FilterStatus';
+import { Todo } from '../types/Todo';
+import { TodosContext } from './TodosContext';
+// #endregion
+
+type Props = {
+ sortedTodos: {
+ active: Todo[];
+ completed: Todo[];
+ };
+ filterStatus: FilterStatus;
+ onStatusChange: (status: FilterStatus) => void;
+};
+
+export const Footer: React.FC = ({
+ sortedTodos,
+ filterStatus,
+ onStatusChange,
+}) => {
+ const { changeTodos } = useContext(TodosContext);
+ const { active, completed } = sortedTodos;
+ const filterLinks: {
+ [key: string]: string;
+ } = {
+ All: '#/',
+ Active: '#/active',
+ Completed: '#/completed',
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
new file mode 100644
index 000000000..0dfd8654b
--- /dev/null
+++ b/src/components/Header.tsx
@@ -0,0 +1,84 @@
+// #region imports
+import classNames from 'classnames';
+import {
+ FormEvent,
+ memo,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
+import { TodosContext } from './TodosContext';
+// #endregion
+
+export const Header = memo(function Header() {
+ // #region hooks
+ const { todos, changeTodos } = useContext(TodosContext);
+ const [newTitle, setNewTitle] = useState('');
+ const titleInput = useRef(null);
+
+ useEffect(() => {
+ titleInput.current?.focus();
+ });
+ // #endregion
+
+ const areTodosCompleted = todos.every(todo => todo.completed);
+
+ // #region handlings
+ const handleTodosToggle = () => {
+ changeTodos(
+ todos.map(todo => ({
+ ...todo,
+ completed: !areTodosCompleted,
+ })),
+ );
+ };
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+
+ const trimmedTitle = newTitle.trim();
+
+ if (!trimmedTitle) {
+ return;
+ }
+
+ changeTodos([
+ ...todos,
+ {
+ id: +new Date(),
+ title: trimmedTitle,
+ completed: false,
+ },
+ ]);
+ setNewTitle('');
+ };
+ // #endregion
+
+ return (
+
+ {todos.length > 0 && (
+
+ )}
+
+
+
+ );
+});
diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx
new file mode 100644
index 000000000..460a9cd66
--- /dev/null
+++ b/src/components/TodoItem.tsx
@@ -0,0 +1,132 @@
+// #region imports
+import cn from 'classnames';
+import { memo, useContext, useEffect, useRef, useState } from 'react';
+import { Todo } from '../types/Todo';
+import { TodosContext } from './TodosContext';
+// #endregion
+
+type Props = {
+ todo: Todo;
+ isEdited?: boolean;
+ onEditedChange?: (id: number | null) => void;
+};
+
+export const TodoItem: React.FC = memo(function TodoItem({ todo }) {
+ const { id, title, completed } = todo;
+
+ // #region hooks
+ const { todos, changeTodos } = useContext(TodosContext);
+ const [isEdited, setIsEdited] = useState(false);
+ const [editedTitle, setEditedTitle] = useState(title);
+ const todoDivRef = useRef(null);
+
+ useEffect(() => {
+ const startEditing = () => {
+ setIsEdited(true);
+ };
+
+ const stopEditing = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ setIsEdited(false);
+ setEditedTitle(title);
+ }
+ };
+
+ const todoDiv = todoDivRef.current;
+
+ todoDiv?.addEventListener('dblclick', startEditing);
+
+ document.addEventListener('keyup', stopEditing);
+
+ return () => {
+ todoDiv?.removeEventListener('dblclick', startEditing);
+
+ document.removeEventListener('keyup', stopEditing);
+ };
+ }, [title]);
+ // #endregion
+
+ // #region handlings
+ const handleDelete = () => {
+ changeTodos(todos.filter(t => t.id !== id));
+ };
+
+ const handleEditing = (property: keyof Todo, newValue: string | boolean) => {
+ const index = todos.findIndex(t => t.id === id);
+ const newTodos = [...todos];
+
+ const editedValue =
+ typeof newValue === 'string' ? newValue.trim() : newValue;
+
+ if (editedValue === '') {
+ newTodos.splice(index, 1);
+ } else {
+ const newTodo = {
+ ...todos[index],
+ [property]: editedValue,
+ };
+
+ newTodos.splice(index, 1, newTodo);
+ }
+
+ changeTodos(newTodos);
+ };
+
+ const onSubmit = () => {
+ handleEditing('title', editedTitle);
+ setIsEdited(false);
+ };
+ // #endregion
+
+ return (
+
+
+ {/* eslint-disable jsx-a11y/control-has-associated-label */}
+ handleEditing('completed', e.target.checked)}
+ />
+
+
+ {isEdited ? (
+
+ ) : (
+ <>
+
+ {title}
+
+
+
+ ×
+
+ >
+ )}
+
+ );
+});
diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx
new file mode 100644
index 000000000..d44734181
--- /dev/null
+++ b/src/components/TodoList.tsx
@@ -0,0 +1,14 @@
+import { Todo } from '../types/Todo';
+import { TodoItem } from './TodoItem';
+
+type Props = {
+ todos: Todo[];
+};
+
+export const TodoList: React.FC = ({ todos }) => (
+
+ {todos.map(todo => (
+
+ ))}
+
+);
diff --git a/src/components/TodosContext.tsx b/src/components/TodosContext.tsx
new file mode 100644
index 000000000..c9b6bd823
--- /dev/null
+++ b/src/components/TodosContext.tsx
@@ -0,0 +1,36 @@
+import React, { createContext, useMemo } from 'react';
+import { useLocalStorage } from '../hooks/useLocalStorage';
+import { Todo } from '../types/Todo';
+
+interface ContextProperty {
+ todos: Todo[];
+ changeTodos: (todos: Todo[]) => void;
+}
+
+export const TodosContext = createContext({
+ todos: new Array(),
+ changeTodos: () => {},
+});
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export const TodosProvider: React.FC = ({ children }) => {
+ const [todos, changeTodos] = useLocalStorage(
+ 'todos',
+ new Array(),
+ );
+
+ const value = useMemo(
+ () => ({
+ todos,
+ changeTodos,
+ }),
+ [todos, changeTodos],
+ );
+
+ return (
+ {children}
+ );
+};
diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
new file mode 100644
index 000000000..db2595431
--- /dev/null
+++ b/src/hooks/useLocalStorage.ts
@@ -0,0 +1,29 @@
+import { useState } from 'react';
+
+export function useLocalStorage(
+ itemName: string,
+ initialValue: T,
+): [T, (item: T) => void] {
+ const [item, setItem] = useState(() => {
+ const data = localStorage.getItem(itemName);
+
+ if (data === null) {
+ return initialValue;
+ }
+
+ try {
+ return JSON.parse(data);
+ } catch {
+ localStorage.removeItem(itemName);
+
+ return initialValue;
+ }
+ });
+
+ const changeItem = (newItem: T) => {
+ localStorage.setItem(itemName, JSON.stringify(newItem));
+ setItem(newItem);
+ };
+
+ return [item, changeItem];
+}
diff --git a/src/index.tsx b/src/index.tsx
index a9689cb38..092259365 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/filter.scss';
+import './styles/todo.scss';
+import './styles/todoapp.scss';
import { App } from './App';
+import { TodosProvider } from './components/TodosContext';
const container = document.getElementById('root') as HTMLDivElement;
-createRoot(container).render( );
+createRoot(container).render(
+
+
+ ,
+);
diff --git a/src/services/getFilteredTodos.ts b/src/services/getFilteredTodos.ts
new file mode 100644
index 000000000..c326e3b46
--- /dev/null
+++ b/src/services/getFilteredTodos.ts
@@ -0,0 +1,25 @@
+import { FilterStatus } from '../types/FilterStatus';
+import { Todo } from '../types/Todo';
+
+export function getFilteredTodos(
+ todos: Todo[],
+ sortedTodos: {
+ active: Todo[];
+ completed: Todo[];
+ },
+ filterStatus: FilterStatus,
+) {
+ let filteredTodos = [...todos];
+
+ if (filterStatus === FilterStatus.All) {
+ return filteredTodos;
+ }
+
+ if (filterStatus === FilterStatus.Completed) {
+ filteredTodos = sortedTodos.completed;
+ } else {
+ filteredTodos = sortedTodos.active;
+ }
+
+ return filteredTodos;
+}
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..434b7f6cd 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;
@@ -130,5 +131,9 @@
&:active {
text-decoration: none;
}
+
+ &:disabled {
+ visibility: hidden;
+ }
}
}
diff --git a/src/types/FilterStatus.ts b/src/types/FilterStatus.ts
new file mode 100644
index 000000000..7ca17f289
--- /dev/null
+++ b/src/types/FilterStatus.ts
@@ -0,0 +1,5 @@
+export enum FilterStatus {
+ All = 'All',
+ Active = 'Active',
+ Completed = 'Completed',
+}
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;
+}