Skip to content

Commit

Permalink
add task solution
Browse files Browse the repository at this point in the history
  • Loading branch information
LevytskyiV committed Dec 22, 2024
1 parent 62dbb71 commit ffa61ea
Show file tree
Hide file tree
Showing 10 changed files with 645 additions and 14 deletions.
261 changes: 247 additions & 14 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,259 @@
/* 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, {
useCallback,
useEffect,
useState,
useMemo,
useRef,
} from 'react';
import { UserWarning } from './UserWarning';
import { getTodos, USER_ID } from './api/api';
import * as todoService from './api/api';

const USER_ID = 0;
import { Todo } from './types/Todo';
import { Filter } from './types/Filter';

import { Header } from './components/Header';
import { TodoList } from './components/TodoLIst';
import { Footer } from './components/Footer';
import { Notification } from './components/Notification';

export const App: React.FC = () => {
const [todosFromServer, setTodosFromServer] = useState<Todo[]>([]);
const [filter, setFilter] = useState<Filter>(Filter.All);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [loadingTodoIds, setLoadingTodoIds] = useState<number[]>([]);
const [notification, setNotification] = useState({
isHidden: true,
message: '',
});

const inputRef = useRef<HTMLInputElement>(null);

const showNotification = (message: string) => {
setNotification({ isHidden: false, message });
setTimeout(() => setNotification({ isHidden: true, message: '' }), 3000);
};

const filterTodos = useCallback((todos: Todo[], filterBy: Filter): Todo[] => {
switch (filterBy) {
case Filter.Completed:
return todos.filter(todo => todo.completed);

case Filter.Active:
return todos.filter(todo => !todo.completed);

default:
return todos;
}
}, []);

const visibleTodos = useMemo(
() => filterTodos(todosFromServer, filter),
[filterTodos, todosFromServer, filter],
);

const activeTodosCount = useMemo(
() => todosFromServer.filter(todo => !todo.completed).length,
[todosFromServer],
);

const allTodosCompleted = useMemo(
() => activeTodosCount === 0,
[activeTodosCount],
);

const hasCompletedTodos = useMemo(
() => todosFromServer.some(todo => todo.completed),
[todosFromServer],
);

const handleAddTodo = (title: string) => {
setNotification({ isHidden: true, message: '' });

const trimmedTitle = title.trim();

if (!trimmedTitle.length) {
showNotification('Title should not be empty');

return Promise.reject('Title is empty');
}

setTempTodo({
title: trimmedTitle,
userId: USER_ID,
completed: false,
id: 0,
});

return todoService
.createTodo({ title: trimmedTitle, userId: USER_ID, completed: false })
.then(newTodo => {
setTodosFromServer(currentTodos => [...currentTodos, newTodo]);
})
.catch(error => {
showNotification('Unable to add a todo');
throw new Error(error);
})
.finally(() => {
setTempTodo(null);
});
};

const handleDeleteTodo = (todoId: number) => {
setLoadingTodoIds([todoId]);

return todoService
.deleteTodo(todoId)
.then(() => {
setTodosFromServer(curr => curr.filter(todo => todo.id !== todoId));
})
.catch(error => {
showNotification('Unable to delete a todo');
throw new Error(error);
})
.finally(() => {
setLoadingTodoIds([]);
if (inputRef.current) {
inputRef.current.focus();
}
});
};

const handleClearCompletedTodos = () => {
const completedTodoIds = todosFromServer
.filter(todo => todo.completed)
.map(todo => todo.id);

setLoadingTodoIds(completedTodoIds);
Promise.all(
completedTodoIds.map(id =>
todoService
.deleteTodo(id)
.then(() => {
setTodosFromServer(curr => curr.filter(todo => todo.id !== id));
})
.catch(error => {
showNotification('Unable to delete a todo');
throw new Error(error);
})
.finally(() => {
setLoadingTodoIds([]);
if (inputRef.current) {
inputRef.current.focus();
}
}),
),
);
};

const handleUpdateTodo = (updatedTodo: Todo) => {
setLoadingTodoIds([updatedTodo.id]);

return todoService
.updateTodo(updatedTodo)
.then(receivedTodo => {
setTodosFromServer(curr =>
curr.map(todo => (todo.id === receivedTodo.id ? receivedTodo : todo)),
);
})
.catch(error => {
showNotification('Unable to update a todo');
throw new Error(error);
})
.finally(() => {
setLoadingTodoIds([]);
});
};

const handleToggleAllTodoStatus = () => {
let todosToChange = [];

if (allTodosCompleted) {
todosToChange = [...todosFromServer];
} else {
todosToChange = todosFromServer.filter(todo => !todo.completed);
}

const todoToChangeIds = todosToChange.map(todo => todo.id);

setLoadingTodoIds(todoToChangeIds);
Promise.all(
todosToChange.map(todoToChange => {
const { id, completed, title, userId } = todoToChange;

todoService
.updateTodo({ id, completed: !completed, title, userId })
.then(receivedTodo => {
setTodosFromServer(curr =>
curr.map(todo =>
todo.id === receivedTodo.id ? receivedTodo : todo,
),
);
})
.catch(error => {
showNotification('Unable to update a todo');
throw new Error(error);
})
.finally(() => {
setLoadingTodoIds([]);
});
}),
);
};

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

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">
<Header
inputRef={inputRef}
hasTodos={!!todosFromServer.length}
allCompletedTodos={allTodosCompleted}
onAddTodo={handleAddTodo}
onToggleAll={handleToggleAllTodoStatus}
/>

{!!todosFromServer.length && (
<TodoList
todos={visibleTodos}
tempTodo={tempTodo}
onDeleteTodo={handleDeleteTodo}
loading={loadingTodoIds}
onUpdateTodo={handleUpdateTodo}
/>
)}

{!!todosFromServer.length && (
<Footer
activeTodosCount={activeTodosCount}
currFilter={filter}
hasCompletedTodos={hasCompletedTodos}
onFilter={setFilter}
onClearCompletedTodos={handleClearCompletedTodos}
/>
)}
</div>

<Notification
message={notification.message}
isHidden={notification.isHidden}
onClose={() => setNotification({ ...notification, isHidden: true })}
/>
</div>
);
};
22 changes: 22 additions & 0 deletions src/api/api.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Todo } from '../types/Todo';
import { client } from '../utils/fetchClients';

export const USER_ID = 2159;

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

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

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

export function updateTodo(data: Todo): Promise<Todo> {
const { id } = data;

return client.patch(`/todos/${id}`, data);
}
57 changes: 57 additions & 0 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import classNames from 'classnames';
import { Filter } from '../types/Filter';

type Props = {
activeTodosCount: number;
currFilter: Filter;
hasCompletedTodos: boolean;
onFilter: (newFilter: Filter) => void;
onClearCompletedTodos: () => void;
};

export const Footer: React.FC<Props> = ({
activeTodosCount,
currFilter,
hasCompletedTodos,
onFilter,
onClearCompletedTodos,
}) => {
return (
<footer className="todoapp__footer" data-cy="Footer">
<span className="todo-count" data-cy="TodosCounter">
{`${activeTodosCount} items left`}
</span>

<nav className="filter" data-cy="Filter">
{Object.values(Filter).map(filter => {
const capitalizedFilter =
filter[0].toUpperCase() + filter.slice(1).toLowerCase();

return (
<a
key={filter}
href={`#/${filter === Filter.All ? '' : filter}`}
className={classNames('filter__link', {
selected: currFilter === filter,
})}
data-cy={`FilterLink${capitalizedFilter}`}
onClick={() => onFilter(filter)}
>
{capitalizedFilter}
</a>
);
})}
</nav>

<button
type="button"
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
onClick={onClearCompletedTodos}
disabled={!hasCompletedTodos}
>
Clear completed
</button>
</footer>
);
};
Loading

0 comments on commit ffa61ea

Please sign in to comment.