diff --git a/src/App.tsx b/src/App.tsx index 5749bdf784..42c56a3a4d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,75 @@ -/* eslint-disable max-len */ -/* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; -import { UserWarning } from './UserWarning'; - -const USER_ID = 0; +import React, { useState } from 'react'; +import { Todo } from './types/Todo'; +import { Errors } from './types/Errors'; +import { FilterOption } from './types/filterOption'; +import { TodoList } from './components/todoList'; +import { InCaseOfError } from './components/inCaseOfError'; +import { InputOfTodos } from './components/inputOfTodos'; +import { Footer } from './components/footer'; export const App: React.FC = () => { - if (!USER_ID) { - return ; - } + const [todos, setTodos] = useState([]); + const [error, setError] = useState(null); + const [filterTodos, setFilterTodos] + = useState(FilterOption.ALL); + const [newTodo, setNewTodo] = useState(''); + + const handleSetFilter = (newFilter: FilterOption) => { + setFilterTodos(newFilter); + }; + + const closeError = () => { + setError(null); + }; + + const addTodo = () => { + if (newTodo.trim() !== '') { + const newTodoItem: Todo = { + id: todos.length + 1, + title: newTodo, + completed: false, + removed: false, + editing: false, + }; + + setTodos([...todos, newTodoItem]); + setNewTodo(''); + } + }; return ( -
-

- Copy all you need from the prev task: -
- React Todo App - Add and Delete -

- -

Styles are already copied

-
+
+

todos

+ +
+ + + {todos && ( + + )} + + {todos.length > 0 && ( +
+ )} +
+ + {error && ( + + )} +
); }; diff --git a/src/components/footer.tsx b/src/components/footer.tsx new file mode 100644 index 0000000000..57294aa87b --- /dev/null +++ b/src/components/footer.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { TodoCount } from './todoCount'; +import { Todo } from '../types/Todo'; +import { FilterOption } from '../types/filterOption'; + +interface Props { + handleSetFilter: (newFilter: FilterOption) => void; + todos: Todo[]; + setTodos: (todos: Todo[]) => void; +} + +export const Footer: React.FC = ({ + handleSetFilter, + todos, + setTodos, +}) => { + const handleRemoveComplited = () => { + setTodos(todos.filter((todo) => todo.completed !== true)); + }; + + return ( + + ); +}; diff --git a/src/components/inCaseOfError.tsx b/src/components/inCaseOfError.tsx new file mode 100644 index 0000000000..3cddcb8db5 --- /dev/null +++ b/src/components/inCaseOfError.tsx @@ -0,0 +1,27 @@ +import React, { useEffect } from 'react'; +import { Errors } from '../types/Errors'; + +interface Props { + error: Errors; + closeError: () => void; +} + +export const InCaseOfError: React.FC = ({ error, closeError }) => { + useEffect(() => { + const timeoutId = setTimeout(() => { + closeError(); + }, 3000); + + return () => { + clearTimeout(timeoutId); + }; + }, [closeError]); + + return ( +
+ +
+ ); +}; diff --git a/src/components/inputOfTodos.tsx b/src/components/inputOfTodos.tsx new file mode 100644 index 0000000000..18c396366f --- /dev/null +++ b/src/components/inputOfTodos.tsx @@ -0,0 +1,77 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ +import React from 'react'; +import { Todo } from '../types/Todo'; + +interface Props { + setNewTodo: (newTodo: string) => void; + newTodo: string; + addTodo: (title: string) => void; + todos: Todo[]; + setTodos: (todos: Todo[]) => void; +} + +export const InputOfTodos: React.FC = ({ + setNewTodo, newTodo, addTodo, todos, setTodos, +}) => { + const handleAddNewTodo = () => { + if (newTodo) { + addTodo(newTodo); + setNewTodo(''); + } + }; + + const allComplited = (todos.every((t) => t.completed)); + + const handleInputChange = (event: React.ChangeEvent) => { + setNewTodo(event.target.value); + }; + + const handleInputKeyPress + = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + handleAddNewTodo(); + } + }; + + const handleTaggleAll = () => { + let updatedTodos = todos; + + if (!allComplited) { + updatedTodos = todos.map((t) => ({ + ...t, + completed: true, + })); + } else { + updatedTodos = todos.map((t) => ({ + ...t, + completed: !t.completed, + })); + } + + setTodos(updatedTodos); + }; + + return ( +
+ {todos.length > 0 && ( +
+ ); +}; diff --git a/src/components/task.tsx b/src/components/task.tsx new file mode 100644 index 0000000000..6f495a6241 --- /dev/null +++ b/src/components/task.tsx @@ -0,0 +1,118 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Todo } from '../types/Todo'; + +type Props = { + todo: Todo; + setTodos: React.Dispatch>; + todos: Todo[]; +}; + +export const Task: React.FC = ({ todo, setTodos, todos }) => { + const [inputValue, setInputValue] = useState(todo.title); + const inputRef = useRef(null); + + const handleToggleCompletion = () => { + const updatedTodo = { ...todo, completed: !todo.completed }; + + setTodos(todos.map((t) => (t.id === updatedTodo.id ? updatedTodo : t))); + }; + + const handleRemove = () => { + const updatedTodo = { ...todo, removed: true }; + + setTodos(todos.map((t) => (t.id === updatedTodo.id ? updatedTodo : t))); + }; + + const handleEditStart = () => { + const updatedTodo = { ...todo, editing: true }; + + setInputValue(todo.title); + setTodos(todos.map((t) => (t.id === updatedTodo.id ? updatedTodo : t))); + }; + + const saveChanges = () => { + const updatedTodo = { ...todo, title: inputValue, editing: false }; + + if (updatedTodo.title === '') { + handleRemove(); + } else { + setTodos(todos.map((t) => (t.id === updatedTodo.id ? updatedTodo : t))); + } + + setInputValue(''); + }; + + const handleInputChange = (event: React.ChangeEvent) => { + setInputValue(event.target.value); + }; + + const cancelEditing = () => { + const updatedTodo = { ...todo, editing: false }; + + setTodos(todos.map((t) => (t.id === updatedTodo.id ? updatedTodo : t))); + + setInputValue(''); + }; + + const handleInputKeyPress + = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + saveChanges(); + } + }; + + const handleInputEscPress + = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + cancelEditing(); + } + }; + + const handleInputBlur = () => { + saveChanges(); + }; + + useEffect(() => { + if (todo.editing && inputRef.current) { + inputRef.current.focus(); + } + }, [todo.editing]); + + return ( +
+ + {!todo.editing ? ( + <> + {todo.title} + + + ) : ( + + )} +
+ ); +}; diff --git a/src/components/todoCount.tsx b/src/components/todoCount.tsx new file mode 100644 index 0000000000..cbc071e192 --- /dev/null +++ b/src/components/todoCount.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Todo } from '../types/Todo'; + +interface Props { + todos: Todo[]; +} + +export const TodoCount: React.FC = ({ todos }) => { + const todosLeftCount = todos.filter((todo) => !todo.completed + && !todo.removed).length; + + return ( + + {todosLeftCount} + {' '} + {todosLeftCount === 1 ? 'item' : 'items'} + {' '} + left + + ); +}; diff --git a/src/components/todoList.tsx b/src/components/todoList.tsx new file mode 100644 index 0000000000..679f9491be --- /dev/null +++ b/src/components/todoList.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Todo } from '../types/Todo'; +import { FilterOption } from '../types/filterOption'; +import { Task } from './task'; + +interface Props { + todos: Todo[]; + filterTodos: FilterOption; + setTodos: React.Dispatch>; +} + +export const TodoList: React.FC = ({ todos, filterTodos, setTodos }) => { + const visibleTodos = (() => { + let filteredTodos = todos; + + if (filterTodos === FilterOption.ACTIVE) { + filteredTodos = filteredTodos.filter((todo) => !todo.completed); + } else if (filterTodos === FilterOption.COMPLETED) { + filteredTodos = filteredTodos.filter((todo) => todo.completed); + } + + filteredTodos = filteredTodos.filter((todo) => !todo.removed); + + return filteredTodos; + })(); + + return ( +
+ {visibleTodos.map((todo) => ( + + ))} +
+ ); +}; diff --git a/src/types/Errors.ts b/src/types/Errors.ts new file mode 100644 index 0000000000..6264053510 --- /dev/null +++ b/src/types/Errors.ts @@ -0,0 +1,5 @@ +export enum Errors { + Add = 'Unable to add a todo', + Delete = 'Unable to delete a todo', + Update = 'Unable to update a todo', +} diff --git a/src/types/Todo.ts b/src/types/Todo.ts new file mode 100644 index 0000000000..0274a3bbfb --- /dev/null +++ b/src/types/Todo.ts @@ -0,0 +1,7 @@ +export type Todo = { + id: number; + title: string; + completed: boolean; + removed: boolean; + editing: boolean; +}; diff --git a/src/types/filterOption.ts b/src/types/filterOption.ts new file mode 100644 index 0000000000..e1e33a19ae --- /dev/null +++ b/src/types/filterOption.ts @@ -0,0 +1,5 @@ +export const enum FilterOption { + ALL = 'all', + ACTIVE = 'active', + COMPLETED = 'completed', +}