Skip to content
Open

PR #2278

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
6 changes: 3 additions & 3 deletions cypress/integration/page.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ describe('', () => {
});

// this test may be flaky
it.skip('should replace loader with a created todo', () => {
it('should replace loader with a created todo', () => {
page.flushJSTimers();
todos.assertCount(6);
todos.assertNotLoading(5);
Expand Down Expand Up @@ -1515,7 +1515,7 @@ describe('', () => {
});

// It depend on your implementation
it.skip('should stay while waiting', () => {
it('should stay while waiting', () => {
page.mockUpdate(257334);

todos.title(0).trigger('dblclick');
Expand Down Expand Up @@ -1694,7 +1694,7 @@ describe('', () => {
});

// this test may be unstable
it.skip('should hide loader on fail', () => {
it('should hide loader on fail', () => {
// to prevent Cypress from failing the test on uncaught exception
cy.once('uncaught:exception', () => false);

Expand Down
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
301 changes: 286 additions & 15 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,297 @@
/* 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 React, { useEffect, useRef, useState } from 'react';
import { UserWarning } from './UserWarning';

const USER_ID = 0;
import {
addTodos,
changeTodos,
deleteTodos,
getTodos,
USER_ID,
} from './api/todos';
import { Todo } from './types/Todo';
import { TodoList } from './Components/TodoList/TodoList';
import { Footer } from './Components/Footer/Footer';
import { SortType } from './types/SortType';
/* eslint-disable-next-line max-len */
import { ErrorNotification } from './Components/ErrorNotification/ErrorNotification';
import { Header } from './Components/Header/Header';

export const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [sortType, setSortType] = useState<SortType>(SortType.all);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [changePostsId, setChangePostsId] = useState<number[]>([]);
const inputRef = useRef<HTMLInputElement>(null);

const [errorMessage, setErrorMessage] = useState('');

useEffect(() => {
setErrorMessage('');

getTodos()
.then(todosFromServer => setTodos(todosFromServer))
.catch(() => {
setErrorMessage('Unable to load todos');
setTimeout(() => {
setErrorMessage('');
}, 3000);

throw new Error();
});
}, []);

const filteredTodos = todos.filter(todo => {
switch (sortType) {
case SortType.Active: {
return !todo.completed;
}

case SortType.Completed: {
return todo.completed;
}

case SortType.all:
default:
return todo;
}
});

function addDataToServer(listValue: string) {
setErrorMessage('');
const normalValue = listValue.trim();

if (!normalValue) {
setErrorMessage('Title should not be empty');

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

return Promise.resolve(false);
}

const newTodoData = {
title: listValue,
completed: false,
userId: USER_ID,
};

setTempTodo({
id: 0,
...newTodoData,
});

return addTodos(newTodoData)
.then(newTodo => {
setTodos(currentTodo => [...currentTodo, newTodo]);

return true;
})
.catch(() => {
setErrorMessage('Unable to add a todo');

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

return false;
})
.finally(() => {
setTempTodo(null);
});
}

function deleteDataFromServer(postId: number) {
setErrorMessage('');
setChangePostsId(currentIds => [...currentIds, postId]);

if (!postId) {
setErrorMessage('Title should not be empty to delete');

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

return Promise.reject(); // <--- ДОДАНО: повертаємо відхилений проміс
}

return deleteTodos(postId) // <--- ДОДАНО: return, щоб проміс пішов наверх
.then(() => {
setTodos(currentTodos =>
currentTodos.filter(todo => todo.id !== postId),
);
})
.catch(() => {
setErrorMessage('Unable to delete a todo');

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

throw new Error(); // <--- ДОДАНО: прокидаємо помилку далі
})
.finally(() => {
setChangePostsId(idOfPosts => idOfPosts.filter(id => id !== postId));
inputRef.current?.focus();
});
}

function deleteAllCompletedFromServer() {
const completedId = todos
.filter(todoFromArray => todoFromArray.completed)
.map(todo => todo.id);

for (const id of completedId) {
deleteDataFromServer(id);
}
}

function changeStatusToServer(changeId: number) {
setErrorMessage('');
setChangePostsId(currentIds => [...currentIds, changeId]);
const searcData = todos.find(todo => todo.id === changeId) || null;

if (!searcData) {
setErrorMessage('Unable to update a todo');

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

return;
}

const changePost = {
...searcData,
completed: !searcData.completed,
};

changeTodos(changePost)
.then(updatedTodo =>
setTodos(currentPosts => {
return currentPosts.map(post =>
post.id === updatedTodo.id ? updatedTodo : post,
);
}),
)
.catch(() => {
setErrorMessage('Unable to update a todo');

setTimeout(() => {
setErrorMessage('');
}, 3000);
})
.finally(() => {
setChangePostsId(currentIds =>
currentIds.filter(currentId => currentId !== changeId),
);
});
}

function changeAllStatusToServer() {
let targetTodos = todos.filter(todo => !todo.completed);

if (targetTodos.length === 0) {
targetTodos = todos;
}

const changesId = targetTodos.map(todo => todo.id);

for (const id of changesId) {
changeStatusToServer(id);
}
}

function renameTodoData(todoId: number, newTitle: string) {
setErrorMessage('');
setChangePostsId(currentIds => [...currentIds, todoId]);

const searchData = todos.find(todo => todo.id === todoId);

if (!searchData) {
setErrorMessage('Unable to update a todo');
setTimeout(() => setErrorMessage(''), 3000);

return Promise.reject();
}

const updatedPost = {
...searchData,
title: newTitle,
};

return changeTodos(updatedPost)
.then(updatedTodo =>
setTodos(currentPosts => {
return currentPosts.map(post =>
post.id === updatedTodo.id ? updatedTodo : post,
);
}),
)
.catch(() => {
setErrorMessage('Unable to update a todo');

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

throw new Error();
})
.finally(() => {
setChangePostsId(currentIds =>
currentIds.filter(currentId => currentId !== todoId),
);
});
}

const activeTodoCount = todos.filter(todo => !todo.completed).length;

const hasCompletedTodos = todos.length > activeTodoCount;

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

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
todos={todos}
active={activeTodoCount}
onChange={addDataToServer}
inputRef={inputRef}
changeAll={changeAllStatusToServer}
/>

{todos.length > 0 && (
<>
<TodoList
filteredTodos={filteredTodos}
tempTodo={tempTodo}
deleteId={changePostsId}
deleteData={deleteDataFromServer}
changeStatusData={changeStatusToServer}
renameData={renameTodoData}
/>

<Footer
activeTodosCount={activeTodoCount}
currentSortType={sortType}
hasCompletedTodos={hasCompletedTodos}
onSortChange={setSortType}
deletedAllCompleted={deleteAllCompletedFromServer}
/>
</>
)}
</div>

<ErrorNotification
error={errorMessage}
setError={catchError => setErrorMessage(catchError)}
/>
</div>
);
};
22 changes: 22 additions & 0 deletions src/Components/ErrorNotification/ErrorNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import classNames from 'classnames';
import { ErrorProps } from '../../types/ErrorProps';

export const ErrorNotification = ({ error, setError }: ErrorProps) => {
return (
<div
data-cy="ErrorNotification"
className={classNames(
`notification is-danger is-light has-text-weight-normal ${!error ? 'hidden' : ''}`,
)}
>
<button
data-cy="HideErrorButton"
type="button"
className="delete"
onClick={() => setError('')}
/>
{/* show only one message at a time */}
{error}
</div>
);
};
Loading
Loading