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

Develop #1551

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open

Develop #1551

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

const USER_ID = 0;
import { getTodos, USER_ID } from './api/todos';
import { Todo } from './types/Todo';
import { TodoFilter } from './types/TodoFilter';
import { TodoList } from './components/TodoList';
import { TodoFooter } from './components/TodoFooter';
import * as todoServices from './api/todos';
import { TodoHeader } from './components/TodoHeader';
import { ErrorNotification } from './components/ErrorNotification';

export const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [error, setError] = useState('');
const [filter, setFilter] = useState<TodoFilter>(TodoFilter.All);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [todosInProcess, setTodosInProcess] = useState<number[]>([0]);

// #region loadTodos
const loadTodos = async () => {
try {
const loadedTodos = await getTodos();

setTodos(loadedTodos);
} catch {
setError('Unable to load todos');
}
};

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

useEffect(() => {
if (error !== '') {
setTimeout(() => {
setError('');
}, 3000);
}
}, [error]);

// #endregion

// #region filter
const filteredTodos = todos.filter(todo => {
switch (filter) {
case TodoFilter.Active:
return !todo.completed;
case TodoFilter.Completed:
return todo.completed;
default:
return true;
}
});

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

// #endregion
// #region todoServices
const addTodo = async (newTodo: Todo) => {
try {
const addedTodo = await todoServices.addTodo(newTodo);

setTodos(prevTodos => [...prevTodos].concat(addedTodo));
} finally {
setTempTodo(null);
}
};

const deleteTodo = async (id: number | number[]) => {
const ids = Array.isArray(id) ? id : [id];

setTodosInProcess(currentTodo => [...currentTodo, ...ids]);

try {
const results = await Promise.all(
ids.map(async i => {
try {
await todoServices.deleteTodo(i);

return i;
} catch {
setError('Unable to delete a todo');

return null;
}
}),
);

const successDelete = results.filter(result => result !== null);

setTodos(todos.filter(todo => !successDelete.includes(todo.id)));
} finally {
setTodosInProcess(currentId => currentId.filter(i => !ids.includes(i)));
}
};

const updateTodo = async (updatedTodo: Todo | Todo[]) => {
const ids = Array.isArray(updatedTodo)
? updatedTodo.map(todo => todo.id)
: [updatedTodo.id];

setTodosInProcess(currentTodo => [...currentTodo, ...ids]);

const handleUpdatedTodo = async (todo: Omit<Todo, 'userId'>) => {
return todoServices.updatedTodo(todo);
};

try {
let updatedTodos: Todo[] = [];

if (Array.isArray(updatedTodo)) {
const updatePromises = updatedTodo.map(todo => handleUpdatedTodo(todo));
const results = await Promise.allSettled(updatePromises);

updatedTodos = results
.filter(
(result): result is PromiseFulfilledResult<Todo> =>
result.status === 'fulfilled',
)
.map(result => result.value);
} else {
const updated = await handleUpdatedTodo(updatedTodo);

updatedTodos = updated ? [updated] : [];
}

setTodos(prevTodos =>
prevTodos.map(prevTodo => {
const updatedTodoItem = updatedTodos.find(
todo => prevTodo.id === todo.id,
);

return updatedTodoItem ? updatedTodoItem : prevTodo;
}),
);
} catch {
setError('Unable to update a todo');
} finally {
setTodosInProcess(currentId => currentId.filter(id => !ids.includes(id)));
}
};

// #endregion

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">
<TodoHeader
todos={todos}
onError={setError}
onTempTodo={setTempTodo}
onAdd={addTodo}
onUpdate={updateTodo}
/>

{(todos.length > 0 || tempTodo) && (
<TodoList
todos={filteredTodos}
tempTodo={tempTodo}
todosInProcess={todosInProcess}
onUpdate={updateTodo}
onDelete={deleteTodo}
/>
)}

{(todos.length > 0 || tempTodo) && (
<TodoFooter
todos={todos}
filter={filter}
onFilter={setFilter}
onDelete={deleteTodo}
/>
)}
</div>

<ErrorNotification error={error} onError={setError} />
</div>
);
};
22 changes: 22 additions & 0 deletions src/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Todo } from '../types/Todo';
import { client } from '../utils/fetchClient';

export const USER_ID = 2038;

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

// Add more methods here

export const addTodo = ({ title, userId, id, completed }: Todo) => {
return client.post<Todo[]>(`/todos`, { title, userId, id, completed });
};

export const deleteTodo = (id: number) => {
return client.delete(`/todos/${id}`);
};

export const updatedTodo = ({ title, id, completed }: Omit<Todo, 'userId'>) => {
return client.patch<Todo>(`/todos/${id}`, { title, completed });
};
27 changes: 27 additions & 0 deletions src/components/ErrorNotification/ErrorNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import classNames from 'classnames';
import React from 'react';

interface Props {
error: string;
onError: (error: string) => void;
}

export const ErrorNotification: React.FC<Props> = ({ error, onError }) => {
return (
<div
data-cy="ErrorNotification"
className={classNames(
'notification is-danger is-light has-text-weight-normal',
{ hidden: !error },
)}
>
<button
data-cy="HideErrorButton"
type="button"
className="delete"
onClick={() => onError('')}
/>
{error}
</div>
);
};
1 change: 1 addition & 0 deletions src/components/ErrorNotification/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ErrorNotification';
35 changes: 35 additions & 0 deletions src/components/TempTodo/TempTodo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { Todo } from '../../types/Todo';

interface Props {
tempTitle: Todo;
}

export const TempTodo: React.FC<Props> = ({ tempTitle: { title } }) => {
return (
<div data-cy="Todo" className="todo">
<label className="todo__status-label">
<input
id={`todo-status`}
data-cy="TodoStatus"
type="checkbox"
className="todo__status"
disabled
/>
{/*accessible text*/}
</label>

<span data-cy="TodoTitle" className="todo__title">
{title}
</span>
<button type="button" className="todo__remove" data-cy="TodoDelete">
×
</button>

<div data-cy="TodoLoader" className="modal overlay is-active">
<div className="modal-background has-background-white-ter" />
<div className="loader" />
</div>
</div>
);
};
1 change: 1 addition & 0 deletions src/components/TempTodo/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TempTodo';
60 changes: 60 additions & 0 deletions src/components/TodoFooter/TodoFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import classNames from 'classnames';
import React, { useMemo } from 'react';
import { Todo } from '../../types/Todo';
import { TodoFilter } from '../../types/TodoFilter';

interface Props {
todos: Todo[];
filter: string;
onFilter: (filter: TodoFilter) => void;
onDelete: (arrayId: number[]) => Promise<void>;
}

export const TodoFooter: React.FC<Props> = ({
todos,
filter,
onFilter,
onDelete,
}) => {
const completedTodos = useMemo(() => {
return todos.filter(todo => todo.completed);
}, [todos]);

const activeItems = useMemo(() => {
return todos.length - completedTodos.length;
}, [completedTodos, todos]);

return (
<footer className="todoapp__footer" data-cy="Footer">
<span className="todo-count" data-cy="TodosCounter">
{activeItems} items left
</span>

<nav className="filter" data-cy="Filter">
{Object.values(TodoFilter).map(filterOption => (
<a
key={filterOption}
href="#/"
className={classNames('filter__link', {
selected: filterOption === filter,
})}
data-cy={`FilterLink${filterOption.charAt(0).toUpperCase() + filterOption.slice(1)}`}
onClick={() => onFilter(filterOption)}
>
{filterOption.charAt(0).toUpperCase() + filterOption.slice(1)}
</a>
))}
</nav>

<button
type="button"
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
disabled={completedTodos.length === 0}
onClick={() => onDelete(completedTodos.map(todo => todo.id))}
>
Clear completed
</button>
</footer>
);
};
1 change: 1 addition & 0 deletions src/components/TodoFooter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TodoFooter';
Loading
Loading