Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
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://SerhiyShimko.github.io/react_todo-app-with-api/) and add it to the PR description.
6 changes: 3 additions & 3 deletions cypress/integration/page.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ describe('', () => {
});

// this test may be flaky
it.skip('should replace loader with a created todo', () => {
it('should replace loader with a created todo', () => {
page.flushJSTimers();
todos.assertCount(6);
todos.assertNotLoading(5);
Expand Down Expand Up @@ -1515,7 +1515,7 @@ describe('', () => {
});

// It depend on your implementation
it.skip('should stay while waiting', () => {
it('should stay while waiting', () => {
page.mockUpdate(257334);

todos.title(0).trigger('dblclick');
Expand Down Expand Up @@ -1694,7 +1694,7 @@ describe('', () => {
});

// this test may be unstable
it.skip('should hide loader on fail', () => {
it('should hide loader on fail', () => {
// to prevent Cypress from failing the test on uncaught exception
cy.once('uncaught:exception', () => false);

Expand Down
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
"@mate-academy/scripts": "^1.8.5",
"@mate-academy/scripts": "^2.1.3",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
Expand Down
46 changes: 28 additions & 18 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
/* 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, { useState } from 'react';
import './styles/todoapp.scss';
import classNames from 'classnames';
import { TodoApp } from './components/TodoApp';

export const App: React.FC = () => {
if (!USER_ID) {
return <UserWarning />;
}
const [error, setError] = useState('');

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>
<div className="todoapp">
<h1 className="todoapp__title">todos</h1>

<TodoApp setError={prev => setError(prev)} />

<p className="subtitle">Styles are already copied</p>
</section>
<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>
</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 = 4185;

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

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

export const updateTodo = ({ id, ...data }: Todo) => {
return client.patch<Todo>(`/todos/${id}`, data);
};

export const deleteTodo = (id: number) => {
return client.delete(`/todos/${id}`);
};
146 changes: 146 additions & 0 deletions src/components/Filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Todo } from '../types/Todo';
import classNames from 'classnames';

type Props = {
todosFromServer: Todo[] | null;
setTodos: (todos: Todo[]) => void;
deleteTodo: (id: number, todo: Todo) => void;
};

enum TypeFilter {
All = 'All',
Active = 'Active',
Completed = 'Completed',
}

function getCount(serverTodos: Todo[] | null) {
let count = 0;

if (serverTodos && serverTodos.length > 0) {
serverTodos.forEach(todo => {
if (todo.completed === false) {
count++;
}
});
}

return count;
}

type ArrayButtons = {
title: string;
href: string;
typeFilter: TypeFilter;
id: number;
link: string;
};

const arrayButtons: ArrayButtons[] = [
{
title: 'All',
href: '#/',
typeFilter: TypeFilter.All,
id: 1,
link: 'FilterLinkAll',
},
{
title: 'Active',
href: '#/active',
typeFilter: TypeFilter.Active,
id: 2,
link: 'FilterLinkActive',
},
{
title: 'Completed',
href: '#/completed',
typeFilter: TypeFilter.Completed,
id: 3,
link: 'FilterLinkCompleted',
},
];

export const Filter: React.FC<Props> = ({
setTodos,
todosFromServer,
deleteTodo,
}) => {
const [selectedFilter, setSelectedFilter] = useState(TypeFilter.All);

const filtrationTodos = useCallback(
(buttonType: TypeFilter) => {
if (todosFromServer) {
if (buttonType === TypeFilter.Active) {
const newTodos: Todo[] = todosFromServer.filter((todo: Todo) => {
return todo.completed === false;
});

setTodos(newTodos);
} else if (buttonType === TypeFilter.Completed) {
const newTodos: Todo[] = todosFromServer.filter((todo: Todo) => {
return todo.completed === true;
});

setTodos(newTodos);
} else {
setTodos(todosFromServer);
}
}
},
[todosFromServer, setTodos],
);

useEffect(() => {
filtrationTodos(selectedFilter);
}, [todosFromServer]);

Check warning on line 95 in src/components/Filter.tsx

View workflow job for this annotation

GitHub Actions / run_linter (20.x)

React Hook useEffect has missing dependencies: 'filtrationTodos' and 'selectedFilter'. Either include them or remove the dependency array

const clearCompleted = useCallback(async () => {
if (todosFromServer) {
const needDelete = todosFromServer
.filter(todo => todo.completed === true)
.map((todo: Todo) => {
return deleteTodo(todo.id, todo);
});

await Promise.all(needDelete);
}
}, [todosFromServer, deleteTodo]);

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

<nav className="filter" data-cy="Filter">
{arrayButtons.map(button => (
<a
href={button.href}
key={button.id}
className={classNames('filter__link', {
selected: selectedFilter === button.typeFilter,
})}
data-cy={button.link}
onClick={e => {
e.preventDefault();
filtrationTodos(button.typeFilter);
setSelectedFilter(button.typeFilter);
}}
>
{button.title}
</a>
))}
</nav>

<button
type="button"
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
disabled={!todosFromServer?.some(todo => todo.completed === true)}
onClick={clearCompleted}
>
Clear completed
</button>
</footer>
);
};
Loading
Loading