Skip to content

Commit

Permalink
Add solution
Browse files Browse the repository at this point in the history
  • Loading branch information
Wave committed Sep 22, 2023
1 parent 6532826 commit f807943
Show file tree
Hide file tree
Showing 19 changed files with 757 additions and 14 deletions.
242 changes: 229 additions & 13 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,240 @@
/* eslint-disable max-len */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import React, {
useCallback,
useEffect, useMemo, useState,
} from 'react';
import { UserWarning } from './UserWarning';

const USER_ID = 0;
import { TodoList } from './components/TodoList';
import { Form } from './components/Form';
import { Footer } from './components/Footer';
import { Notifications } from './components/Notifications';
import { Todo } from './types/Todo';
import * as todoService from './api/todos';
import * as TodoFilter from './utils/TodoFilter';
import { FilterType } from './types/FilterType';
import { NOTIFICATION } from './types/Notification';
import { USER_ID } from './constants/USER_ID';
import { findTodoById } from './utils/FindPostById';

export const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [filterType, setFilterType] = useState<FilterType>(FilterType.ALL);
const [notification, setNotification] = useState(NOTIFICATION.CLEAR);
const [loading, setLoading] = useState(false);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [listOfTodoId, setListOfTodoId] = useState<number[]>([]);
const [updatingTodos, setUpdatingTodos] = useState<number[]>([]);
const [titleFocus, setTitleFocus] = useState(true);

useEffect(() => {
todoService.getTodos(USER_ID)
.then((data) => {
setTodos(data);
setTempTodo(null);
}).catch(() => setNotification(NOTIFICATION.LOAD));
}, []);

useEffect(() => {
setListOfTodoId([]);
setUpdatingTodos([]);
}, [todos]);

const addTodo = useCallback((newTodo: Todo) => {
if (!newTodo.title.trim()) {
setNotification(NOTIFICATION.ADD);

throw new Error();
}

setLoading(true);
setTempTodo(newTodo);

todoService.createTodo(newTodo)
.then(todo => {
setTodos(currentTodos => [...currentTodos, todo]);
})
.catch((error) => {
setNotification(NOTIFICATION.ADD);
throw error;
})
.finally(() => {
setLoading(false);
setTempTodo(null);
setTitleFocus(!titleFocus);
});
}, [todos]);

const deleteTodo = useCallback((todoId: number) => {
setLoading(true);

todoService.deleteTodo(todoId)
.then(() => {
setTodos((currentTodos: Todo[]) => {
return currentTodos.filter(todo => todo.id !== todoId);
});
})
.catch((error) => {
setNotification(NOTIFICATION.DELETE);
throw error;
})
.finally(() => {
setLoading(false);
setListOfTodoId([]);
});
}, [todos]);

const deleteCompletedTodo = useCallback(() => {
setLoading(true);

const completedTodosList = TodoFilter.completedTodos(todos);

const completedTodoId = completedTodosList.map((todo) => todo.id);

setListOfTodoId(completedTodoId);

if (!completedTodosList || completedTodosList.length === 0) {
setNotification(NOTIFICATION.NO_COMPLETED);
setLoading(false);

return;
}

completedTodosList.map(todo => deleteTodo(todo.id));
}, [todos]);

const changeTodoCompleted = useCallback((todoId: number) => {
setLoading(true);

const todoForUpdate = findTodoById(todos, todoId);

if (todoForUpdate !== null) {
todoForUpdate.completed = !todoForUpdate.completed;

todoService.updateTodo(todoForUpdate)
.then(todo => {
setTodos(currentTodos => {
const newTodos = [...currentTodos];
const index = newTodos.findIndex(newTodo => {
return newTodo.id === todoForUpdate.id;
});

newTodos.splice(index, 1, todo);

return newTodos;
});
})
.catch((error) => {
setNotification(NOTIFICATION.UPDATE);
throw error;
})
.finally(() => {
setLoading(false);
});
}
}, [todos]);

const reverteCompletedTodo = useCallback((todosForRevert: Todo[]) => {
const todosForUpdate = todosForRevert;
const uncompletedTodosForUpdate = TodoFilter
.uncompletedTodos(todosForRevert);

const todosForUpdateId = uncompletedTodosForUpdate.length !== 0
? uncompletedTodosForUpdate.map(todo => todo.id)
: todosForUpdate.map(todo => todo.id);

setUpdatingTodos(todosForUpdateId);

if (uncompletedTodosForUpdate.length !== 0) {
uncompletedTodosForUpdate.map(todo => changeTodoCompleted(todo.id));
} else {
todosForUpdate.map(todo => changeTodoCompleted(todo.id));
}
}, [todos]);

const updateTodo = useCallback((todoId: number | null, title: string) => {
const todoForUpdate = findTodoById(todos, todoId);

setLoading(true);

if (todoForUpdate !== null) {
todoForUpdate.title = title;

todoService.updateTodo(todoForUpdate)
.then(todo => {
setTodos(currentTodos => {
const newTodos = [...currentTodos];
const index = newTodos.findIndex(newTodo => {
return newTodo.id === todoForUpdate.id;
});

newTodos.splice(index, 1, todo);

return newTodos;
});
})
.catch((error) => {
setNotification(NOTIFICATION.UPDATE);
throw error;
})
.finally(() => {
setLoading(false);
});
}
}, [todos]);

const filteredTodos: Todo[] = useMemo(() => {
return TodoFilter.getFilteredTodos(todos, filterType);
}, [todos, filterType]);

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 className="todoapp__header">
<Form
loading={loading}
todos={todos}
addTodo={(newTodo) => addTodo(newTodo)}
reverteCompletedTodo={
(todosForRevert) => reverteCompletedTodo(todosForRevert)
}
titleFocus={titleFocus}
/>
</header>

<TodoList
listOfTodoId={listOfTodoId}
loading={loading}
todos={filteredTodos}
deleteTodo={deleteTodo}
tempTodo={tempTodo}
updateTodo={(todo, title) => updateTodo(todo, title)}
onChange={(todoId) => changeTodoCompleted(todoId)}
updatingTodos={updatingTodos}
/>

{todos.length !== 0
&& (
<Footer
todos={todos}
filterType={filterType}
setFilterType={setFilterType}
removeCompleted={deleteCompletedTodo}
/>
)}
</div>

{notification !== NOTIFICATION.CLEAR
&& (
<Notifications
notification={notification}
/>
)}
</div>
);
};
23 changes: 23 additions & 0 deletions src/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Todo } from '../types/Todo';
import { client } from '../utils/fetchClient';

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

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

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

export const updateTodo = ({
id,
userId,
title,
completed,
}: Todo) => {
return client.patch<Todo>(`/todos/${id}`, { userId, title, completed });
};
76 changes: 76 additions & 0 deletions src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { Todo } from '../../types/Todo';
import { FilterType } from '../../types/FilterType';
import { completedTodos } from '../../utils/TodoFilter';

type Props = {
todos: Todo[]
filterType: FilterType,
setFilterType: React.Dispatch<React.SetStateAction<FilterType>>,
removeCompleted: () => void,
};

export const Footer: React.FC<Props> = React.memo(
({
todos,
filterType,
setFilterType,
removeCompleted,
}) => {
const listOfUncompletedTodos = useMemo(() => {
return todos.filter(todo => !todo.completed);
}, [todos]);

const listOfCompletedTodos = completedTodos(todos);

return (
<footer className="todoapp__footer">
<span className="todo-count">
{`${listOfUncompletedTodos.length} items left`}
</span>

<nav className="filter centered">
<a
href="#/"
className={classNames('filter__link', {
'filter__link selected': filterType === FilterType.ALL,
})}
onClick={() => setFilterType(FilterType.ALL)}
>
All
</a>

<a
href="#/active"
className={classNames('filter__link', {
'filter__link selected': filterType === FilterType.ACTIVE,
})}
onClick={() => setFilterType(FilterType.ACTIVE)}
>
Active
</a>

<a
href="#/completed"
className={classNames('filter__link', {
'filter__link selected': filterType === FilterType.COMPLETED,
})}
onClick={() => setFilterType(FilterType.COMPLETED)}
>
Completed
</a>
</nav>

<button
type="button"
className="todoapp__clear-completed"
disabled={listOfCompletedTodos.length === 0}
onClick={removeCompleted}
>
Clear completed
</button>
</footer>
);
},
);
1 change: 1 addition & 0 deletions src/components/Footer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Footer';
Loading

0 comments on commit f807943

Please sign in to comment.