Skip to content

Commit

Permalink
add react todo-app-with-api
Browse files Browse the repository at this point in the history
  • Loading branch information
moskkat40 committed Dec 10, 2024
1 parent cbf4047 commit 804f07c
Show file tree
Hide file tree
Showing 11 changed files with 583 additions and 14 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://moskkat40.github.io/react_todo-app-with-api/) and add it to the PR description.
147 changes: 134 additions & 13 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,147 @@
/* 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, { useEffect, useMemo, useState } from 'react';
import { UserWarning } from './UserWarning';
import { USER_ID } from './api/todos';
import * as servisesTodos from './api/todos';
import { Todo } from './types/Todo';
import { TodoInput } from './components/TodoInput/TodoInput';
import { TodoList } from './components/TodoList/TodoList';
import { ErrorNotification } from './components/ErrorNotification/ErrorNotification';
import { Footer } from './components/Footer/Footer';
import { Filter } from './types/Filter';

const USER_ID = 0;
export enum FilterParam {
All = 'All',
Active = 'Active',
Completed = 'Completed',
}

export const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [errorMessage, setErrorMessage] = useState('');
const [filter, setFilter] = useState<Filter>(FilterParam.All);
const [tempTodo, setTempTodo] = useState(null);
const [loadingIds, setLoadingIds] = useState<number[]>([]);

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

useEffect(() => {
setTimeout(() => {
setErrorMessage('');
}, 3000);
}, [errorMessage]);

const filteredTodos = useMemo(() => {
if (filter === FilterParam.Active) {
return todos.filter(todo => !todo.completed);
}

if (filter === FilterParam.Completed) {
return todos.filter(todo => todo.completed);
}

return todos;
}, [filter, todos]);

const handleDeleteTodo = (todoId: number) => {
setLoadingIds(current => [...current, todoId]);

return servisesTodos
.deleteTodo(todoId)
.then(() => {
setTodos(currentTodo => currentTodo.filter(todo => todo.id !== todoId));
})
.catch(error => {
setErrorMessage('Unable to delete a todo');
throw error;
})
.finally(() => {
setLoadingIds(current => current.filter(id => id !== todoId));
});
};

const handleUpdateTodo = (
todoId: number,
newTitle: string,
completed: boolean,
) => {
setLoadingIds(current => [...current, todoId]);
const todoToUpdate = todos.find(todo => todo.id === todoId);

if (!todoToUpdate) {
return;
}

const updatedTodo = {
...todoToUpdate,
title: newTitle.trim(),
completed: completed,
};

return servisesTodos
.updateTodo(todoId, updatedTodo)
.then(() =>
setTodos(currentTodo =>
currentTodo.map(todo => (todo.id === todoId ? updatedTodo : todo)),
),
)
.catch(error => {
setErrorMessage('Unable to update a todo');
throw error;
})
.finally(() => {
setLoadingIds(current => current.filter(id => id !== todoId));
});
};

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">
<TodoInput
setErrorMessage={setErrorMessage}
todos={todos}
setTodos={setTodos}
setTempTodo={setTempTodo}
tempTodo={tempTodo}
handleUpdateTodo={handleUpdateTodo}
loadingIds={loadingIds}
/>

<TodoList
filteredTodos={filteredTodos}
tempTodo={tempTodo}
handleDeleteTodo={handleDeleteTodo}
handleUpdateTodo={handleUpdateTodo}
loadingIds={loadingIds}
/>
{todos.length > 0 && (
<Footer
filter={filter}
setFilter={setFilter}
todos={todos}
handleDeleteTodo={handleDeleteTodo}
/>
)}
</div>
<ErrorNotification
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
/>
</div>
);
};
21 changes: 21 additions & 0 deletions src/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Todo } from '../types/Todo';
import { client } from '../utils/fetchClient';

export const USER_ID = 1913;

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

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

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

export const updateTodo = (todoId: number, todo: Todo) => {
return client.patch(`/todos/${todoId}`, todo);
};
// Add more methods here
35 changes: 35 additions & 0 deletions src/components/ErrorNotification/ErrorNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import classNames from 'classnames';
import React from 'react';

type Props = {
errorMessage: string;
setErrorMessage: (a: string) => void;
};

export const ErrorNotification: React.FC<Props> = ({
errorMessage,
setErrorMessage,
}) => {
const handleCloseNotification = () => {
setErrorMessage('');
};

return (
<div
data-cy="ErrorNotification"
className={classNames(
'notification is-danger is-light has-text-weight-normal',
// eslint-disable-next-line prettier/prettier
{ hidden: !errorMessage },
)}
>
<button
onClick={handleCloseNotification}
data-cy="HideErrorButton"
type="button"
className="delete"
/>
{errorMessage}
</div>
);
};
68 changes: 68 additions & 0 deletions src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import classNames from 'classnames';
import { Todo } from '../../types/Todo';
import { Filter } from '../../types/Filter';
import { FilterParam } from '../../App';

type Props = {
filter: string;
setFilter: (a: Filter) => void;
todos: Todo[];
handleDeleteTodo: (a: number) => void;
};

export const Footer: React.FC<Props> = ({
filter,
setFilter,
todos,
handleDeleteTodo,
}) => {
const handleFilter = (event: React.MouseEvent<HTMLElement>) => {
const filterValue = event.currentTarget.textContent as Filter;

setFilter(filterValue);
};

const handleClearComplete = () => {
const completedTodos = todos.filter(todo => todo.completed);
const deletePromises = completedTodos.map(todo =>
handleDeleteTodo(todo.id),
);

Promise.allSettled(deletePromises);
};

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

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

<nav className="filter" data-cy="Filter">
{Object.values(FilterParam).map(param => (
<a
key={param}
href="#/"
className={classNames('filter__link', {
selected: filter === param,
})}
data-cy={`FilterLink${param}`}
onClick={handleFilter}
>
{param}
</a>
))}
</nav>
<button
type="button"
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
disabled={todos.every(todo => !todo.completed)}
onClick={handleClearComplete}
>
Clear completed
</button>
</footer>
);
};
102 changes: 102 additions & 0 deletions src/components/TodoInput/TodoInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useEffect, useRef, useState } from 'react';
import * as servisesTodos from '../../api/todos';
import { Todo } from '../../types/Todo';
import classNames from 'classnames';

const userId = servisesTodos.USER_ID;

type Props = {
setErrorMessage: (a: string) => void;
todos: Todo[];
setTodos: (a: Todo[]) => void;
setTempTodo: (a: Todo[] | null) => void;
tempTodo: Todo | null;
handleUpdateTodo: (a: number, b: string, c: boolean) => void;
loadingIds: number[];
};

export const TodoInput: React.FC<Props> = ({
setErrorMessage,
todos,
setTodos,
setTempTodo,
tempTodo,
handleUpdateTodo,
loadingIds,
}) => {
const [title, setTitle] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const AllCompletedTodo = todos.every(todo => todo.completed);

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

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

const handleAddTodo = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
setTempTodo({ id: 0, title: title.trim(), userId, completed: false });
if (title.trim().length > 0) {
servisesTodos
.postTodos({ title: title.trim(), userId, completed: false })
.then(newTodo => {
setTodos([...todos, newTodo]);
setTitle('');
setTempTodo(null);
})
.catch(() => {
setTempTodo(null);
setErrorMessage('Unable to add a todo');
});
} else {
setTempTodo(null);
setErrorMessage('Title should not be empty');
}
}
};

const handleChangleAllStatus = () => {
const newCompletedStatus = !AllCompletedTodo;
const todosToUpdate = todos.filter(
todo => todo.completed !== newCompletedStatus,
);

todosToUpdate.map(todo =>
handleUpdateTodo(todo.id, todo.title, newCompletedStatus),
);
};

return (
<header className="todoapp__header">
{todos.length > 0 && (
<button
type="button"
className={classNames('todoapp__toggle-all', {
active: AllCompletedTodo,
})}
data-cy="ToggleAllButton"
onClick={handleChangleAllStatus}
disabled={loadingIds.length > 0}
/>
)}
<form onKeyDown={handleAddTodo}>
<input
ref={inputRef}
data-cy="NewTodoField"
value={title}
type="text"
className="todoapp__new-todo"
placeholder="What needs to be done?"
onChange={handleInputValue}
disabled={tempTodo}
/>
</form>
</header>
);
};
Loading

0 comments on commit 804f07c

Please sign in to comment.