Skip to content

Commit

Permalink
add updating
Browse files Browse the repository at this point in the history
  • Loading branch information
AsyaYeromina committed Dec 28, 2024
1 parent cbf4047 commit 6843b11
Show file tree
Hide file tree
Showing 10 changed files with 652 additions and 16 deletions.
275 changes: 259 additions & 16 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,269 @@
/* eslint-disable max-len */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import { UserWarning } from './UserWarning';
/* eslint-disable jsx-a11y/label-has-associated-control */
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Todo } from './types/Todo';
import { FilterName } from './types/FilterName';
import * as todosService from './api/todos';
import { Header } from './Components/Header';
import { TodoList } from './Components/TodoList';
import { TempTodo } from './Components/TempTodo';
import { Footer } from './Components/Footer';
import { ErrorMessage } from './Components/ErrorMessage';

const USER_ID = 0;
export const USER_ID = 1918;

export const App: React.FC = () => {
if (!USER_ID) {
return <UserWarning />;
const [todos, setTodos] = useState<Todo[]>([]);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [newTodoTitle, setNewTodoTitle] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [activeFilter, setActiveFilter] = useState<FilterName>('ALL');
const [inputDisabled, setInputDisabled] = useState(false);
const [loadingTodoId, setLoadingTodoId] = useState<number | null>(null);
const [editedTodo, setEditedTodo] = useState<Todo | null>(null);

const inputRef = useRef<HTMLInputElement>(null);
const activeTodosQuantity = todos.filter(todo => !todo.completed).length;

function loadTodos() {
todosService
.getTodos()
.then(setTodos)
.catch(() => {
setErrorMessage('Unable to load todos');
setTimeout(() => setErrorMessage(''), 3000);
});
}

const filteredTodos = useMemo(() => {
switch (activeFilter) {
case 'ACTIVE':
return todos.filter(todo => !todo.completed);
case 'COMPLETED':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}, [todos, activeFilter]);

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

const handleNewTodoTitleChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
setNewTodoTitle(event.target.value);
};

function addTodo(loadingTodo: Todo) {
setErrorMessage('');

todosService
.createTodo({
userId: loadingTodo.userId,
title: loadingTodo.title,
completed: loadingTodo.completed,
})
.then(newTodo => {
setTempTodo(null);
setTodos(currentTodos => [...currentTodos, newTodo]);
setNewTodoTitle('');
})
.catch(() => {
setErrorMessage('Unable to add a todo');
setTimeout(() => setErrorMessage(''), 3000);
setTempTodo(null);
})
.finally(() => {
setInputDisabled(false);
if (inputRef.current) {
inputRef.current?.focus();
}
});
}

function handleSubmit(event: React.FormEvent) {
event.preventDefault();
if (!newTodoTitle.trim()) {
setErrorMessage('Title should not be empty');
setTimeout(() => setErrorMessage(''), 3000);

return;
}

const loadingTodo: Todo = {
userId: USER_ID,
title: newTodoTitle.trim(),
completed: false,
id: 0,
};

setTempTodo(loadingTodo);
setInputDisabled(true);
addTodo(loadingTodo);
}

function deleteTodo(todoId: number) {
// setEditedTodo(editedTodo);
// console.log(editedTodo?.title);

setLoadingTodoId(todoId);
todosService
.deleteTodo(todoId)
.then(() => {
setTodos(currentTodos =>
currentTodos.filter(todo => todo.id !== todoId),
);
setEditedTodo(null);
})
.catch(() => {
setEditedTodo(editedTodo);
setErrorMessage('Unable to delete a todo');
setTimeout(() => setErrorMessage(''), 3000);
})
.finally(() => {
setLoadingTodoId(null);

if (!inputDisabled && inputRef.current) {
inputRef.current?.focus();
}
});
}

function deleteCompletedTodos() {
todos.map(todo => {
if (todo.completed) {
deleteTodo(todo.id);
}
});
}

function toggleTodoStatus(
updatedTodo: Todo,
status = !updatedTodo.completed,
) {
setLoadingTodoId(updatedTodo.id);

todosService
.updateTodo(updatedTodo.id, { completed: status })
.then((todoResult: Todo) => {
setTodos(currentTodos => {
const newTodos = [...currentTodos];
const index = newTodos.findIndex(todo => todo.id === updatedTodo.id);

newTodos.splice(index, 1, todoResult);

return newTodos;
});
})
.catch(() => {
setErrorMessage('Unable to update a todo');
setTimeout(() => setErrorMessage(''), 3000);
})
.finally(() => {
setLoadingTodoId(null);
});
}

function toggleAllTodosStatus() {
if (activeTodosQuantity === 0) {
todos.map(todo => {
toggleTodoStatus(todo, false);
});
} else {
todos.map(todo => {
if (todo.completed === false) {
toggleTodoStatus(todo, true);
}
});
}
}

function handleTodoTitleUpdate(updatedTodoTitle: string) {
if (editedTodo) {
if (editedTodo.title === updatedTodoTitle) {
setEditedTodo(null);

return;
}

if (!updatedTodoTitle.trim()) {
deleteTodo(editedTodo.id);
// setEditedTodo(null);

return;
}

setLoadingTodoId(editedTodo.id);
todosService
.updateTodo(editedTodo.id, { title: updatedTodoTitle.trim() })
.then((todoResult: Todo) => {
setTodos(currentTodos => {
const newTodos = [...currentTodos];
const index = newTodos.findIndex(todo => todo.id === editedTodo.id);

newTodos.splice(index, 1, todoResult);

return newTodos;
});
setEditedTodo(null);
})
.catch(() => {
setErrorMessage('Unable to update a todo');
setTimeout(() => setErrorMessage(''), 3000);
})
.finally(() => {
setLoadingTodoId(null);
});
}
}

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
activeTodosQuantity={activeTodosQuantity}
onSubmitNewTodo={handleSubmit}
onTitleChange={handleNewTodoTitleChange}
inputDisabled={inputDisabled}
newTodoTitle={newTodoTitle}
inputRef={inputRef}
toggleAllStatuses={toggleAllTodosStatus}
todos={todos}
/>
{todos.length > 0 && (
<section className="todoapp__main" data-cy="TodoList">
<TodoList
todos={filteredTodos}
deleteTodo={deleteTodo}
loadingTodoId={loadingTodoId}
onToggleTodoStatus={toggleTodoStatus}
editedTodo={editedTodo}
onTodoTitleUpdateSubmit={handleTodoTitleUpdate}
onTodoEditing={setEditedTodo}
/>
{tempTodo !== null && <TempTodo tempTodo={tempTodo} />}
</section>
)}
{todos.length > 0 && (
<Footer
todos={todos}
activeFilter={activeFilter}
setActiveFilter={setActiveFilter}
activeTodosQuantity={activeTodosQuantity}
deleteCompletedTodos={deleteCompletedTodos}
/>
)}
</div>

<ErrorMessage errorMessage={errorMessage} />
</div>
);
};
23 changes: 23 additions & 0 deletions src/Components/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import classNames from 'classnames';

type Props = {
errorMessage: string;
};

export const ErrorMessage: React.FC<Props> = ({ errorMessage }) => {
return (
<div
data-cy="ErrorNotification"
className={classNames(
'notification',
'is-danger',
'is-light',
'has-text-weight-normal',
{ hidden: !errorMessage },
)}
>
<button data-cy="HideErrorButton" type="button" className="delete" />
{errorMessage}
</div>
);
};
72 changes: 72 additions & 0 deletions src/Components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import classNames from 'classnames';
import { Todo } from '../types/Todo';
import { FilterName } from '../types/FilterName';

type Props = {
todos: Todo[];
activeFilter: FilterName;
setActiveFilter: (filterName: FilterName) => void;
activeTodosQuantity: number;
deleteCompletedTodos: () => void;
};

export const Footer: React.FC<Props> = ({
todos,
activeFilter,
setActiveFilter,
activeTodosQuantity,
deleteCompletedTodos,
}) => {
return (
<footer className="todoapp__footer" data-cy="Footer">
<span className="todo-count" data-cy="TodosCounter">
{activeTodosQuantity} items left
</span>

<nav className="filter" data-cy="Filter">
<a
href="#/"
className={classNames('filter__link', {
selected: activeFilter === 'ALL',
})}
data-cy="FilterLinkAll"
onClick={() => setActiveFilter('ALL')}
>
All
</a>

<a
href="#/active"
className={classNames('filter__link', {
selected: activeFilter === 'ACTIVE',
})}
data-cy="FilterLinkActive"
onClick={() => setActiveFilter('ACTIVE')}
>
Active
</a>

<a
href="#/completed"
className={classNames('filter__link', {
selected: activeFilter === 'COMPLETED',
})}
data-cy="FilterLinkCompleted"
onClick={() => setActiveFilter('COMPLETED')}
>
Completed
</a>
</nav>

<button
type="button"
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
disabled={todos.length === activeTodosQuantity}
onClick={() => deleteCompletedTodos()}
>
Clear completed
</button>
</footer>
);
};
Loading

0 comments on commit 6843b11

Please sign in to comment.