Skip to content

Commit

Permalink
solution 0.01 one test failed
Browse files Browse the repository at this point in the history
  • Loading branch information
StanislavKapytsia committed Dec 11, 2024
1 parent cbf4047 commit ddea68f
Show file tree
Hide file tree
Showing 10 changed files with 761 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://StanislavKapytsia.github.io/react_todo-app-with-api/) and add it to the PR description.
311 changes: 291 additions & 20 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,297 @@
/* eslint-disable max-len */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import { UserWarning } from './UserWarning';

const USER_ID = 0;
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { addTodo, delTodo, get, updTodo } from './api/todos';
import { TodoInterface } from './types/Todo';
import { Filter } from './types/filter';
import { TodoList } from './components/todoList/todoList';

Check failure on line 5 in src/App.tsx

View workflow job for this annotation

GitHub Actions / run_linter (20.x)

Missing file extension for "./components/todoList/todoList"
import { FilteredTodoList } from './components/footer/filteredTodoList';

Check failure on line 6 in src/App.tsx

View workflow job for this annotation

GitHub Actions / run_linter (20.x)

Missing file extension for "./components/footer/filteredTodoList"
import classNames from 'classnames';

export const App: React.FC = () => {
if (!USER_ID) {
return <UserWarning />;
}
const [todos, setTodos] = useState<TodoInterface[]>([]);
const [filter, setFilter] = useState<Filter>(Filter.All);
const [value, setValue] = useState('');
const [tempTodo, setTempTodo] = useState<TodoInterface | null>(null);
const [errorMessage, setErrorMessage] = useState('');
const [active, setActive] = useState(false);
const [todosForModify, setTodosForModify] = useState<TodoInterface[]>([]);

const inputForFocusRef = useRef<HTMLInputElement>(null);
const notificationRef = useRef<HTMLDivElement>(null);

const hideNotification = () => {
if (notificationRef.current) {
notificationRef.current.classList.add('hidden');
}
};

const errorHandling = (error: Error) => {
if (notificationRef.current) {
notificationRef.current.classList.remove('hidden');
setErrorMessage(error.message);
setTimeout(() => {
if (notificationRef.current) {
notificationRef.current.classList.add('hidden');
}
}, 3000);
}
};

const newId = () => {
const maxId = Math.max(0, ...todos.map(todo => todo.id));

return maxId + 1;
};

const errorManagement = (er: unknown) => {
if (er instanceof Error) {
errorHandling(er);
}
};

const createTodo = (id: number, title: string): TodoInterface => ({
id,
userId: 2039,
title: title.trim(),
completed: false,
});

useEffect(() => {
if (inputForFocusRef.current) {
inputForFocusRef.current.focus();
}

const fetchTodos = async () => {
try {
hideNotification();

const data = await get();

setTodos(data);
} catch (error) {
if (error instanceof Error) {
errorHandling(error);
}
}
};

fetchTodos();
}, []);

const allTodosComplited = useMemo(() => {
return todos.every(item => item.completed);
}, [todos]);

useEffect(() => {
if (allTodosComplited) {
setActive(true);
} else {
setActive(false);
}
}, [allTodosComplited, active]);

useEffect(() => {
if (inputForFocusRef.current && !tempTodo) {
inputForFocusRef.current.focus();
}
}, [tempTodo]);

const deleteTodos = async (content: number[], inputEdit?: boolean) => {
for (const todoId of content) {
try {
hideNotification();
await delTodo(todoId);
// await new Promise(resolve => setTimeout(resolve, 2000)); for checking delete/save every single todo;

setTodos(current => current.filter(item => todoId !== item.id));
} catch (error) {
errorManagement(error);
}
}

if (inputForFocusRef.current && !inputEdit) {
inputForFocusRef.current.focus();
}

setTodosForModify([]);
};

const addTodos = async (data: string) => {
if (inputForFocusRef.current) {
inputForFocusRef.current.focus();
}

setTempTodo(() => createTodo(0, data));

try {
hideNotification();

await addTodo(data);

const newTodo = createTodo(newId(), data);

setTodos(current => [...current, newTodo]);

setValue('');
} catch (error) {
errorManagement(error);
setValue(value);
} finally {
setTempTodo(null);
}
};

const updateTodos = async (updateTodo: TodoInterface[]) => {
for (const upTodo of updateTodo) {
try {
const updatedTodo = (await updTodo(upTodo)) as TodoInterface;

setTodos(current => {
const newTodosList = [...current];
const index = newTodosList.findIndex(todo => todo.id === upTodo.id);

newTodosList.splice(index, 1, updatedTodo);

return newTodosList;
});

if (inputForFocusRef.current) {
inputForFocusRef.current.focus();
}
} catch (error) {
errorManagement(error);
}
}

setTodosForModify([]);
};

const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

if (value.trim()) {
addTodos(value);
} else {
const empty = new Error('Title should not be empty');

errorHandling(empty);
setValue('');
}
};

const handleClose = () => {
if (notificationRef.current) {
notificationRef.current.classList.add('hidden');
}
};

const filteredTodos = (): TodoInterface[] => {
return todos.filter(todo => {
switch (filter) {
case Filter.All:
return true;
case Filter.Active:
return !todo.completed;
case Filter.Completed:
return todo.completed;
default:
return true;
}
});
};

const handleChangeValue = (e: React.ChangeEvent<HTMLInputElement>) => {
if (notificationRef.current) {
notificationRef.current.classList.add('hidden');
}

setValue(e.target.value);
};

const handleUpdateStatus = () => {
const copyTodos = [...todos];

if (active) {
const content = copyTodos.map(item => ({ ...item, completed: false }));

setTodosForModify(content);

updateTodos(content);
} else {
const content = copyTodos
.filter(todo => !todo.completed)
.map(item => ({ ...item, completed: true }));

setTodosForModify(content);

updateTodos(content);
}
};

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: active })}
data-cy="ToggleAllButton"
onClick={handleUpdateStatus}
/>
)}

<form onSubmit={onSubmit}>
<input
ref={inputForFocusRef}
data-cy="NewTodoField"
type="text"
className="todoapp__new-todo"
placeholder="What needs to be done?"
value={value}
onChange={handleChangeValue}
disabled={tempTodo !== null}
/>
</form>
</header>

{todos.length > 0 && (
<TodoList
filteredTodos={filteredTodos()}
deleteTodos={deleteTodos}
tempTodo={tempTodo}
setTodosForModify={setTodosForModify}
todosForModify={todosForModify}
updateTodos={updateTodos}
/>
)}

{todos.length > 0 && (
<FilteredTodoList
todos={todos}
setFilter={setFilter}
filter={filter}
deleteTodos={deleteTodos}
setTodosForModify={setTodosForModify}
/>
)}
</div>

<div
ref={notificationRef}
data-cy="ErrorNotification"
className="notification
is-danger is-light has-text-weight-normal hidden"
>
<button
data-cy="HideErrorButton"
type="button"
className="delete"
onClick={handleClose}
/>
{errorMessage}
</div>
</div>
);
};
55 changes: 55 additions & 0 deletions src/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { TodoInterface } from '../types/Todo';
import { client } from '../utils/fetchClient';

export const USER_ID = 2039;

export const get = async () => {
try {
const todos = await client.get<TodoInterface[]>(`/todos?userId=${USER_ID}`);

return todos;
} catch (error) {
throw new Error('Unable to load todos');
}
};

export const delTodo = async (id: number) => {
try {
const deleteTodo = await client.delete(`/todos/${id}`);

return deleteTodo;
} catch (error) {
throw new Error('Unable to delete a todo');
}
};

export const addTodo = async (title: string) => {
try {
const adTodo = await client.post(`/todos/`, {
completed: false,
title,
userId: 2039,
});

return adTodo;
} catch (error) {
throw new Error('Unable to add a todo');
}
};

export const updTodo = async (updateTodo: TodoInterface) => {
const { id, title, completed, userId } = updateTodo;

try {
const upTodo = await client.patch(`/todos/${id}`, {
id,
completed,
title,
userId,
});

return upTodo;
} catch (error) {
throw new Error('Unable to update a todo');
}
};
Loading

0 comments on commit ddea68f

Please sign in to comment.