Skip to content

Commit

Permalink
Copied code from previous task and deleted logic with loader (changin…
Browse files Browse the repository at this point in the history
…g status)
  • Loading branch information
Iryna Mariiko authored and Iryna Mariiko committed Dec 25, 2024
1 parent 62dbb71 commit e21709c
Show file tree
Hide file tree
Showing 17 changed files with 3,543 additions and 2,168 deletions.
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:/IrynaMariiko00.github.io/react_todo-app-with-api/) and add it to the PR description.
5,184 changes: 3,033 additions & 2,151 deletions package-lock.json

Large diffs are not rendered by default.

177 changes: 161 additions & 16 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,171 @@
/* eslint-disable max-len */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import React, { useEffect, useState, useMemo, useRef } from 'react';
import { UserWarning } from './UserWarning';

const USER_ID = 0;
import { Header } from './components/Header';
import { Todo } from './types/Todo';
import { getTodos, addTodos, deleteTodos, USER_ID } from './api/todos';
import { TodoItem } from './components/TodoItem';
import { Errors } from './components/Errors';
import { Footer } from './components/Footer';
import { filterTodos } from './utils/FilterTodo';
import { FilterBy } from './types/FilterBy';
import { ErrorMessage } from './types/ErrorMessage';

export const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [error, setError] = useState('');
const [currentFilter, setCurrentFilter] = useState<FilterBy>(FilterBy.All);
const [currentTodoIds, setCurrentTodoIds] = useState<number[]>([]);
const [isInputDisabled, setIsInputDisabled] = useState(false);
const [query, setQuery] = useState('');
const inputRef = useRef<HTMLInputElement | null>(null);

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

getTodos()
.then(setTodos)
.catch(() => {
setError('Unable to load todos');
});
}, []);

const filtered = useMemo(
() => filterTodos(todos, currentFilter),
[todos, currentFilter],
);

const handleNewTodo = (newTodo: Todo) => {
const newTodoWithStatus = { ...newTodo, isPending: true };
const newTodosList = [...todos, newTodoWithStatus];

setIsInputDisabled(true);
setCurrentTodoIds([...currentTodoIds, newTodo.id]);
setTodos(newTodosList);

addTodos(newTodo)
.then(todoFromServer => {
setTodos(currentTodos =>
currentTodos.map(todo =>
todo.id === newTodo.id
? { ...todo, id: todoFromServer.id, isPending: false }
: todo,
),
);
setQuery('');
})
.catch(() => {
setTodos(currentTodos =>
currentTodos.filter(todo => todo.id !== newTodo.id),
);
setError(ErrorMessage.Add);
})
.finally(() => {
setCurrentTodoIds([]);
setIsInputDisabled(false);
});
};

const handleDeleteTodo = (todoId: number) => {
setCurrentTodoIds([...currentTodoIds, todoId]);
deleteTodos(todoId)
.then(() => {
setTodos(todos.filter(todo => todo.id !== todoId));
})
.catch(() => setError(ErrorMessage.Delete))
.finally(() => setCurrentTodoIds([]));
};

useEffect(() => {
if (inputRef.current && !isInputDisabled) {
inputRef.current.focus();
}
}, [isInputDisabled, inputRef, todos]);

const activeTodos = todos.filter(todo => !todo.completed && !todo.isPending);

const handleClearCompleted = () => {
const completedTodos = todos.filter(todo => todo.completed);

const failedDeletions: Todo[] = [];

Promise.all(
completedTodos.map(todo =>
deleteTodos(todo.id).catch(() => {
failedDeletions.push(todo);
}),
),
).finally(() => {
if (failedDeletions.length) {
setError(ErrorMessage.Delete);
}

const newTodos = todos.filter(
todo =>
!todo.completed ||
failedDeletions.some(failed => failed.id === todo.id),
);

setTodos(newTodos);
});
};

const toggleTodoStatus = (updatedTodo: Todo) => {
setTodos(prevTodos => {
const copyTodos = [...prevTodos];

for (let i = 0; i < copyTodos.length; ++i) {
if (copyTodos[i].id === updatedTodo.id) {
copyTodos[i] = updatedTodo;
break;
}
}

return copyTodos;
});
};

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
onTodo={handleNewTodo}
onError={setError}
isInputDisabled={isInputDisabled}
query={query}
setQuery={setQuery}
inputRef={inputRef}
/>

<section className="todoapp__main" data-cy="TodoList">
{filtered.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggleStatus={toggleTodoStatus}
handleDeleteTodo={handleDeleteTodo}
isLoading={currentTodoIds.includes(todo.id)}
/>
))}
</section>

{todos.length !== 0 && (
<Footer
currentFilter={currentFilter}
setCurrentFilter={setCurrentFilter}
todos={todos}
handleClearCompleted={handleClearCompleted}
activeTodos={activeTodos}
/>
)}
</div>

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

export const USER_ID = 2177;

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

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

export const deleteTodos = (todoId: number) => {
return client.delete(`/todos/${todoId}`);
};
37 changes: 37 additions & 0 deletions src/components/Errors/Errors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { useEffect } from 'react';
import classNames from 'classnames';

type Props = {
error: string;
onClearError: (v: string) => void;
};

export const Errors: React.FC<Props> = ({ error, onClearError }) => {
useEffect(() => {
if (error) {
setTimeout(() => {
onClearError('');
}, 3000);
}
}, [error, onClearError]);

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={() => onClearError('')}
/>
{error}
</div>
);
};
1 change: 1 addition & 0 deletions src/components/Errors/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Errors';
59 changes: 59 additions & 0 deletions src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { Dispatch, SetStateAction } from 'react';
import { Todo } from '../../types/Todo';
import { FilterBy, filterByValues } from '../../types/FilterBy';
import classNames from 'classnames';

type Props = {
todos: Todo[];
activeTodos: Todo[];
currentFilter: FilterBy;
setCurrentFilter: Dispatch<SetStateAction<FilterBy>>;
handleClearCompleted: () => void;
};

export const Footer: React.FC<Props> = ({
currentFilter,
setCurrentFilter,
todos,
activeTodos,
handleClearCompleted,
}) => {
const displayFilter = (filter: FilterBy) =>
filter.charAt(0).toUpperCase() + filter.slice(1);

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

<nav className="filter" data-cy="Filter">
{filterByValues.map(filter => (
<a
key={filter}
href={`#/${filter.toLowerCase()}`}
data-cy={`FilterLink${displayFilter(filter)}`}
className={classNames('filter__link', {
selected: currentFilter === filter,
})}
onClick={() => {
setCurrentFilter(filter);
}}
>
{displayFilter(filter)}
</a>
))}
</nav>

<button
type="button"
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
disabled={todos.every(todo => !todo.completed)}
onClick={handleClearCompleted}
>
Clear completed
</button>
</footer>
);
};
1 change: 1 addition & 0 deletions src/components/Footer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Footer';
78 changes: 78 additions & 0 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { useEffect } from 'react';
import { Todo } from '../../types/Todo';
import { ErrorMessage } from '../../types/ErrorMessage';

type Props = {
onTodo: (todo: Todo) => void;
onError: (v: string) => void;
isInputDisabled: boolean;
query: string;
setQuery: (v: string) => void;
inputRef: React.RefObject<HTMLInputElement>;
};

export const Header: React.FC<Props> = ({
onTodo,
onError,
isInputDisabled,
query,
setQuery,
inputRef,
}) => {
useEffect(() => {
if (inputRef.current && !isInputDisabled) {
inputRef.current.focus();
}
}, [isInputDisabled, inputRef]);

const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
};

const handleAddTodo = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();

if (query.trim().length > 0) {
const newTodo = {
id: +new Date(),
userId: 2177,
title: query.trim(),
completed: false,
};

onTodo(newTodo);

onError('');

return;
}

onError(ErrorMessage.EmptyTitle);
};

return (
<header className="todoapp__header">
{/* this button should have `active` class only if all todos are completed */}
<button
type="button"
className="todoapp__toggle-all active"
data-cy="ToggleAllButton"
/>

{/* Add a todo on form submit */}
<form onSubmit={handleAddTodo}>
<input
data-cy="NewTodoField"
type="text"
className="todoapp__new-todo"
placeholder="What needs to be done?"
value={query}
disabled={isInputDisabled}
onChange={handleQueryChange}
ref={inputRef}
/* eslint-disable-next-line jsx-a11y/no-autofocus */
/>
</form>
</header>
);
};
1 change: 1 addition & 0 deletions src/components/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Header';
Loading

0 comments on commit e21709c

Please sign in to comment.