Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

togle completed, allTodoCompleted, esc #818

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ Implement the ability to edit a todo title on double click:

- Implement a solution following the [React task guideline](https://github.com/mate-academy/react_task-guideline#react-tasks-guideline).
- Use the [React TypeScript cheat sheet](https://mate-academy.github.io/fe-program/js/extra/react-typescript).
- Replace `<your_account>` with your Github username in the [DEMO LINK](https://<your_account>.github.io/react_todo-app-with-api/) and add it to the PR description.
- Replace `<your_account>` with your Github username in the [DEMO LINK](https://voronine.github.io/react_todo-app-with-api/) and add it to the PR description.
249 changes: 232 additions & 17 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,239 @@
/* eslint-disable max-len */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import { UserWarning } from './UserWarning';
import React, {
useEffect,
useMemo,
useState,
useCallback,
} from 'react';

const USER_ID = 0;
import { Todo } from './types/Todo';
import * as todoService from './api/todos';
import { ForComletedTodo } from './types/enumFilter';
import { TodoItem } from './Components/TodoItem';
import { Footer } from './Components/Footer';
import { Header } from './Components/Header';
import { ErrorNotification } from './Components/ErrorNotification';

export const App: React.FC = () => {
if (!USER_ID) {
return <UserWarning />;
}
const [todos, setTodos] = useState<Todo[]>([]);
const [condition, setCondition] = useState(ForComletedTodo.All);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [inputDisabled, setInputDisabled] = useState(false);
const [processingTodoIds, setProcessingTodoIds] = useState<number[]>([]);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [errorTimer, setErrorTimer] = useState<NodeJS.Timeout | null>(null);

useEffect(() => {
if (errorTimer) {
clearTimeout(errorTimer);
}

setErrorTimer(setTimeout(() => {
setErrorMessage(null);
}, 3000));
}, [errorMessage, errorTimer]);

const hasTodos = !!todos.length;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is not really needed

const isAllCompleted = todos.every(todo => todo.completed);

const fetchData = useCallback(async () => {
try {
setErrorMessage(null);
const todosFetch = await todoService.getTodos();

setTodos(todosFetch);
} catch (err) {
setErrorMessage('Unable to load todos');
}
}, []);

useEffect(() => {
fetchData();
}, []);

const hadleAddTodo = async (title: string) => {
try {
setInputDisabled(true);

const newTodo = {
id: 0,
userId: 0,
title: title.trim(),
completed: false,
};

setTempTodo(newTodo);

const addedTodo = await todoService.addTodo(title);

setTodos((prevTodos) => [...prevTodos, addedTodo]);
} catch (error) {
setErrorMessage('Unable to add a todo');
throw error;
} finally {
setInputDisabled(false);
setTempTodo(null);
}
};

const filteredTodos = useMemo(() => todos.filter(({ completed }) => {
switch (condition) {
case ForComletedTodo.Active:
return !completed;
case ForComletedTodo.Completed:
return completed;
default:
return 1;
}
}), [condition, todos]);
voronine marked this conversation as resolved.
Show resolved Hide resolved

const handleDeleteTodo = (todoId: number) => {
setInputDisabled(true);
setProcessingTodoIds((prevTodoIds) => [...prevTodoIds, todoId]);

return todoService
.deleteTodo(todoId)
.then((() => {
setTodos((prevTodos) => prevTodos.filter(todo => todo.id !== todoId));
}))
.catch(() => {
setErrorMessage('Unable to delete a todo');
throw new Error('Unable to delete a todo');
})
.finally(() => {
setInputDisabled(false);
setProcessingTodoIds(
(prevTodoIds) => prevTodoIds.filter(id => id !== todoId),
);
});
};

const handleRenameTodo = async (todo: Todo, newTodoTitle: string) => {
setProcessingTodoIds((prevTodoIds) => [...prevTodoIds, todo.id]);
voronine marked this conversation as resolved.
Show resolved Hide resolved

return todoService.updateTodo({
id: todo.id,
title: newTodoTitle.trim(),
userId: todo.userId,
completed: todo.completed,
})
voronine marked this conversation as resolved.
Show resolved Hide resolved
.then(updatedTodo => {
setTodos(prevState => prevState.map(currentTodo => (
currentTodo.id !== updatedTodo.id
? currentTodo
: updatedTodo
)));
})
.catch(() => {
setErrorMessage('Unable to update a todo');
throw new Error('Unable to update a todo');
})
.finally(() => {
setProcessingTodoIds(
(prevTodoIds) => prevTodoIds.filter(id => id !== todo.id),
);
});
};

const handleClearCompletedTodos = () => {
todos.forEach(todo => {
if (todo.completed) {
handleDeleteTodo(todo.id);
}
});
};

const handleToggleTodo = async (todo: Todo) => {
try {
setProcessingTodoIds((prevTodoIds) => [...prevTodoIds, todo.id]);

const updatedTodo = await todoService.updateTodo({
...todo,
completed: !todo.completed,
});

setTodos(prevState => prevState.map(currentTodo => (
currentTodo.id !== updatedTodo.id
? currentTodo
: updatedTodo
)));
} catch (error) {
setErrorMessage('Unable to update a todo');
} finally {
setProcessingTodoIds(
(prevTodoIds) => prevTodoIds.filter(id => id !== todo.id),
);
}
};

const handleToggleAllTodos = () => {
const activeTodos = todos.filter(todo => !todo.completed);

if (isAllCompleted) {
todos.forEach(handleToggleTodo);
} else {
activeTodos.forEach(handleToggleTodo);
}
};

const handleDelete = async (todoId: number) => {
return handleDeleteTodo(todoId);
};

const handleRename = async (todo: Todo, todoTitle: string) => {
await handleRenameTodo(todo, todoTitle);
};

voronine marked this conversation as resolved.
Show resolved Hide resolved
return (
<section className="section container">
<p className="title is-4">
Copy all you need from the prev task:
<br />
<a href="https://github.com/mate-academy/react_todo-app-add-and-delete#react-todo-app-add-and-delete">React Todo App - Add and Delete</a>
</p>

<p className="subtitle">Styles are already copied</p>
</section>
<div className="todoapp">
<h1 className="todoapp__title">todos</h1>

<div className="todoapp__content">
<Header
onTodoAddError={setErrorMessage}
isAllCompleted={isAllCompleted}
hasTodos={hasTodos}
onTodoAdd={hadleAddTodo}
inputDisabled={inputDisabled}
onToggleAll={handleToggleAllTodos}
/>

<section className="todoapp__main" data-cy="TodoList">
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onTodoDelete={() => handleDelete(todo.id)}
onRenameTodo={(todoTitle) => handleRename(todo, todoTitle)}
onTodoToggle={() => handleToggleTodo(todo)}
isProcessing={processingTodoIds.includes(todo.id)}
/>
))}
</section>

{tempTodo && (
<TodoItem
todo={tempTodo}
isProcessing
/>
)}

{hasTodos && (
<Footer
todos={todos}
condition={condition}
setCondition={setCondition}
handleClearCompletedTodos={handleClearCompletedTodos}
/>
)}
</div>

<ErrorNotification
setErrorMessage={setErrorMessage}
errorMessage={errorMessage}
errorTimer={errorTimer}
setErrorTimer={setErrorTimer}
/>
</div>
);
};
60 changes: 60 additions & 0 deletions src/Components/ErrorNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useEffect } from 'react';
import cn from 'classnames';

type Props = {
setErrorMessage: (errorMessage: string | null) => void;
errorMessage: string | null;
errorTimer: NodeJS.Timeout | null;
setErrorTimer: React.Dispatch<React.SetStateAction<NodeJS.Timeout | null>>;
};

export const ErrorNotification: React.FC<Props> = ({
setErrorMessage,
errorMessage,
errorTimer,
setErrorTimer,
}) => {
useEffect(() => {
if (errorTimer) {
clearTimeout(errorTimer);
}

const newTimer = setTimeout(() => {
setErrorMessage(null);
}, 3000);

setErrorTimer(newTimer);

return () => {
if (newTimer) {
clearTimeout(newTimer);
}
};
}, [errorMessage, setErrorTimer]);

return (
<div
data-cy="ErrorNotification"
className={cn(
'notification',
'is-danger',
'is-light',
'has-text-weight-normal',
{
hidden: !errorMessage,
},
)}
>

voronine marked this conversation as resolved.
Show resolved Hide resolved
<button
aria-label="HideErrorButton"
data-cy="HideErrorButton"
type="button"
className="delete"
onClick={() => setErrorMessage(null)}
/>

{errorMessage}
</div>
);
};
38 changes: 38 additions & 0 deletions src/Components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { Todo } from '../types/Todo';
import { NavMenu } from './NavMenu';
import { ForComletedTodo } from '../types/enumFilter';

type Props = {
todos: Todo[];
condition: ForComletedTodo;
setCondition: (condition: ForComletedTodo) => void;
handleClearCompletedTodos: () => void;
};

export const Footer: React.FC<Props> = ({
todos,
condition,
setCondition,
handleClearCompletedTodos,
}) => {
const activeTodos = todos.filter(todo => !todo.completed).length;

return (
<footer className="todoapp__footer" data-cy="Footer">
<span className="todo-count" data-cy="TodosCounter">
{`${activeTodos} item${activeTodos !== 1 ? 's' : ''} left`}
</span>
<NavMenu condition={condition} setCondition={setCondition} />
<button
type="button"
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
disabled={todos.every(todo => !todo.completed)}
voronine marked this conversation as resolved.
Show resolved Hide resolved
onClick={handleClearCompletedTodos}
>
Clear completed
</button>
</footer>
);
};
Loading
Loading