Skip to content

Commit

Permalink
updated
Browse files Browse the repository at this point in the history
  • Loading branch information
didarie committed Dec 19, 2024
1 parent c4ee1d7 commit 4eca8eb
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 237 deletions.
156 changes: 62 additions & 94 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React, { useEffect, useMemo, useState } from 'react';
import { UserWarning } from './components/UserWarning/UserWarning';
import React, { useEffect, useState } from 'react';
import { UserWarning } from './UserWarning';
import { getTodos, USER_ID } from './api/todos';
import { Todo } from './types/Todo';
import { TodoFilter } from './types/TodoFilter';
import classNames from 'classnames';
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 [loading, setLoading] = useState(false);
const [loadingId, setLoadingId] = useState<number | number[]>(0);
const [todosInProcess, setTodosInProcess] = useState<number[]>([0]);

// #region loadTodos
const loadTodos = async () => {
Expand Down Expand Up @@ -56,111 +55,98 @@ export const App: React.FC = () => {
}
});

const completedTodos = useMemo(() => {
return todos.filter(todo => todo.completed);
}, [todos]);

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

// #endregion

// #region todoServices
const addTodo = async (newTodo: Todo) => {
setLoading(true);

setLoadingId(newTodo.id);

try {
const addedTodo = await todoServices.addTodo(newTodo);

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

const deleteTodo = async (id: number | number[]) => {
setLoading(true);

const ids = Array.isArray(id) ? id : [id];

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

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

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

return null;
}
});
const results = await Promise.all(deletePromise);
return null;
}
}),
);

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

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

const updateTodo = async (updatedTodo: Todo | Todo[]) => {
setLoading(true);

const ids = !Array.isArray(updatedTodo)
? updatedTodo.id
: updatedTodo.map(todo => todo.id);

setLoadingId(ids);

const handleUpdatedTodo = ({
title,
id,
completed,
}: Omit<Todo, 'userId'>) => {
return todoServices.updatedTodo({
title,
id,
completed,
});
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 updatedTodos = await Promise.all(
updatedTodo.map(todo => handleUpdatedTodo(todo)),
);

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

return updatedTodoItem ? updatedTodoItem : prevTodo;
}),
);
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 {
await handleUpdatedTodo(updatedTodo);
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === updatedTodo.id ? updatedTodo : todo,
),
);
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 {
setLoading(false);
setTodosInProcess(currentId => currentId.filter(id => !ids.includes(id)));
}
};

// #endregion

return (
<div className="todoapp">
<h1 className="todoapp__title">todos</h1>
Expand All @@ -169,50 +155,32 @@ export const App: React.FC = () => {
<TodoHeader
todos={todos}
onError={setError}
onSubmit={addTodo}
onTempTodo={setTempTodo}
onCompleted={updateTodo}
onAdd={addTodo}
onUpdate={updateTodo}
/>

{/* Hide the footer if there are no todos */}

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

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

<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={() => setError('')}
/>
{error}
</div>
<ErrorNotification error={error} onError={setError} />
</div>
);
};
File renamed without changes.
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';
24 changes: 14 additions & 10 deletions src/components/TodoFooter/TodoFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
import classNames from 'classnames';
import React from 'react';
import React, { useMemo } from 'react';
import { Todo } from '../../types/Todo';
import { TodoFilter } from '../../types/TodoFilter';

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

export const TodoFooter: React.FC<Props> = ({
completedTodos,
todos,
filter,
onChange,
onFilter,
onDelete,
activeItems,
}) => {
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>

{/* Active link should have the 'selected' class */}
<nav className="filter" data-cy="Filter">
{Object.values(TodoFilter).map(filterOption => (
<a
Expand All @@ -34,14 +39,13 @@ export const TodoFooter: React.FC<Props> = ({
selected: filterOption === filter,
})}
data-cy={`FilterLink${filterOption.charAt(0).toUpperCase() + filterOption.slice(1)}`}
onClick={() => onChange(filterOption)}
onClick={() => onFilter(filterOption)}
>
{filterOption.charAt(0).toUpperCase() + filterOption.slice(1)}
</a>
))}
</nav>

{/* this button should be disabled if there are no completed todos */}
<button
type="button"
className="todoapp__clear-completed"
Expand Down
Loading

0 comments on commit 4eca8eb

Please sign in to comment.