Skip to content

Commit

Permalink
Solution
Browse files Browse the repository at this point in the history
  • Loading branch information
andrushchenkoo committed Jan 6, 2025
1 parent 62dbb71 commit f435907
Show file tree
Hide file tree
Showing 12 changed files with 604 additions and 20 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://andrushchenkoo.github.io/react_todo-app-with-api/) and add it to the PR description.
175 changes: 156 additions & 19 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,163 @@
/* 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';

const USER_ID = 0;
import React, { useEffect, useMemo, useState } from 'react';
import { Todo } from './types/Todo';
import { TodoHeader } from './components/TodoHeader';
import { TodoFooter } from './components/TodoFooter';
import { ErrorNotification } from './components/ErrorNotification';
import {
addTodo,
deleteTodo,
getTodos,
updateTodo,
USER_ID,
} from './api/todos';
import { Error } from './types/Error';
import { Filter } from './types/Filter';
import { TodoList } from './components/TodoList';

export const App: React.FC = () => {
if (!USER_ID) {
return <UserWarning />;
}
const [todos, setTodos] = useState<Todo[]>([]);
const [errorMessage, setErrorMessage] = useState<Error | null>(null);
const [filter, setFilter] = useState<Filter>(Filter.All);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [loadingIds, setLoadingIds] = useState<number[]>([]);

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

setTodos(data);
} catch (err) {
setErrorMessage(Error.LoadTodos);
}
})();
}, []);

const filteredTodos = useMemo(() => {
if (filter === Filter.All) {
return todos;
}

return todos.filter(todo => {
return filter === Filter.Completed ? todo.completed : !todo.completed;
});
}, [todos, filter]);

const todosCompletedCounter: number = useMemo(() => {
return todos.filter(todo => todo.completed).length;
}, [todos]);

const todosCounter: number = useMemo(() => {
return todos.length - todosCompletedCounter;
}, [todos, todosCompletedCounter]);

const areAllCompletedTodo: boolean = useMemo(() => {
return todos.every(todo => todo.completed);
}, [todos]);

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

setTodos(prev => [...prev, newTodo]);
} catch (err) {
setErrorMessage(Error.AddTodos);
throw err;
} finally {
setTempTodo(null);
}
};

const onDeleteTodo = async (todoId: number) => {
setLoadingIds(prev => [...prev, todoId]);
try {
await deleteTodo(todoId);

setTodos(prev => prev.filter(todo => todo.id !== todoId));
} catch (err) {
setErrorMessage(Error.DeleteTodos);
throw err;
} finally {
setLoadingIds(prev => prev.filter(id => id !== todoId));
}
};

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

completedTodos.forEach(todo => onDeleteTodo(todo.id));
};

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

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

const onToggleAll = async () => {
if (todosCounter > 0) {
const activeTodos = todos.filter(todo => !todo.completed);

activeTodos.forEach(todo => {
onUpdateTodo({ ...todo, completed: true });
});
} else {
todos.forEach(todo => {
onUpdateTodo({ ...todo, completed: false });
});
}
};

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}
onAddTodo={onAddTodo}
setErrorMessage={setErrorMessage}
isInputDisabled={!!tempTodo}
onToggleAll={onToggleAll}
areAllCompletedTodo={areAllCompletedTodo}
/>

{(!!todos.length || tempTodo) && (
<>
<TodoList
filteredTodos={filteredTodos}
loadingIds={loadingIds}
onDeleteTodo={onDeleteTodo}
tempTodo={tempTodo}
onUpdateTodo={onUpdateTodo}
/>
<TodoFooter
filter={filter}
setFilter={setFilter}
todosCounter={todosCounter}
onClearCompleted={onClearCompleted}
todosCompletedCounter={todosCompletedCounter}
/>
</>
)}
</div>
<ErrorNotification
error={errorMessage}
setErrorMessage={setErrorMessage}
/>
</div>
);
};
23 changes: 23 additions & 0 deletions src/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Todo } from '../types/Todo';
import { client } from '../utils/fetchClient';

export const USER_ID = 2167;

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

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

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

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

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

useEffect(() => {
if (!error) {
return;
}

const timerId = setTimeout(() => {
setErrorMessage(null);
}, 3000);

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

return (
<div
data-cy="ErrorNotification"
className={cn('notification is-danger is-light has-text-weight-normal', {
hidden: !error,
})}
>
<button
data-cy="HideErrorButton"
type="button"
className="delete"
onClick={() => {
setErrorMessage(null);
}}
/>
{error}
</div>
);
};
59 changes: 59 additions & 0 deletions src/components/TodoFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { Dispatch, SetStateAction } from 'react';
import cn from 'classnames';
import { Filter } from '../types/Filter';

type Props = {
filter: Filter;
setFilter: Dispatch<SetStateAction<Filter>>;
todosCounter: number;
todosCompletedCounter: number;
onClearCompleted: () => Promise<void>;
};

export const TodoFooter: React.FC<Props> = props => {
const {
filter,
setFilter,
todosCounter,
onClearCompleted,
todosCompletedCounter,
} = props;

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

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

<button
type="button"
className="todoapp__clear-completed"
disabled={todosCompletedCounter === 0}
data-cy="ClearCompletedButton"
onClick={() => {
onClearCompleted();
}}
>
Clear completed
</button>
</footer>
);
};
75 changes: 75 additions & 0 deletions src/components/TodoHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
import cn from 'classnames';
import { Error } from '../types/Error';
import { Todo } from '../types/Todo';

type Props = {
onAddTodo: (value: string) => Promise<void>;
setErrorMessage: Dispatch<SetStateAction<Error | null>>;
isInputDisabled: boolean;
todos: Todo[];
onToggleAll: () => Promise<void>;
areAllCompletedTodo: boolean;
};
export const TodoHeader: React.FC<Props> = props => {
const {
onAddTodo,
setErrorMessage,
isInputDisabled,
todos,
onToggleAll,
areAllCompletedTodo,
} = props;

const [inputValue, setInputValue] = useState('');

const inputRef = useRef<HTMLInputElement>(null);

const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (inputValue.trim() === '') {
setErrorMessage(Error.EmptyTitle);

return;
}

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

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

return (
<header className="todoapp__header">
{todos.length !== 0 && (
<button
type="button"
className={cn('todoapp__toggle-all', {
active: areAllCompletedTodo,
})}
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 f435907

Please sign in to comment.