Skip to content

Commit

Permalink
Todo App
Browse files Browse the repository at this point in the history
  • Loading branch information
SemenVodolazskij committed Dec 26, 2024
1 parent 62dbb71 commit cef7117
Show file tree
Hide file tree
Showing 11 changed files with 553 additions and 21 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://SemenVodolazskij.github.io/react_todo-app-with-api/) and add it to the PR description.
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": "^1.9.12",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
Expand Down
297 changes: 282 additions & 15 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,293 @@
/* eslint-disable max-len */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { UserWarning } from './UserWarning';
import {
addTodos,
deleteTodos,
getTodos,
updateTodos as updateTodo,
USER_ID,
} from './api/todos';
import { Todo } from './types/Todo';
import classNames from 'classnames';

const USER_ID = 0;
import { Filter } from './types/FilterButton';
import { Error } from './types/ErrorMessage';
import { CompletedTodo } from './components/CompletedTodo';

export const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [error, setError] = useState<Error>(Error.Reset);
const [filter, setFilter] = useState<Filter>(Filter.All);
const [inputValue, setInputValue] = useState('');
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [loadingTodoIds, setLoadingTodoIds] = useState<number[]>([]);
const [isToogleAll, setIsToogleAll] = useState(false);
const [editingTodoId, setEditingTodoId] = useState<number | null>(null);

const handleTodoDoubleClick = (id: number | null) => {
setEditingTodoId(id);
};

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

setTodos(item => item.filter(todo => todo.id !== todoId));
} catch (err) {
setError(Error.Delete);
throw err;
} finally {
setLoadingTodoIds(prev => prev.filter(id => id !== todoId));
}
};

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

setTodos(item => [...item, newTodo]);
} catch (err) {
setError(Error.Add);
throw err;
} finally {
setTempTodo(null);
}
};

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

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

const onUpdateTodo = async (todoUpdate: Todo) => {
const oldTodos = [...todos];

setLoadingTodoIds(prev => [...prev, todoUpdate.id]);

try {
await updateTodo(todoUpdate);
setTodos(currentTodos =>
currentTodos.map(todo => {
return todo.id === todoUpdate.id ? todoUpdate : todo;
}),
);
} catch (err) {
setTodos(oldTodos);
setError(Error.Update);
throw err;
} finally {
setLoadingTodoIds(prev => prev.filter(id => id !== todoUpdate.id));
}
};

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (inputValue.trim() === '') {
setError(Error.Empty);

return;
}

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

const inputFocus = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};

useEffect(() => {
if (todos && !editingTodoId) {
inputFocus();
}
}, [error, todos, editingTodoId]);

useEffect(() => {
const timer = setTimeout(() => setError(Error.Reset), 3000);

return () => clearTimeout(timer);
}, [error]);

useEffect(() => {
getTodos()
.then(setTodos)
.catch(() => setError(Error.Load));
}, []);

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

if (filter === Filter.Completed) {
return todo.completed;
}

return Filter.All;
});

if (!USER_ID) {
return <UserWarning />;
}

const unCompletedTodosCounter = todos.filter(todo => !todo.completed).length;
const todosCompletedNum = todos.filter(todo => todo.completed).length;
const allTodosCompleted =
todos.length > 0 && todos.every(todo => todo.completed);

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

activeTodos.forEach(todo => {
onUpdateTodo({ ...todo, completed: true });
});
setIsToogleAll(true);
} else {
todos.forEach(todo => {
onUpdateTodo({ ...todo, completed: false });
});
setIsToogleAll(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">
<header className="todoapp__header">
{todos.length > 0 && (
<button
type="button"
className={classNames('todoapp__toggle-all', {
active: isToogleAll || allTodosCompleted,
})}
data-cy="ToggleAllButton"
onClick={onToggleAll}
/>
)}

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

<section className="todoapp__main" data-cy="TodoList">
{filteredTodos.map(todo => (
<CompletedTodo
error={error}
editingTodoId={editingTodoId}
handleTodoDoubleClick={handleTodoDoubleClick}
key={todo.id}
todo={todo}
onDelete={onDeleteTodo}
onUpdateTodo={onUpdateTodo}
isLoading={loadingTodoIds.includes(todo.id)}
setEditingTodoId={setEditingTodoId}
/>
))}
{tempTodo && (
<CompletedTodo
error={error}
setEditingTodoId={setEditingTodoId}
editingTodoId={editingTodoId}
handleTodoDoubleClick={handleTodoDoubleClick}
todo={tempTodo}
isLoading
onDelete={onDeleteTodo}
onUpdateTodo={onUpdateTodo}
/>
)}
</section>

{todos.length > 0 && (
<footer className="todoapp__footer" data-cy="Footer">
<span className="todo-count" data-cy="TodosCounter">
{unCompletedTodosCounter} items left
</span>
<nav className="filter" data-cy="Filter">
<button
className={classNames('filter__link', {
selected: filter === 'all',
})}
data-cy="FilterLinkAll"
onClick={() => setFilter(Filter.All)}
>
All
</button>

<button
className={classNames('filter__link', {
selected: filter === 'active',
})}
data-cy="FilterLinkActive"
onClick={() => setFilter(Filter.Active)}
>
Active
</button>

<button
className={classNames('filter__link', {
selected: filter === 'completed',
})}
data-cy="FilterLinkCompleted"
onClick={() => setFilter(Filter.Completed)}
>
Completed
</button>
</nav>

<button
type="button"
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
onClick={onClearCompleted}
disabled={todosCompletedNum === 0}
>
Clear completed
</button>
</footer>
)}
</div>

<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.Reset)}
/>
{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 = 2171;

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

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

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

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

0 comments on commit cef7117

Please sign in to comment.