Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,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://ArtemNosachenko.github.io/react_todo-app-with-api/) and add it to the PR description.
372 changes: 359 additions & 13 deletions src/App.tsx

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

separate your App to components

Original file line number Diff line number Diff line change
@@ -1,26 +1,372 @@
/* eslint-disable max-len */
/* eslint-disable jsx-a11y/label-has-associated-control */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import { UserWarning } from './UserWarning';
import {
USER_ID,
getTodos,
addTodo,
deleteTodo,
updateTodo,
} from './api/todos';
import { Todo } from './types/Todo';
import classNames from 'classnames';
import { FilterType } from './types/FilterType';
import { ErrorNotification } from './components/ErrorNotification';
import { Header } from './components/Header';
import { Footer } from './components/Footer';

const USER_ID = 0;
export enum ErrorMessage {
LOAD_TODOS = 'Unable to load todos',
ADD_TODO = 'Unable to add a todo',
DELETE_TODO = 'Unable to delete a todo',
UPDATE_TODO = 'Unable to update a todo',
EMPTY_TITLE = 'Title should not be empty',
}

export const App: React.FC = () => {
const [todos, setTodos] = React.useState<Todo[]>([]);
const [errorMessage, setErrorMessage] = React.useState('');
const [filter, setFilter] = React.useState(FilterType.All);
const activeTodos = todos.filter(todo => !todo.completed);
const visibleTodos = todos.filter(todo => {
switch (filter) {
case FilterType.Active:
return !todo.completed;

case FilterType.Completed:
return todo.completed;

default:
return true;
}
});

const [query, setQuery] = React.useState('');
const inputRef = React.useRef<HTMLInputElement>(null);
const [tempTodo, setTempTodo] = React.useState<Todo | null>(null);
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [loadingTodoIds, setLoadingTodoIds] = React.useState<number[]>([]);
const [editingTodoId, setEditingTodoId] = React.useState<number | null>(null);
const [editingTitle, setEditingTitle] = React.useState('');
const [, setIsTodosLoading] = React.useState(true);

React.useEffect(() => {
setIsTodosLoading(true);

getTodos()
.then(setTodos)
.catch(() => {
setErrorMessage(ErrorMessage.LOAD_TODOS);
})
.finally(() => {
setIsTodosLoading(false);
});
}, []);

React.useEffect(() => {
if (!errorMessage) {
return;
}

const timer = setTimeout(() => {
setErrorMessage('');
}, 3000);

return () => clearTimeout(timer);
}, [errorMessage]);

React.useEffect(() => {
inputRef.current?.focus();
}, []);

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();

const trimmedTitle = query.trim();

if (!trimmedTitle) {
setErrorMessage(ErrorMessage.EMPTY_TITLE);

return;
}

const temporaryTodo: Todo = {
id: 0,
title: trimmedTitle,
completed: false,
userId: USER_ID,
};

setTempTodo(temporaryTodo);
setIsSubmitting(true);

try {
const createdTodo = await addTodo({
title: trimmedTitle,
completed: false,
userId: USER_ID,
});

setTodos(current => [...current, createdTodo]);
setQuery('');
} catch {
setErrorMessage(ErrorMessage.ADD_TODO);
} finally {
setTempTodo(null);
setIsSubmitting(false);
setTimeout(() => {
inputRef.current?.focus();
}, 0);
}
};

const preparedTodos = tempTodo ? [...visibleTodos, tempTodo] : visibleTodos;
const completedTodos = todos.filter(todo => todo.completed);

const handleDelete = async (id: number) => {
setLoadingTodoIds(prev => [...prev, id]);

try {
await deleteTodo(id);

setTodos(current => current.filter(todo => todo.id !== id));
} catch {
setErrorMessage(ErrorMessage.DELETE_TODO);
} finally {
setLoadingTodoIds(prev => prev.filter(todoId => todoId !== id));

inputRef.current?.focus();
}
};

const handleClearCompleted = async () => {
const completed = todos.filter(todo => todo.completed);

const promises = completed.map(todo => {
setLoadingTodoIds(prev => [...prev, todo.id]);

return deleteTodo(todo.id)
.then(() => {
setTodos(current => current.filter(t => t.id !== todo.id));
})
.catch(() => {
setErrorMessage(ErrorMessage.DELETE_TODO);
})
.finally(() => {
setLoadingTodoIds(prev => prev.filter(id => id !== todo.id));
});
});

await Promise.allSettled(promises);

inputRef.current?.focus();
};

const handleToggle = async (todo: Todo) => {
setLoadingTodoIds(prev => [...prev, todo.id]);

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

setTodos(current =>
current.map(t => (t.id === todo.id ? updatedTodo : t)),
);
} catch {
setErrorMessage(ErrorMessage.UPDATE_TODO);
} finally {
setLoadingTodoIds(prev => prev.filter(id => id !== todo.id));
}
};

const handleToggleAll = async () => {
const newStatus = activeTodos.length > 0;

const todosToUpdate = todos.filter(todo => todo.completed !== newStatus);

await Promise.all(
todosToUpdate.map(async todo => {
setLoadingTodoIds(prev => [...prev, todo.id]);

try {
const updatedTodo = await updateTodo(todo.id, {
completed: newStatus,
});

setTodos(current =>
current.map(t => (t.id === todo.id ? updatedTodo : t)),
);
} catch {
setErrorMessage(ErrorMessage.UPDATE_TODO);
} finally {
setLoadingTodoIds(prev => prev.filter(id => id !== todo.id));
}
}),
);
};

const handleRename = async (todo: Todo) => {
const trimmedTitle = editingTitle.trim();

if (trimmedTitle === todo.title) {
setEditingTodoId(null);

return;
}

if (!trimmedTitle) {
handleDelete(todo.id);

return;
}

setLoadingTodoIds(prev => [...prev, todo.id]);

try {
const updatedTodo = await updateTodo(todo.id, {
title: trimmedTitle,
});

setTodos(current =>
current.map(t => (t.id === todo.id ? updatedTodo : t)),
);

setEditingTodoId(null);
} catch {
setErrorMessage(ErrorMessage.UPDATE_TODO);
} finally {
setLoadingTodoIds(prev => prev.filter(id => id !== todo.id));
}
};

if (!USER_ID) {
return <UserWarning />;
}

const handleRenameSubmit = (
event: React.FormEvent<HTMLFormElement>,
todo: Todo,
) => {
event.preventDefault();
handleRename(todo);
};

const handleEditKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Escape') {
setEditingTodoId(null);
}
};

const handleStartEditing = (todo: Todo) => {
setEditingTodoId(todo.id);
setEditingTitle(todo.title);
};

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
hasTodos={todos.length > 0}
allCompleted={activeTodos.length === 0}
query={query}
isSubmitting={isSubmitting}
inputRef={inputRef}
onSubmit={handleSubmit}
onQueryChange={setQuery}
onToggleAll={handleToggleAll}
/>

<section className="todoapp__main" data-cy="TodoList">
{preparedTodos.map(todo => {
const isLoading = todo.id === 0 || loadingTodoIds.includes(todo.id);

return (
<div
key={todo.id}
data-cy="Todo"
className={classNames('todo', {
completed: todo.completed,
})}
>
<label className="todo__status-label">
<input
data-cy="TodoStatus"
type="checkbox"
className="todo__status"
checked={todo.completed}
onChange={() => handleToggle(todo)}
/>
</label>

{editingTodoId === todo.id ? (
<form onSubmit={e => handleRenameSubmit(e, todo)}>
<input
data-cy="TodoTitleField"
type="text"
className="todo__title-field"
value={editingTitle}
onChange={e => setEditingTitle(e.target.value)}
onBlur={() => handleRename(todo)}
onKeyUp={handleEditKeyUp}
autoFocus
/>
</form>
) : (
<span
data-cy="TodoTitle"
className="todo__title"
onDoubleClick={() => {
handleStartEditing(todo);
}}
Comment on lines +322 to +324

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

move to separate fn

>
{todo.title}
</span>
)}

{editingTodoId !== todo.id && (
<button
type="button"
className="todo__remove"
data-cy="TodoDelete"
onClick={() => handleDelete(todo.id)}
>
×
</button>
)}

<div
data-cy="TodoLoader"
className={classNames('modal overlay', {
hidden: !isLoading,
'is-active': isLoading,
})}
>
<div className="loader" />
</div>
</div>
);
})}
</section>

{todos.length > 0 && (
<Footer
activeCount={activeTodos.length}
completedCount={completedTodos.length}
filter={filter}
onFilterChange={setFilter}
onClearCompleted={handleClearCompleted}
/>
)}
</div>

<ErrorNotification
errorMessage={errorMessage}
onClose={() => setErrorMessage('')}
/>
</div>
);
};
Loading
Loading