Skip to content

Commit

Permalink
solution
Browse files Browse the repository at this point in the history
  • Loading branch information
pogorielova committed Dec 19, 2024
1 parent 62dbb71 commit d58b934
Show file tree
Hide file tree
Showing 12 changed files with 612 additions and 18 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://pogorielova.github.io/react_todo-app-with-api/) and add it to the PR description.
201 changes: 184 additions & 17 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,193 @@
/* eslint-disable max-len */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { UserWarning } from './UserWarning';

const USER_ID = 0;
import * as todoService from './api/todos';
import { Todo } from './types/Todo';
import { Header } from './components/Header';
import { Footer } from './components/Footer';
import { ErrorNotification } from './components/ErrorNotification';
import { TodoItem } from './components/TodoItem';
import { FilterType } from './types/FilterType';
import { ErrorTypes } from './types/ErrorTypes';
import { CSSTransition, TransitionGroup } from 'react-transition-group';

export const App: React.FC = () => {
if (!USER_ID) {
const [todos, setTodos] = useState<Todo[]>([]);
const [errorMessage, setErrorMessage] = useState<ErrorTypes | null>(null);
const [filterType, setFilterType] = useState(FilterType.All);
const [isSubmitting, setIsSubmitting] = useState(false);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [loadingTodoIds, setLoadingTodoIds] = useState<number[]>([]);
const [editingTodo, setEditingTodo] = useState<number | null>(null);

const visibleTodos = useMemo(
() =>
todos.filter(todo => {
if (filterType === FilterType.All) {
return true;
}

return filterType === FilterType.Completed
? todo.completed
: !todo.completed;
}),
[todos, filterType],
);

const inputField = useRef<HTMLInputElement>(null);

const isAllCompleted = todos.every(todo => todo.completed);
const activeTodos = todos.filter(todo => !todo.completed);
const completedTodos = todos.filter(todo => todo.completed);

const onAddTodo = async (newTodoTitle: string) => {
setIsSubmitting(true);
setTempTodo({
id: 0,
title: newTodoTitle,
completed: false,
userId: todoService.USER_ID,
});
try {
const newTodo = await todoService.addTodo({
title: newTodoTitle,
completed: false,
userId: todoService.USER_ID,
});

setTodos(currentTodos => [...currentTodos, newTodo]);
} catch (error) {
setErrorMessage(ErrorTypes.Adding);
inputField?.current?.focus();
throw error;
} finally {
setIsSubmitting(false);
setTempTodo(null);
}
};

const onDeleteTodo = async (todoToDelete: number) => {
setLoadingTodoIds(prev => [...prev, todoToDelete]);
try {
await todoService.deleteTodo(todoToDelete);
setTodos(prev => prev.filter(todo => todo.id !== todoToDelete));
} catch (error) {
setErrorMessage(ErrorTypes.Deleting);
inputField?.current?.focus();
throw error;
} finally {
setLoadingTodoIds(prev => prev.filter(id => id !== todoToDelete));
}
};

const onUpdateTodo = async (todoToUpdate: Todo) => {
setLoadingTodoIds(prev => [...prev, todoToUpdate.id]);
try {
const updated = await todoService.updateTodo(todoToUpdate);

setTodos(prev =>
prev.map(todo => {
return todo.id === updated.id ? updated : todo;
}),
);
} catch (error) {
setErrorMessage(ErrorTypes.Updating);
throw error;
} finally {
setLoadingTodoIds(prev => prev.filter(id => id !== todoToUpdate.id));
}
};

const onToggleAll = async () => {
if (activeTodos.length > 0) {
activeTodos.forEach(todo => {
onUpdateTodo({ ...todo, completed: true });
});
} else {
todos.forEach(todo => {
onUpdateTodo({ ...todo, completed: false });
});
}
};

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

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

useEffect(() => {
todoService
.getTodos()
.then(setTodos)
.catch(() => {
setErrorMessage(ErrorTypes.Loading);
});
}, []);

if (!todoService.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
isAllCompleted={isAllCompleted}
onAddTodo={onAddTodo}
isSubmitting={isSubmitting}
setErrorMessage={setErrorMessage}
todosLength={todos.length}
inputField={inputField}
onToggleAll={onToggleAll}
/>
<section className="todoapp__main" data-cy="TodoList">
<TransitionGroup>
{visibleTodos.map((todo: Todo) => (
<CSSTransition key={todo.id} timeout={300} classNames="item">
<TodoItem
todo={todo}
onDeleteTodo={onDeleteTodo}
onUpdateTodo={onUpdateTodo}
key={todo.id}
isLoading={loadingTodoIds.includes(todo.id)}
isEditing={editingTodo === todo.id}
setEditingTodo={setEditingTodo}
/>
</CSSTransition>
))}
{tempTodo && (
<CSSTransition key={0} timeout={300} classNames="temp-item">
<TodoItem
todo={tempTodo}
onDeleteTodo={onDeleteTodo}
onUpdateTodo={onUpdateTodo}
setEditingTodo={setEditingTodo}
isLoading
/>
</CSSTransition>
)}
</TransitionGroup>
</section>

{todos.length > 0 && (
<Footer
activeTodos={activeTodos}
completedTodos={completedTodos}
filterType={filterType}
setFilterType={setFilterType}
onClearCompleted={onClearCompleted}
/>
)}
</div>

<ErrorNotification
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
/>
</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 = 2139;

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

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

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

export const updateTodo = (todo: Todo) => {
return client.patch<Todo>(`/todos/${todo.id}`, todo);
};
45 changes: 45 additions & 0 deletions src/components/ErrorNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import classNames from 'classnames';
import { ErrorTypes } from '../types/ErrorTypes';
import { Dispatch, SetStateAction, useEffect } from 'react';

type Props = {
errorMessage: ErrorTypes | null;
setErrorMessage: Dispatch<SetStateAction<ErrorTypes | null>>;
};

export const ErrorNotification: React.FC<Props> = ({
errorMessage,
setErrorMessage,
}) => {
useEffect(() => {
if (errorMessage === null) {
return;
}

const timerId = setTimeout(() => {
setErrorMessage(null);
}, 3000);

return () => {
clearTimeout(timerId);
};
}, [errorMessage, setErrorMessage]);

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

type Props = {
activeTodos: Todo[];
completedTodos: Todo[];
filterType: FilterType;
setFilterType: (selectedOption: FilterType) => void;
onClearCompleted: () => Promise<void>;
};

export const Footer: React.FC<Props> = ({
activeTodos,
filterType,
completedTodos,
setFilterType,
onClearCompleted,
}) => {
return (
<footer className="todoapp__footer" data-cy="Footer">
<span className="todo-count" data-cy="TodosCounter">
{activeTodos.length} items left
</span>

<nav className="filter" data-cy="Filter">
{Object.values(FilterType).map(filter => (
<a
href={`#/${filter === FilterType.All ? '' : filter.toLowerCase()}`}
key={filter}
className={classNames('filter__link', {
selected: filterType === filter,
})}
data-cy={`FilterLink${filter}`}
onClick={() => setFilterType(filter)}
>
{filter}
</a>
))}
</nav>

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

0 comments on commit d58b934

Please sign in to comment.