Skip to content

Commit

Permalink
add React Transition Group
Browse files Browse the repository at this point in the history
  • Loading branch information
nataliia2211 committed Dec 20, 2024
1 parent cbf4047 commit a5cdd3e
Show file tree
Hide file tree
Showing 15 changed files with 732 additions and 16 deletions.
176 changes: 160 additions & 16 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,170 @@
/* 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 { UserWarning } from './UserWarning';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import * as todoService from './api/todos';
import { Todo } from './types/Todo';

const USER_ID = 0;
import { Header } from './components/Header/Header';
import { Footer } from './components/Footer/Footer';
import { UserWarning } from './UserWarning';
import { Errors } from './types/Errors';
import { ErrorNotification } from './components/ErrorNotification';
import { FilterType } from './types/FilterType';
import { TodoList } from './components/TodoList/TodoList';

export const App: React.FC = () => {
if (!USER_ID) {
const [todos, setTodos] = useState<Todo[]>([]);
const [errorMessage, setErrorMessage] = useState<Errors>(Errors.Empty);
const [filteredField, setFilteredField] = useState<FilterType>(
FilterType.All,
);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [loadingTodoIds, setLoadingTodoIds] = useState<number[]>([]);
const inputAddRef = useRef<HTMLInputElement>(null);

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

const areAllTodosCompleted = useMemo(() => {
return todos.every(todo => todo.completed === true);
}, [todos]);

useEffect(() => {
(async () => {
try {
const data = await todoService.getTodos();

setTodos(data);
} catch (err) {
setErrorMessage(Errors.UnableToLoad);
}
})();
}, []);

const onAddTodo = async (todoTitle: string) => {
setTempTodo({
id: 0,
title: todoTitle,
completed: false,
userId: todoService.USER_ID,
});
try {
const newTodo = await todoService.addTodo({
title: todoTitle,
completed: false,
});

setTodos(prev => [...prev, newTodo]);
} catch (err) {
setErrorMessage(Errors.UnableToAdd);
inputAddRef?.current?.focus();
throw err;
} finally {
setTempTodo(null);
}
};

const onRemoveTodo = async (todoId: number) => {
setLoadingTodoIds(prev => [...prev, todoId]);
try {
await todoService.deleteTodo(todoId);

setTodos(prev => prev.filter(todo => todo.id !== todoId));
} catch (err) {
setErrorMessage(Errors.UnableToDelete);
inputAddRef?.current?.focus();
throw err;
} finally {
setLoadingTodoIds(prev => prev.filter(id => id !== todoId));
}
};

const onUpdateTodo = async (todoToUpdate: Todo) => {
setLoadingTodoIds(prev => [...prev, todoToUpdate.id]);
try {
const updatedTodo = await todoService.updateTodo(todoToUpdate);

setTodos(prev =>
prev.map(todo => (todo.id === updatedTodo.id ? updatedTodo : todo)),
);
} catch (err) {
setErrorMessage(Errors.UnableToUpdate);
throw err;
} finally {
setLoadingTodoIds(prev => prev.filter(id => id !== todoToUpdate.id));
}
};

const onToggleAll = async () => {
if (todosActiveNumber > 0) {
todos
.filter(todo => !todo.completed)
.forEach(item => onUpdateTodo({ ...item, completed: true }));
} else {
todos.forEach(todo => onUpdateTodo({ ...todo, completed: false }));
}
};

const onClearCompleted = async () => {
const completedTodo = todos.filter(todo => todo.completed);

completedTodo.forEach(todo => onRemoveTodo(todo.id));
};

const filteredTodos = todos.filter(todo => {
switch (filteredField) {
case FilterType.Active:
return !todo.completed;
case FilterType.Completed:
return todo.completed;
default:
return todos;
}
});

if (!todoService.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
setErrorMessage={setErrorMessage}
onAddTodo={onAddTodo}
isInputDisabled={!!tempTodo}
onToggleAll={onToggleAll}
todosLength={todos.length}
inputRef={inputAddRef}
areAllTodosCompleted={areAllTodosCompleted}
/>

{(todos.length > 0 || tempTodo) && (
<>
<TodoList
todos={filteredTodos}
onRemoveTodo={onRemoveTodo}
onUpdateTodo={onUpdateTodo}
loadingTodoIds={loadingTodoIds}
tempTodo={tempTodo}
/>
<Footer
todos={todos}
filteredField={filteredField}
setFilteredField={setFilteredField}
activeTodo={todosActiveNumber}
onClearCompleted={onClearCompleted}
/>
</>
)}
</div>
<ErrorNotification
error={errorMessage}
setErrorMessage={setErrorMessage}
/>
</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 = 2136;

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

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

export const addTodo = (newTodo: Omit<Todo, 'id' | 'userId'>) => {
return client.post<Todo>('/todos', { ...newTodo, userId: USER_ID });
};

export function updateTodo(todo: Todo) {
return client.patch<Todo>(`/todos/${todo.id}`, todo);
}
43 changes: 43 additions & 0 deletions src/components/ErrorNotification/ErrorNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import classNames from 'classnames';
import { Errors } from '../../types/Errors';
import { Dispatch, SetStateAction, useEffect } from 'react';

type Props = {
error: Errors;
setErrorMessage: Dispatch<SetStateAction<Errors>>;
};
export const ErrorNotification: React.FC<Props> = props => {
const { error, setErrorMessage } = props;

useEffect(() => {
if (error === Errors.Empty) {
return;
}

const timerId = setTimeout(() => {
setErrorMessage(Errors.Empty);
}, 3000);

return () => {
clearTimeout(timerId);
};
}, [error, setErrorMessage]);

return (
<div
data-cy="ErrorNotification"
className={classNames(
'notification is-danger is-light has-text-weight-normal',
{ hidden: !error.length },
)}
>
<button
data-cy="HideErrorButton"
type="button"
className="delete"
onClick={() => setErrorMessage(Errors.Empty)}
/>
{error}
</div>
);
};
1 change: 1 addition & 0 deletions src/components/ErrorNotification/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ErrorNotification';
55 changes: 55 additions & 0 deletions src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import classNames from 'classnames';

import { Dispatch, SetStateAction } from 'react';
import { Todo } from '../../types/Todo';
import { FilterType } from '../../types/FilterType';

type Props = {
todos: Todo[];
activeTodo: number;
filteredField: FilterType;
setFilteredField: Dispatch<SetStateAction<FilterType>>;
onClearCompleted: () => Promise<void>;
};

export const Footer: React.FC<Props> = ({
todos,
activeTodo,
filteredField,
setFilteredField,
onClearCompleted,
}) => {
return (
<footer className="todoapp__footer" data-cy="Footer">
<span className="todo-count" data-cy="TodosCounter">
{activeTodo} items left
</span>

<nav className="filter" data-cy="Filter">
{Object.values(FilterType).map(filter => (
<a
key={filter}
href={`#/${filter === FilterType.All ? '' : filter.toLowerCase()}`}
className={classNames('filter__link', {
selected: filteredField === filter,
})}
data-cy={`FilterLink${filter}`}
onClick={() => setFilteredField(filter)}
>
{filter}
</a>
))}
</nav>

<button
type="button"
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
disabled={todos.every(todo => !todo.completed)}
onClick={onClearCompleted}
>
Clear completed
</button>
</footer>
);
};
81 changes: 81 additions & 0 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import cn from 'classnames';
import { Errors } from '../../types/Errors';

type Props = {
setErrorMessage: Dispatch<SetStateAction<Errors>>;
onAddTodo: (value: string) => Promise<void>;
isInputDisabled: boolean;
onToggleAll: () => Promise<void>;
todosLength: number;
inputRef: React.RefObject<HTMLInputElement> | null;
areAllTodosCompleted: boolean;
};

export const Header: React.FC<Props> = props => {
const {
setErrorMessage,
onAddTodo,
isInputDisabled,
onToggleAll,
todosLength,
inputRef,
areAllTodosCompleted,
} = props;
const [inputValue, setInputValue] = useState('');

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

if (inputValue.trim() === '') {
setErrorMessage(Errors.EmptyTitle);

return;
}

try {
await onAddTodo(inputValue.trim());
setInputValue('');
} catch (err) {}
};

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

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

return (
<header className="todoapp__header">
{todosLength !== 0 && (
<button
type="button"
className={cn('todoapp__toggle-all', {
active: areAllTodosCompleted,
})}
data-cy="ToggleAllButton"
onClick={onToggleAll}
/>
)}

<form onSubmit={onSubmit}>
<input
data-cy="NewTodoField"
type="text"
className="todoapp__new-todo"
placeholder="What needs to be done?"
ref={inputRef}
value={inputValue}
onChange={event => {
setInputValue(event.target.value);
}}
disabled={isInputDisabled}
/>
</form>
</header>
);
};
Loading

0 comments on commit a5cdd3e

Please sign in to comment.