diff --git a/src/App.tsx b/src/App.tsx index 975c84994b..f46fb5302c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,22 @@ /* eslint-disable max-len */ /* eslint-disable jsx-a11y/control-has-associated-label */ /* eslint-disable jsx-a11y/label-has-associated-control */ -import React, { useEffect, useRef, useState } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { Header } from './components/Header/Header'; import { Footer } from './components/Footer/Footer'; import { TodoList } from './components/TodoList/TodoList'; import { Todo } from './types/Todo'; -import { deleteTodo, getTodos, USER_ID } from './api/todos'; -import { Errors } from './utils/Errors'; -import { FilterTodosBy } from './utils/FilterTodosBy'; +import { addTodo, deleteTodo, getTodos, updateTodo } from './api/todos'; +import { Errors } from './types/Errors'; +import { FilterTodosBy } from './types/FilterTodosBy'; import { Error } from './components/Error/Error'; +import { filterTodos } from './utils/utils'; export const App: React.FC = () => { const [todos, setTodos] = useState([]); @@ -22,6 +29,9 @@ export const App: React.FC = () => { const [isUpdating, setIsUpdating] = useState(false); const [updatingTodoId, setUpdatingTodoId] = useState(null); const [toggleCompleteAll, setToggleCompleteAll] = useState(false); + const [isHeaderDisabled, setIsHeaderDisabled] = useState(false); + const [tempIdCounter, setTempIdCounter] = useState(0); + const [deletingCardId, setDeletingCardId] = useState(null); useEffect(() => { setHasError(Errors.NoError); @@ -37,23 +47,55 @@ export const App: React.FC = () => { })(); }, []); - const filteredTodos = todos.filter(todo => { - switch (filterBy) { - case FilterTodosBy.Active: - return !todo.completed; - case FilterTodosBy.Completed: - return todo.completed; - default: - return true; - } - }); + const filteredTodos = filterTodos(todos, filterBy); + + const uncompletedTodosLength = useMemo(() => { + return todos.filter(todo => !todo.completed).length; + }, [todos]); - const uncompletedTodosLength = todos.filter(todo => !todo.completed).length; - const completedTodosLenght = todos.filter(todo => todo.completed).length; + const completedTodosLength = useMemo(() => { + return todos.filter(todo => todo.completed).length; + }, [todos]); const inputRef = useRef(null); - const handleDeleteAllCompleted = async () => { + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setHasError(Errors.NoError); + + if (!title.trim()) { + setHasError(Errors.TitleEmpty); + + return; + } + + const newTodo = { + userId: 0, + title: title.trim(), + completed: false, + }; + + setTempTodo({ ...newTodo, id: tempIdCounter }); + setIsHeaderDisabled(true); + + try { + const addNewTodo = await addTodo(newTodo); + + setTodos([...todos, addNewTodo]); + setIsHeaderDisabled(false); + setTitle(''); + setTempTodo(null); + setTempIdCounter(tempIdCounter + 1); + + inputRef.current?.focus(); + } catch { + setHasError(Errors.UnableToAdd); + setTempTodo(null); + setIsHeaderDisabled(false); + } + }; + + const handleDeleteAllCompleted = useCallback(async () => { const completedTodos = todos.filter(todo => todo.completed); for (const todo of completedTodos) { @@ -66,6 +108,65 @@ export const App: React.FC = () => { setHasError(Errors.UnableToDelete); } } + }, [todos]); + + const handleUpdateToCompleteAll = async () => { + setIsUpdating(true); + setUpdatingTodoId(null); + setToggleCompleteAll(true); + + const areAllTodosCompleted = todos.every(todo => todo.completed); + const todosToUpdate = todos.filter( + todo => todo.completed === areAllTodosCompleted, + ); + + try { + const updatePromises = todosToUpdate.map(todo => + updateTodo(todo.id, { + title: todo.title, + completed: !areAllTodosCompleted, + userId: todo.userId, + }), + ); + + await Promise.all(updatePromises); + + const updatedTodos = todos.map(todo => ({ + ...todo, + completed: todosToUpdate.some(t => t.id === todo.id) + ? !areAllTodosCompleted + : todo.completed, + })); + + setTodos(updatedTodos); + + setToggleCompleteAll(false); + setIsUpdating(false); + setUpdatingTodoId(null); + } catch { + setHasError(Errors.UnableToUpdate); + setIsUpdating(false); + setUpdatingTodoId(null); + setToggleCompleteAll(true); + } + }; + + const handleDelete = async (todoId: number) => { + setIsDeleting(true); + setDeletingCardId(todoId); + + try { + await deleteTodo(todoId); + setIsDeleting(false); + setDeletingCardId(null); + setTodos(todos.filter(todo => todo.id !== todoId)); + setDeletingCardId(null); + inputRef.current?.focus(); + } catch { + setHasError(Errors.UnableToDelete); + setIsDeleting(false); + setDeletingCardId(null); + } }; return ( @@ -75,18 +176,11 @@ export const App: React.FC = () => {
{ setIsDeleting={setIsDeleting} setTodos={setTodos} setHasError={setHasError} - inputRef={inputRef} initialTodos={todos} isUpdating={isUpdating} setIsUpdating={setIsUpdating} updatingTodoId={updatingTodoId} setUpdatingTodoId={setUpdatingTodoId} toggleCompleteAll={toggleCompleteAll} + handleDelete={handleDelete} + deletingCardId={deletingCardId} /> {todos.length > 0 && ( @@ -109,7 +204,7 @@ export const App: React.FC = () => { uncompletedTodosLength={uncompletedTodosLength} filterBy={filterBy} setFilteredBy={setFilterBy} - completedTodosLenght={completedTodosLenght} + completedTodosLenght={completedTodosLength} handleDeleteAllCompleted={handleDeleteAllCompleted} /> )} diff --git a/src/api/todos.ts b/src/api/todos.ts index 0400e0b597..57506a08a0 100644 --- a/src/api/todos.ts +++ b/src/api/todos.ts @@ -2,19 +2,20 @@ import { Todo } from '../types/Todo'; import { client } from '../utils/fetchClient'; export const USER_ID = 2142; +const TODOS_PATH = '/todos'; export const getTodos = () => { - return client.get(`/todos?userId=${USER_ID}`); + return client.get(`${TODOS_PATH}?userId=${USER_ID}`); }; export const addTodo = (data: Omit) => { - return client.post('/todos', data); + return client.post(`${TODOS_PATH}`, data); }; export const deleteTodo = (id: number) => { - return client.delete(`/todos/${id}`); + return client.delete(`${TODOS_PATH}/${id}`); }; export const updateTodo = (id: number, data: Omit) => { - return client.patch(`/todos/${id}`, data); + return client.patch(`${TODOS_PATH}/${id}`, data); }; diff --git a/src/components/Error/Error.tsx b/src/components/Error/Error.tsx index 8f1c1da140..a0649a5731 100644 --- a/src/components/Error/Error.tsx +++ b/src/components/Error/Error.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import cn from 'classnames'; -import { Errors } from '../../utils/Errors'; +import { Errors } from '../../types/Errors'; interface Props { hasError: Errors; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index e6840385f3..b205e0c37b 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { FilterTodosBy } from '../../utils/FilterTodosBy'; +import { FilterTodosBy } from '../../types/FilterTodosBy'; import cn from 'classnames'; interface Props { uncompletedTodosLength: number; @@ -17,11 +17,7 @@ export const Footer: React.FC = props => { handleDeleteAllCompleted, } = props; - const filters = [ - { label: 'All', value: FilterTodosBy.All, href: '#/' }, - { label: 'Active', value: FilterTodosBy.Active, href: '#/active' }, - { label: 'Completed', value: FilterTodosBy.Completed, href: '#/completed' }, - ]; + const filters = Object.values(FilterTodosBy); return (