Skip to content
Open

Develop #2186

Show file tree
Hide file tree
Changes from 2 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://Bohdan259.github.io/react_todo-app-with-api/) and add it to the PR description.
129 changes: 39 additions & 90 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
"@mate-academy/scripts": "^1.8.5",
"@mate-academy/scripts": "^2.1.3",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
Expand Down
366 changes: 349 additions & 17 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,358 @@
/* 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';

const USER_ID = 0;
import React, { useEffect, useState } from 'react';
import {
deleteTodo,
editingTodo,
getTodos,
postTodo,
updateTodo,
USER_ID,
} from './api/todos';
import { TodoAppHeader } from './components/TodoAppHeader';
import { TodoAppMain } from './components/TodoAppMain';
import { TodoMainFooter } from './components/TodoMainFooter';
import classNames from 'classnames';
import { FilterOptions } from './components/TodoMainFooter/TodoMainFooter';
import { EditTodo, Todo } from './types/Todo';
import { ErrorMessage } from './types/ErrorMessage';

export const App: React.FC = () => {
const [notificationError, setNotificationError] = useState(false);

const [errorTodos, setErrorTodos] = useState(false);
const [errorTitle, setErrorTitle] = useState(false);
const [errorPostTodo, setErrorPostTodo] = useState(false);
const [loadingPostTodo, setLoadingPostTodo] = useState(false);

const [filterTodos, setFilterTodos] = useState<FilterOptions>('all');
const [todos, setTodos] = useState<Todo[]>([]);
const [loadingTodos, setLoadingTodos] = useState(false);
const [counter, setCounter] = useState(0);

const [titleTodo, setTitleTodo] = useState('');
const [tempTodo, setTempTodo] = useState<Todo | null>(null);

const [loaderDelete, setLoaderDelete] = useState(false);
const [selectedDeleteTodo, setSelectedDeleteTodo] = useState<number | null>(
null,
);
const [deleteError, setDeleteError] = useState(false);
const [clearButton, setClearButton] = useState(false);
const [loaderDeleteCompleted, setLoaderDeleteCompleted] = useState(false);

const [toggleTodo, setToggleTodo] = useState<Todo | null>(null);
const [loaderToggle, setLoaderToggle] = useState(false);
const [toggleError, setToggleError] = useState(false);
const [isClickToggleAllButton, setsIClickToggleAllButton] = useState(false);
const [loadingIds, setLoadingIds] = useState<number[]>([]);
const [editTodo, setEditTodo] = useState<EditTodo | null>(null);
const [selectedUpdateTodo, setSelectedUpdateTodo] = useState<number | null>(
null,
);

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

const visibleTodos = todos.filter(todo => {
if (filterTodos === 'active') {
return todo.completed === false;
}

if (filterTodos === 'completed') {
return todo.completed;
}

return todos;
});

function loadTodos() {
getTodos()
.then(serverTodos => {
setTodos(serverTodos);
setCounter(serverTodos.length);
})
.catch(() => {
setErrorTodos(true);
setNotificationError(true);
})
.finally(() => {
setLoadingTodos(false);
setTimeout(() => {
setNotificationError(false);
setErrorTodos(false);
}, 3000);
});
}

useEffect(() => {
setLoadingTodos(true);
loadTodos();
}, []);

function addTodo(newTodo: Todo) {
setLoadingPostTodo(true);
const { id, ...data } = newTodo;

postTodo(data)
.then(serverTodo => {
setTempTodo(serverTodo);
setTodos(currentTodo => [...currentTodo, serverTodo]);
setCounter(currentCounter => currentCounter + 1);
})
.catch(() => {
setNotificationError(true);
setErrorPostTodo(true);
})
.finally(() => {
setTempTodo(null);
setLoadingPostTodo(false);

setTimeout(() => {
setNotificationError(false);
setErrorPostTodo(false);
}, 3000);
});
}

useEffect(() => {
if (tempTodo) {
addTodo(tempTodo);
}
}, [tempTodo]);

useEffect(() => {
if (notificationError) {
setTimeout(() => {
setNotificationError(false);
}, 3000);
}
}, [notificationError]);

function deleteById(todoId: number, isSingle = false) {
return deleteTodo(todoId)
.then(() => {
setTodos(currentTodos =>
currentTodos.filter(todo => todo.id !== todoId),
);
setCounter(currentCounter => currentCounter - 1);
})
.catch(() => {
setNotificationError(true);
setDeleteError(true);
})
.finally(() => {
if (isSingle) {
setLoaderDelete(false);
setSelectedDeleteTodo(null);
}

setTimeout(() => {
setNotificationError(false);
setDeleteError(false);
}, 3000);
});
}

useEffect(() => {
if (selectedDeleteTodo) {
setLoaderDelete(true);
deleteById(selectedDeleteTodo, true);
}
}, [selectedDeleteTodo]);

function deletAllCompletedTodos(todosArr: Todo[]) {
const completedTodos = todosArr.filter(todo => todo.completed);

Promise.all(completedTodos.map(todo => deleteById(todo.id))).finally(() => {
setLoaderDeleteCompleted(false);
setClearButton(false);
});
}

useEffect(() => {
if (clearButton) {
setLoaderDeleteCompleted(true);
deletAllCompletedTodos(todos);
}
}, [clearButton]);

Check warning on line 177 in src/App.tsx

View workflow job for this annotation

GitHub Actions / run_linter (20.x)

React Hook useEffect has missing dependencies: 'deletAllCompletedTodos' and 'todos'. Either include them or remove the dependency array

function todoUpdateById(todo: Todo, isSingle = false) {
const { id, completed } = todo;

return updateTodo({ id, completed })
.then(newTodo => {
setTodos(currentTodos => {
const newPosts = [...currentTodos];
const index = currentTodos.findIndex(t => t.id === newTodo.id);

newPosts.splice(index, 1, newTodo);

return newPosts;
});
})
.catch(() => {
setNotificationError(true);
setToggleError(true);
})
.finally(() => {
if (isSingle) {
setLoaderToggle(false);
setToggleTodo(null);
}

setTimeout(() => {
setNotificationError(false);
setToggleError(false);
}, 3000);
});
}

useEffect(() => {
if (toggleTodo) {
setLoaderToggle(true);
todoUpdateById(toggleTodo, true);
}
}, [toggleTodo]);

function toggleAll() {
const todosToUpdate = includesFalseCompleted
? todos
: todos.filter(t => !t.completed);

setsIClickToggleAllButton(false);
setLoadingIds(todosToUpdate.map(t => t.id));

Promise.all(todosToUpdate.map(todo => todoUpdateById(todo))).finally(() => {
setLoadingIds([]);
});
}

useEffect(() => {
if (isClickToggleAllButton) {
toggleAll();
}
}, [isClickToggleAllButton]);

Check warning on line 234 in src/App.tsx

View workflow job for this annotation

GitHub Actions / run_linter (20.x)

React Hook useEffect has a missing dependency: 'toggleAll'. Either include it or remove the dependency array

function updateEditTodo(todo: EditTodo) {
const { id, title } = todo;

editingTodo({ id, title })
.then(newTodo => {
setTodos(currentTodos => {
const newPosts = [...currentTodos];
const index = currentTodos.findIndex(t => t.id === newTodo.id);

newPosts.splice(index, 1, newTodo);

return newPosts;
});
setSelectedUpdateTodo(null);
})
.catch(() => {
setNotificationError(true);
setToggleError(true);
})
.finally(() => {
setEditTodo(null);
setTimeout(() => {
setNotificationError(false);
setToggleError(false);
}, 3000);
});
}

useEffect(() => {
if (editTodo) {
updateEditTodo(editTodo);
}
}, [editTodo]);

if (!USER_ID) {
return <UserWarning />;
return (
<section className="section">
<p className="box is-size-3">
Please get your <b> userId </b>{' '}
<a href="https://mate-academy.github.io/react_student-registration">
here
</a>{' '}
and save it in the app <pre>const USER_ID = ...</pre>
All requests to the API must be sent with this
<b> userId.</b>
</p>
</section>
);
}

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">
<TodoAppHeader
todos={todos}
includesFalseCompleted={includesFalseCompleted}
loaderClearButton={loaderDeleteCompleted}
loaderDeleteButton={loaderDelete}
loadingPostTodo={loadingPostTodo}
title={titleTodo}
setTitle={setTitleTodo}
setNotificationError={setNotificationError}
setTempTodo={setTempTodo}
setErrorTitle={setErrorTitle}
errorPostTodo={errorPostTodo}
onClickToggleAll={setsIClickToggleAllButton}
/>

<TodoAppMain
selectedUpdateTodo={selectedUpdateTodo}
setSelectedUpdateTodo={setSelectedUpdateTodo}
editTodo={editTodo}
setEditTodo={setEditTodo}
loadingIds={loadingIds}
toggleTodo={toggleTodo}
loaderToggle={loaderToggle}
setToggleTodo={setToggleTodo}
todos={visibleTodos}
tempTodo={tempTodo}
loaderDelete={loaderDelete}
onSelectedTodo={setSelectedDeleteTodo}
selectedDeleteTodo={selectedDeleteTodo}
loaderClearButton={loaderDeleteCompleted}
/>

{todos.length > 0 && (
<TodoMainFooter
filterTodos={filterTodos}
todos={todos}
onFilterTodos={setFilterTodos}
setTodos={setTodos}
counter={counter}
setClearButton={setClearButton}
/>
)}
</div>

{/* DON'T use conditional rendering to hide the notification */}
{/* Add the 'hidden' class to hide the message smoothly */}
<div
data-cy="ErrorNotification"
className={classNames(
'notification is-danger is-light has-text-weight-normal',
{ hidden: !notificationError },
)}
>
<button
data-cy="HideErrorButton"
type="button"
className="delete"
onClick={() => setNotificationError(false)}
/>
{errorTodos && !loadingTodos && <span>{ErrorMessage.LoadTodos}</span>}
{errorTitle && <span>{ErrorMessage.EmptyTitle}</span>}
{errorPostTodo && <span>{ErrorMessage.AddTodo}</span>}
{deleteError && <span>{ErrorMessage.DeleteTodo}</span>}
{toggleError && <span>{ErrorMessage.ToggleTodo}</span>}
</div>
</div>
);
};
Loading
Loading