Skip to content

Commit

Permalink
solution of react_todo-app-with-api v-1.0 need to solve part of tests
Browse files Browse the repository at this point in the history
  • Loading branch information
vatatan committed Sep 27, 2023
1 parent 5ab610a commit d47c7d3
Show file tree
Hide file tree
Showing 14 changed files with 686 additions and 14 deletions.
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://vatatan.github.io/react_todo-app-with-api/) and add it to the PR description.
264 changes: 252 additions & 12 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,264 @@
/* eslint-disable max-len */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
import classNames from 'classnames';

import { Todo } from './types/Todo';
import { Status } from './types/Status';
import { Error } from './types/Error';
import { getTodos, deleteTodo, createTodo } from './api/todos';
import { TodoList } from './components/TodoList/TodoList';
import { TodoFilter } from './components/TodoFilter/TodoFilter';
import { UserWarning } from './UserWarning';
import { TodoError } from './components/TodoError/TodoError';
import { TodoLoadingItem } from './components/TodoLoadingItem/TodoLoadingItem';
import { filterTodos, getItemsLeftCountMessage } from './utils/functions';
import { client } from './utils/fetchClient';

const USER_ID = 0;
const USER_ID = 11534;

export const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [status, setStatus] = useState<Status>(Status.All);
const [errorMessage, setErrorMessage] = useState(Error.None);
const [value, setValue] = useState('');
const [isSubmitted, setIsSubmitted] = useState(false);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [processingIds, setProcessingIds] = useState<number[]>([]);
const [areSubmiting, setAreSubmiting] = useState(false);
const [togglingId, setTogglingId] = useState<number | null>(null);
const [wasEdited, setWasEdited] = useState(false);

const inputRef = useRef<HTMLInputElement | null>(null);

useEffect(() => {
getTodos(USER_ID)
.then(setTodos)
.catch(() => setErrorMessage(Error.Load));
}, []);

useEffect(() => {
if (errorMessage) {
setTimeout(() => {
setErrorMessage(Error.None);
}, 3000);
}
}, [errorMessage]);

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

const visibleTodos = filterTodos(todos, status);

const activeTodosCount = todos.filter(todo => !todo.completed);
const completedTodosCount = todos.filter(todo => todo.completed);

function addTodo({ userId, title, completed }: Todo): Promise<void> {
setErrorMessage(Error.None);
setWasEdited(false);

return createTodo({ userId, title, completed })
.then(newTodo => {
setTodos(currentTodos => [...currentTodos, newTodo]);
})
.catch(() => {
setErrorMessage(Error.Add);
})
.finally(() => {
setWasEdited(true);
});
}

const handleSubmit = (event: React.ChangeEvent<HTMLFormElement>) => {
event.preventDefault();
setIsSubmitted(true);

const todoTitle = value.trim();

const newTodo = {
id: todos.length + 1,
title: todoTitle,
completed: false,
userId: USER_ID,
};

if (!todoTitle) {
setErrorMessage(Error.EmptyTitle);
setIsSubmitted(false);
} else {
addTodo(newTodo)
.then(() => {
setValue('');
})
.finally(() => {
setTempTodo(null);
setIsSubmitted(false);
});
}
};

const onDelete = (todoId: number) => {
setProcessingIds((prevIds) => [...prevIds, todoId]);
deleteTodo(todoId)
.then(() => setTodos(
currentTodos => currentTodos.filter(
todo => todo.id !== todoId,
),
))
.catch(() => setErrorMessage(Error.Delete))
.finally(() => setProcessingIds(
(prevIds) => prevIds.filter(id => id !== todoId),
));
};

const onDeleteCompleted = () => {
const allCompletedTodos = todos.filter(todo => todo.completed);

allCompletedTodos.forEach((todo) => {
onDelete(todo.id);
});
};

const onToggle = (todoId: number) => {
const todoToToggle = todos.find((todo) => todo.id === todoId);

setIsSubmitted(true);
setTogglingId(todoId);

if (!todoToToggle) {
return;
}

client
.patch(`/todos/${todoId}`, { completed: !todoToToggle.completed })
.catch(() => setErrorMessage(Error.Toggle))
.finally(() => {
setTogglingId(null);
setIsSubmitted(false);
});
};

const toggleAll = () => {
const allTodosAreCompleted = todos.length === completedTodosCount.length;

const promiseArray = (
allTodosAreCompleted
? completedTodosCount
: activeTodosCount).map((todo: { id: number; completed: boolean; }) => client.patch(`/todos/${todo.id}`, { completed: !todo.completed }));

setAreSubmiting(true);

Promise.all(promiseArray)
.then(() => {
setTodos(todos.map(todo => (
{ ...todo, completed: !allTodosAreCompleted }
)));
})
.catch(() => setErrorMessage(Error.Toggle))
.finally(() => setAreSubmiting(false));
};

const updateTodos = (todoId: number, data: Todo) => {
return client
.patch<Todo>(`/todos/${todoId}`, data)
.then(receivedTodo => {
setTodos(todos.map(todo => (todo.id === todoId ? receivedTodo : todo)));
})
.catch(() => setErrorMessage(Error.Update))
.finally(() => {
setTogglingId(null);
setIsSubmitted(false);
});
};

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 className="todoapp__header">
{todos.length !== 0 && (
<button
type="button"
data-cy="ToggleAllButton"
className={classNames('todoapp__toggle-all', {
active: todos.every(todo => todo.completed),
})}
onClick={toggleAll}
/>
)}

<form onSubmit={handleSubmit}>
<input
data-cy="NewTodoField"
type="text"
className="todoapp__new-todo"
placeholder="What needs to be done?"
value={value}
ref={inputRef}
onChange={(event) => setValue(event.target.value)}
disabled={isSubmitted}
onBlur={() => setWasEdited(false)}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
/>
</form>
</header>

{!!todos.length && (
<>
<TodoList
todos={visibleTodos}
onDelete={onDelete}
processingIds={processingIds}
onToggle={onToggle}
togglingId={togglingId}
onUpdate={updateTodos}
isSubmitted={isSubmitted}
areSubmiting={areSubmiting}
/>

{tempTodo && (
<TodoLoadingItem
tempTodo={tempTodo}
isSubmitted={isSubmitted}
/>
)}
<footer className="todoapp__footer" data-cy="Footer">
<span className="todo-count" data-cy="TodosCounter">
{getItemsLeftCountMessage(activeTodosCount)}
</span>
<TodoFilter
todosFilterStatus={status}
handleFilterStatus={setStatus}
/>
<button
data-cy="ClearCompletedButton"
type="button"
className="todoapp__clear-completed"
disabled={!completedTodosCount.length}
onClick={onDeleteCompleted}
>
Clear completed
</button>
</footer>
</>
)}

</div>

<TodoError
errorMessage={errorMessage}
onErrorChange={() => {
setErrorMessage(Error.None);
}}
/>
</div>
);
};
2 changes: 1 addition & 1 deletion src/UserWarning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const UserWarning: React.FC = () => (
{' '}
and save it in the app
{' '}
<pre>const USER_ID = ...</pre>
<pre>const USER_ID = 11534</pre>

All requests to the API must be sent with this
<b> userId.</b>
Expand Down
14 changes: 14 additions & 0 deletions src/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Todo } from '../types/Todo';
import { client } from '../utils/fetchClient';

export const getTodos = (userId: number) => {
return client.get<Todo[]>(`/todos?userId=${userId}`);
};

export const createTodo = ({ title, completed, userId }: Omit<Todo, 'id'>) => {
return client.post<Todo>('/todos', { title, completed, userId });
};

export const deleteTodo = (todoId: number) => {
return client.delete(`/todos/${todoId}`);
};
34 changes: 34 additions & 0 deletions src/components/TodoError/TodoError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import classNames from 'classnames';
import { Error } from '../../types/Error';

type Props = {
errorMessage: Error,
onErrorChange: () => void
};

export const TodoError: React.FC<Props> = ({ errorMessage, onErrorChange }) => {
return (
<div
data-cy="ErrorNotification"
className={classNames(
'notification',
'is-danger',
'is-light',
'has-text-weight-normal',
{
hidden: !errorMessage,
},
)}
>
<button
data-cy="HideErrorButton"
type="button"
className="delete"
onClick={onErrorChange}
/>
{errorMessage}
</div>
);
};
49 changes: 49 additions & 0 deletions src/components/TodoFilter/TodoFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import classNames from 'classnames';
import { Status } from '../../types/Status';

type Props = {
handleFilterStatus: (status: Status) => void,
todosFilterStatus: Status
};

export const TodoFilter: React.FC<Props> = ({
handleFilterStatus,
todosFilterStatus,
}) => {
return (
<nav className="filter" data-cy="Filter">
<a
href="#/"
data-cy="FilterLinkAll"
className={classNames('filter__link', {
selected: todosFilterStatus === Status.All,
})}
onClick={() => handleFilterStatus(Status.All)}
>
{Status.All}
</a>

<a
href="#/active"
data-cy="FilterLinkActive"
className={classNames('filter__link', {
selected: todosFilterStatus === Status.Active,
})}
onClick={() => handleFilterStatus(Status.Active)}
>
{Status.Active}
</a>

<a
href="#/completed"
data-cy="FilterLinkCompleted"
className={classNames('filter__link', {
selected: todosFilterStatus === Status.Completed,
})}
onClick={() => handleFilterStatus(Status.Completed)}
>
{Status.Completed}
</a>
</nav>
);
};
Loading

0 comments on commit d47c7d3

Please sign in to comment.