Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and implement the ability to toggle and rename todos.
## Toggling a todo status

Toggle the `completed` status on `TodoStatus` change:

- Install Prettier Extention and use this [VSCode settings](https://mate-academy.github.io/fe-program/tools/vscode/settings.json) to enable format on save.
- covered the todo with a loader overlay while waiting for API response;
- the status should be changed on success;
Expand Down Expand Up @@ -38,6 +39,7 @@ Implement the ability to edit a todo title on double click:
- or the deletion error message if we tried to delete the todo.

## If you want to enable tests

- open `cypress/integration/page.spec.js`
- replace `describe.skip` with `describe` for the root `describe`

Expand All @@ -47,4 +49,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://A1daros.github.io/react_todo-app-with-api/) and add it to the PR description.
83 changes: 69 additions & 14 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,81 @@
/* 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, { useState } from 'react';

import { UserWarning } from './UserWarning';
import { TodoList } from './components/TodoList';
import { Header } from './components/Header';
import { Footer } from './components/Footer';
import { ErrorNotification } from './components/ErrorNotificaton';

import { FilterStatus } from './types/types';
import { USER_ID } from './api/todos';

const USER_ID = 0;
import { useErrorMessage } from './hooks/useErrorMessage';
import { useTodos } from './hooks/useTodos';
import { getVisibleTodos } from './utils/todoUtils';

export const App: React.FC = () => {
const [filter, setFilter] = useState<FilterStatus>(FilterStatus.All);
const [error, setError] = useErrorMessage();

const {
todos,
tempTodo,
deletingId,
loadingId,
addTodo,
removeTodo,
renameTodo,
clearCompleted,
toggleAll,
toggleTodo,
isAllCompleted,
} = useTodos(setError);

const visibleTodos = getVisibleTodos(todos, filter);

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
onAdd={addTodo}
onError={setError}
loading={!!tempTodo || deletingId.length > 0}
isAllCompleted={isAllCompleted}
onToggleAll={toggleAll}
hasTodos={todos.length > 0}
/>

{(!!todos.length || tempTodo) && (
<TodoList
todos={visibleTodos}
onToggle={toggleTodo}
tempTodo={tempTodo}
deletingId={deletingId}
loadingId={loadingId}
onDelete={removeTodo}
onRename={renameTodo}
/>
)}

{todos.length > 0 && (
<Footer
todos={todos}
filter={filter}
onFilterChange={setFilter}
onClearCompleted={clearCompleted}
/>
)}
</div>

<ErrorNotification error={error} onErrorClose={() => setError(null)} />
</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 = 4182;

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

export const createTodo = (data: Omit<Todo, 'id'>) => {
return client.post<Todo>('/todos', data);
};

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

export const updateTodo = (todoId: number, data: Partial<Todo>) => {
return client.patch<Todo>(`/todos/${todoId}`, data);
};
30 changes: 30 additions & 0 deletions src/components/ErrorNotificaton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import cn from 'classnames';

type Props = {
error: string | null;
onErrorClose: () => void;
};

export const ErrorNotification: React.FC<Props> = ({ error, onErrorClose }) => {
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={onErrorClose}
/>
{error}
</div>
);
};
63 changes: 63 additions & 0 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import { Todo } from '../types/Todo';
import cn from 'classnames';
import { FilterStatus } from '../types/types';
import { FILTERS } from '../constants';

type Props = {
todos: Todo[];
filter: FilterStatus;
onFilterChange: (status: FilterStatus) => void;
onClearCompleted: () => void;
};

export const Footer: React.FC<Props> = ({
todos,
filter,
onFilterChange,
onClearCompleted,
}) => {
if (todos.length === 0) {
return null;
}

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

const handleFilterClick =
(status: FilterStatus) => (event: React.MouseEvent) => {
event.preventDefault();
onFilterChange(status);
};

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

<nav className="filter" data-cy="Filter">
{FILTERS.map(({ id, title, url, cy }) => (
<a
key={id}
href={url}
className={cn('filter__link', { selected: filter === id })}
data-cy={cy}
onClick={handleFilterClick(id)}
>
{title}
</a>
))}
</nav>

<button
type="button"
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
disabled={todos.length - activeTodos === 0}
onClick={onClearCompleted}
>
Clear completed
</button>
</footer>
);
};
80 changes: 80 additions & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useEffect, useState } from 'react';
import { ErrorMessage } from '../types/types';

type Props = {
onAdd: (title: string) => Promise<void>;
onError: (message: string) => void;
loading: boolean;
isAllCompleted: boolean;
onToggleAll: () => void;
hasTodos: boolean;
};

export const Header: React.FC<Props> = ({
onAdd,
onError,
loading,
isAllCompleted,
onToggleAll,
hasTodos,
}) => {
const [title, setTitle] = useState('');
const [disabled, setDisabled] = useState(false);

const inputRef = React.useRef<HTMLInputElement>(null);

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

const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
const trimmedTitle = title.trim();

if (!trimmedTitle) {
onError(ErrorMessage.EmptyTitle);

return;
}

setDisabled(true);
onError('');

try {
await onAdd(trimmedTitle);
setTitle('');
} catch {
} finally {
setDisabled(false);
inputRef.current?.focus();
}
};

return (
<header className="todoapp__header">
{hasTodos && (
<button
type="button"
className={`todoapp__toggle-all ${isAllCompleted ? 'active' : ''}`}
data-cy="ToggleAllButton"
onClick={onToggleAll}
/>
)}

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