From 9b3c6c15405ac4358333dcc4c579a2100c66bffc Mon Sep 17 00:00:00 2001 From: Vitalii Fedusov Date: Mon, 25 Sep 2023 21:41:26 +0300 Subject: [PATCH 1/9] copy src folder from add and delete task --- src/App.tsx | 178 +++++++++++++++++++++++++++-- src/api/todos.ts | 14 +++ src/components/Error/Error.tsx | 33 ++++++ src/components/Footer/Footer.tsx | 94 +++++++++++++++ src/components/Header/Header.tsx | 81 +++++++++++++ src/components/Section/Section.tsx | 132 +++++++++++++++++++++ src/types/Todo.ts | 6 + src/utils/fetchClient.ts | 43 +++++++ 8 files changed, 569 insertions(+), 12 deletions(-) create mode 100644 src/api/todos.ts create mode 100644 src/components/Error/Error.tsx create mode 100644 src/components/Footer/Footer.tsx create mode 100644 src/components/Header/Header.tsx create mode 100644 src/components/Section/Section.tsx create mode 100644 src/types/Todo.ts create mode 100644 src/utils/fetchClient.ts diff --git a/src/App.tsx b/src/App.tsx index 5749bdf784..7f7e5d35c6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,178 @@ -/* eslint-disable max-len */ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; +// #region import +import React, { useEffect, useState } from 'react'; import { UserWarning } from './UserWarning'; +import { Footer, Status } from './components/Footer/Footer'; +import { Header } from './components/Header/Header'; +import { Section } from './components/Section/Section'; +import { Error } from './components/Error/Error'; +import { Todo } from './types/Todo'; +import * as todoService from './api/todos'; +// #endregion +const USER_ID = 11449; -const USER_ID = 0; +function getVisibleTodos(todos: Todo[], newStatus: Status) { + switch (newStatus) { + case Status.ACTIVE: + return todos.filter(todo => !todo.completed); + + case Status.COMPLETED: + return todos.filter(todo => todo.completed); + + default: + return todos; + } +} export const App: React.FC = () => { + // #region loadTodos + const [todos, setTodos] = useState([]); + const [errorMessage, setErrorMessage] = useState(''); + const [status, setStatus] = useState(Status.ALL); + const [isLoading, setIsLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [tempTodo, setTempTodo] = useState(null); + const [newTitle, setNewTitle] = useState(''); + const visibleTodos = getVisibleTodos(todos, status); + + const completedTodos = todos.filter(todo => todo.completed); + const idsOfCompletedTodos = completedTodos.map(todo => todo.id); + + const clearError = () => { + if (errorMessage) { + setTimeout(() => { + setErrorMessage(''); + }, 3000); + } + }; + + useEffect(clearError, [errorMessage]); + + function loadTodos() { + todoService.getTodos(USER_ID) + .then(response => { + setTodos(response); + }) + .catch(() => { + setErrorMessage('Unable to load todos'); + }); + } + + useEffect(loadTodos, []); + // #endregion + + // #region add, delete, update + const addTodo = ({ userId, title, completed }: Todo) => { + setIsLoading(true); + + const promise = todoService.createTodo({ + userId, title: title.trim(), completed, + }) + .then(newTodo => { + setTodos(currentTodos => [...currentTodos, newTodo]); + setNewTitle(''); + }) + .catch(() => { + setErrorMessage('Unable to add a todo'); + clearError(); + }) + .finally(() => { + setIsLoading(false); + setTempTodo(null); + }); + + setTempTodo({ + id: 0, userId: USER_ID, title, completed, + }); + + return promise; + }; + + const deleteTodo = (id: number) => { + setIsLoading(true); + setSelectedId(id); + + return todoService.deleteTodo(id) + .then(() => { + setTodos(currentTodos => currentTodos.filter(todo => todo.id !== id)); + }) + .catch(() => { + setErrorMessage('Unable to delete a todo'); + }) + .finally(() => { + setIsLoading(false); + setSelectedId(null); + }); + }; + + const deleteCompleted = () => { + setIsLoading(true); + + return Promise.all( + idsOfCompletedTodos.map(id => todoService.deleteTodo(id)), + ) + .then(() => { + setTodos( + currentTodos => currentTodos.filter( + todo => !idsOfCompletedTodos.includes(todo.id), + ), + ); + }) + .catch(() => { + setErrorMessage('Unable to delete a todo'); + }) + .finally(() => { + setIsLoading(false); + setSelectedId(null); + }); + }; + + // #endregion if (!USER_ID) { return ; } return ( -
-

- Copy all you need from the prev task: -
- React Todo App - Add and Delete -

- -

Styles are already copied

-
+
+

todos

+ +
+ +
+ + {visibleTodos && ( +
+ )} + + {(todos.length > 0 || tempTodo) && ( +
+ )} + +
+ + +
); }; diff --git a/src/api/todos.ts b/src/api/todos.ts new file mode 100644 index 0000000000..f9f813c1c1 --- /dev/null +++ b/src/api/todos.ts @@ -0,0 +1,14 @@ +import { Todo } from '../types/Todo'; +import { client } from '../utils/fetchClient'; + +export const getTodos = (userId: number) => { + return client.get(`/todos?userId=${userId}`); +}; + +export const createTodo = ({ userId, title, completed }: Omit) => { + return client.post('/todos', { userId, title, completed }); +}; + +export const deleteTodo = (id: number) => { + return client.delete(`/todos/${id}`); +}; diff --git a/src/components/Error/Error.tsx b/src/components/Error/Error.tsx new file mode 100644 index 0000000000..d6f09583cb --- /dev/null +++ b/src/components/Error/Error.tsx @@ -0,0 +1,33 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ + +import classNames from 'classnames'; + +type Props = { + errorMessage: string; + setErrorMessage: (newMessage: string) => void; +}; + +export const Error: React.FC = ({ + errorMessage, + setErrorMessage = () => { }, +}) => { + return ( +
+
+ ); +}; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 0000000000..1b00172827 --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,94 @@ +import classNames from 'classnames'; +import { Todo } from '../../types/Todo'; + +type Props = { + todos: Todo[], + setStatus: (newStatus: Status) => void, + onClearCompleted: () => void; + currentStatus: Status, +}; + +export enum Status { + ALL = 'All', + ACTIVE = 'Active', + COMPLETED = 'Completed', +} + +export const Footer: React.FC = ({ + todos, + currentStatus, + setStatus = () => {}, + onClearCompleted = () => {}, +}) => { + const completedTodos = todos.filter(todo => todo.completed); + const activeTodos = todos.filter(todo => !todo.completed); + + const handleClick = (event: React.MouseEvent) => { + const newStatus = event.currentTarget.textContent as Status; + + setStatus(newStatus); + }; + + return ( + + ); +}; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 0000000000..f23397eae6 --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,81 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ + +import { useEffect, useRef } from 'react'; +import { Todo } from '../../types/Todo'; + +type Props = { + onSubmit: (todo: Todo) => Promise; + todos: Todo[]; + setErrorMessage: (message: string) => void; + userId: number; + isLoading: boolean; + newTitle: string; + setNewTitle: (title: string) => void; +}; + +export const Header: React.FC = ({ + todos, + onSubmit, + setErrorMessage = () => { }, + userId, + isLoading, + newTitle, + setNewTitle = () => { }, +}) => { + // #region state + const inputReference = useRef(null); + + useEffect(() => { + if (inputReference.current && !isLoading) { + inputReference.current.focus(); + } + }, [isLoading]); + // #endregion + + // #region handlers + const handleTitleChange = (event: React.ChangeEvent) => { + setNewTitle(event.target.value); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + if (!newTitle.trim()) { + setErrorMessage('Title should not be empty'); + + return; + } + + onSubmit({ + id: 0, userId, title: newTitle.trim(), completed: false, + }); + }; + // #endregion + + return ( +
+ {todos.length > 0 && ( +
+ ); +}; diff --git a/src/components/Section/Section.tsx b/src/components/Section/Section.tsx new file mode 100644 index 0000000000..8a501d0a70 --- /dev/null +++ b/src/components/Section/Section.tsx @@ -0,0 +1,132 @@ +import classNames from 'classnames'; +import { + CSSTransition, + TransitionGroup, +} from 'react-transition-group'; +import { Todo } from '../../types/Todo'; + +type Props = { + visibleTodos: Todo[]; + onDelete?: (id: number) => void; + selectedId: number | null; + isLoading: boolean; + tempTodo: Todo | null; +}; + +export const Section: React.FC = ({ + visibleTodos, + onDelete = () => { }, + selectedId, + isLoading, + tempTodo, +}) => { + return ( +
+ + {visibleTodos.map(todo => ( + +
+ + + + {todo.title} + + + +
+
+
+
+
+ + ))} + {tempTodo && ( + +
+ + + + {tempTodo.title} + + + +
+
+
+
+
+ + )} + +
+ ); +}; diff --git a/src/types/Todo.ts b/src/types/Todo.ts new file mode 100644 index 0000000000..3f52a5fdde --- /dev/null +++ b/src/types/Todo.ts @@ -0,0 +1,6 @@ +export interface Todo { + id: number; + userId: number; + title: string; + completed: boolean; +} diff --git a/src/utils/fetchClient.ts b/src/utils/fetchClient.ts new file mode 100644 index 0000000000..b7dd63c6ad --- /dev/null +++ b/src/utils/fetchClient.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +const BASE_URL = 'https://mate.academy/students-api'; + +function wait(delay: number) { + return new Promise(resolve => { + setTimeout(resolve, delay); + }); +} + +type RequestMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE'; + +function request( + url: string, + method: RequestMethod = 'GET', + data: any = null, +): Promise { + const options: RequestInit = { method }; + + if (data) { + options.body = JSON.stringify(data); + options.headers = { + 'Content-Type': 'application/json; charset=UTF-8', + }; + } + + // we wait for testing purpose to see loaders + return wait(300) + .then(() => fetch(BASE_URL + url, options)) + .then(response => { + if (!response.ok) { + throw new Error(); + } + + return response.json(); + }); +} + +export const client = { + get: (url: string) => request(url), + post: (url: string, data: any) => request(url, 'POST', data), + patch: (url: string, data: any) => request(url, 'PATCH', data), + delete: (url: string) => request(url, 'DELETE'), +}; From 07e8475ff5ae50ec2007bb67f7fff655d2c0e4cc Mon Sep 17 00:00:00 2001 From: Vitalii Fedusov Date: Thu, 28 Sep 2023 21:21:53 +0300 Subject: [PATCH 2/9] implement toggling --- src/App.tsx | 93 +++++++++++++++++++++++++++++- src/api/todos.ts | 6 ++ src/components/Header/Header.tsx | 19 ++++-- src/components/Section/Section.tsx | 10 ++-- 4 files changed, 114 insertions(+), 14 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7f7e5d35c6..84e6227255 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,10 +30,11 @@ export const App: React.FC = () => { const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState(Status.ALL); const [isLoading, setIsLoading] = useState(false); - const [selectedId, setSelectedId] = useState(null); + const [selectedId, setSelectedId] = useState(null); const [tempTodo, setTempTodo] = useState(null); const [newTitle, setNewTitle] = useState(''); const visibleTodos = getVisibleTodos(todos, status); + const [isFocused, setIsFocused] = useState(true); const completedTodos = todos.filter(todo => todo.completed); const idsOfCompletedTodos = completedTodos.map(todo => todo.id); @@ -64,6 +65,7 @@ export const App: React.FC = () => { // #region add, delete, update const addTodo = ({ userId, title, completed }: Todo) => { setIsLoading(true); + setIsFocused(false); const promise = todoService.createTodo({ userId, title: title.trim(), completed, @@ -79,6 +81,7 @@ export const App: React.FC = () => { .finally(() => { setIsLoading(false); setTempTodo(null); + setIsFocused(true); }); setTempTodo({ @@ -90,7 +93,8 @@ export const App: React.FC = () => { const deleteTodo = (id: number) => { setIsLoading(true); - setSelectedId(id); + setSelectedId([id]); + setIsFocused(false); return todoService.deleteTodo(id) .then(() => { @@ -102,11 +106,14 @@ export const App: React.FC = () => { .finally(() => { setIsLoading(false); setSelectedId(null); + setIsFocused(true); }); }; const deleteCompleted = () => { setIsLoading(true); + setIsFocused(false); + setSelectedId(idsOfCompletedTodos); return Promise.all( idsOfCompletedTodos.map(id => todoService.deleteTodo(id)), @@ -121,6 +128,84 @@ export const App: React.FC = () => { .catch(() => { setErrorMessage('Unable to delete a todo'); }) + .finally(() => { + setIsLoading(false); + setSelectedId(null); + setIsFocused(true); + }); + }; + + const toggleStatus = (todo: Todo) => { + const { + id, userId, title, completed, + } = todo; + + setSelectedId([id]); + setIsLoading(true); + + return todoService.updateTodo({ + id, userId, title, completed: !completed, + }) + .then((newTodo) => { + setTodos(currentTodos => { + const newTodos = [...currentTodos]; + const index = newTodos.findIndex(item => item.id === id); + + newTodos.splice(index, 1, newTodo); + + return newTodos; + }); + }) + .catch(() => { + setErrorMessage('Unable to update a todo'); + }) + .finally(() => { + setSelectedId(null); + setIsLoading(false); + }); + }; + + const toggleAll = () => { + setIsLoading(true); + const activeTodo = todos.filter(todo => !todo.completed); + + let updatingTodo = [...todos]; + + if (activeTodo.length > 0) { + updatingTodo = activeTodo; + } + + const arrOfUpdatingIds = updatingTodo.map(i => i.id); + + setSelectedId(arrOfUpdatingIds); + + return Promise.all( + updatingTodo.map(todo => { + const { + id, userId, title, completed, + } = todo; + + return todoService.updateTodo({ + id, userId, title, completed: !completed, + }); + }), + ) + .then((newTodos) => { + setTodos((currentTodos) => { + const updatedTodos = [...currentTodos]; + + newTodos.forEach(item => { + const index = updatedTodos.findIndex(i => i.id === item.id); + + updatedTodos.splice(index, 1, item); + }); + + return updatedTodos; + }); + }) + .catch(() => { + setErrorMessage('Unable to update a todo'); + }) .finally(() => { setIsLoading(false); setSelectedId(null); @@ -141,11 +226,12 @@ export const App: React.FC = () => {
{visibleTodos && ( @@ -155,6 +241,7 @@ export const App: React.FC = () => { onDelete={deleteTodo} selectedId={selectedId} isLoading={isLoading} + toggleStatus={toggleStatus} /> )} diff --git a/src/api/todos.ts b/src/api/todos.ts index f9f813c1c1..d76c17def9 100644 --- a/src/api/todos.ts +++ b/src/api/todos.ts @@ -12,3 +12,9 @@ export const createTodo = ({ userId, title, completed }: Omit) => { export const deleteTodo = (id: number) => { return client.delete(`/todos/${id}`); }; + +export const updateTodo = ({ + id, userId, title, completed, +}: Todo) => { + return client.patch(`/todos/${id}`, { userId, title, completed }); +}; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index f23397eae6..c645c318c9 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,6 +1,7 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ import { useEffect, useRef } from 'react'; +import classNames from 'classnames'; import { Todo } from '../../types/Todo'; type Props = { @@ -8,9 +9,10 @@ type Props = { todos: Todo[]; setErrorMessage: (message: string) => void; userId: number; - isLoading: boolean; + isFocused: boolean; newTitle: string; setNewTitle: (title: string) => void; + toggleAll: () => void; }; export const Header: React.FC = ({ @@ -18,18 +20,20 @@ export const Header: React.FC = ({ onSubmit, setErrorMessage = () => { }, userId, - isLoading, + isFocused, newTitle, setNewTitle = () => { }, + toggleAll = () => { }, }) => { // #region state const inputReference = useRef(null); + const amountCompletedTodo = todos.filter(todo => todo.completed).length; useEffect(() => { - if (inputReference.current && !isLoading) { + if (inputReference.current && isFocused) { inputReference.current.focus(); } - }, [isLoading]); + }, [isFocused]); // #endregion // #region handlers @@ -56,8 +60,11 @@ export const Header: React.FC = ({
{todos.length > 0 && ( + {isEditing && selectedId?.includes(todo.id) + ? ( +
handleSubmit(event, todo)} + > + handleBlur(todo)} + ref={inputReference} + onChange={handleTitleChange} + value={updatedTitle} + data-cy="TodoTitleField" + type="text" + className="todo__title-field" + placeholder={placeholder} + onKeyUp={handleKeyUp} + /> +
+ ) : ( + <> + handleDoubleClick(todo)} + > + {todo.title} + + + + )}
Date: Fri, 29 Sep 2023 22:26:16 +0300 Subject: [PATCH 4/9] solution --- src/App.tsx | 14 ++++++++--- src/components/Footer/Footer.tsx | 42 ++++++++++++-------------------- src/utils/fetchClient.ts | 1 - 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b0d60a4744..e1ee6984d1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,6 +39,7 @@ export const App: React.FC = () => { const [updatedTitle, setUpdatedTitle] = useState(''); const completedTodos = todos.filter(todo => todo.completed); + const activeTodos = todos.filter(todo => !todo.completed); const idsOfCompletedTodos = completedTodos.map(todo => todo.id); const clearError = () => { @@ -129,6 +130,11 @@ export const App: React.FC = () => { }) .catch(() => { setErrorMessage('Unable to delete a todo'); + setTodos( + currentTodos => currentTodos.filter( + todo => !idsOfCompletedTodos.includes(todo.id), + ), + ); }) .finally(() => { setIsLoading(false); @@ -238,14 +244,15 @@ export const App: React.FC = () => { return newTodos; }); + setSelectedId(null); + setIsEditing(false); }) .catch(() => { + setIsLoading(false); setErrorMessage('Unable to update a todo'); }) .finally(() => { - setSelectedId(null); setIsLoading(false); - setIsEditing(false); }); }; @@ -290,10 +297,11 @@ export const App: React.FC = () => { {(todos.length > 0 || tempTodo) && (
)} diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index 1b00172827..9c360e68ab 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -2,10 +2,11 @@ import classNames from 'classnames'; import { Todo } from '../../types/Todo'; type Props = { - todos: Todo[], setStatus: (newStatus: Status) => void, onClearCompleted: () => void; currentStatus: Status, + activeTodos: Todo[], + completedTodos: Todo[], }; export enum Status { @@ -15,14 +16,12 @@ export enum Status { } export const Footer: React.FC = ({ - todos, currentStatus, - setStatus = () => {}, - onClearCompleted = () => {}, + activeTodos, + completedTodos, + setStatus = () => { }, + onClearCompleted = () => { }, }) => { - const completedTodos = todos.filter(todo => todo.completed); - const activeTodos = todos.filter(todo => !todo.completed); - const handleClick = (event: React.MouseEvent) => { const newStatus = event.currentTarget.textContent as Status; @@ -70,25 +69,16 @@ export const Footer: React.FC = ({ - {completedTodos.length > 0 ? ( - - ) : ( - - )} + +
); }; diff --git a/src/utils/fetchClient.ts b/src/utils/fetchClient.ts index b7dd63c6ad..6d5e913e4f 100644 --- a/src/utils/fetchClient.ts +++ b/src/utils/fetchClient.ts @@ -23,7 +23,6 @@ function request( }; } - // we wait for testing purpose to see loaders return wait(300) .then(() => fetch(BASE_URL + url, options)) .then(response => { From ee368e33a1394970d6a45a76b5c42262a8a5e427 Mon Sep 17 00:00:00 2001 From: Vitalii Fedusov Date: Wed, 4 Oct 2023 22:51:41 +0300 Subject: [PATCH 5/9] rewrite using context --- src/App.tsx | 317 +---------------------------- src/TodoApp.tsx | 294 ++++++++++++++++++++++++++ src/TodoContext.tsx | 68 +++++++ src/components/Error/Error.tsx | 11 +- src/components/Footer/Footer.tsx | 2 +- src/components/Header/Header.tsx | 15 +- src/components/Section/Section.tsx | 22 +- 7 files changed, 389 insertions(+), 340 deletions(-) create mode 100644 src/TodoApp.tsx create mode 100644 src/TodoContext.tsx diff --git a/src/App.tsx b/src/App.tsx index e1ee6984d1..3c1da33734 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,316 +1,11 @@ -/* eslint-disable jsx-a11y/control-has-associated-label */ -// #region import -import React, { useEffect, useState } from 'react'; -import { UserWarning } from './UserWarning'; -import { Footer, Status } from './components/Footer/Footer'; -import { Header } from './components/Header/Header'; -import { Section } from './components/Section/Section'; -import { Error } from './components/Error/Error'; -import { Todo } from './types/Todo'; -import * as todoService from './api/todos'; -// #endregion -const USER_ID = 11449; - -function getVisibleTodos(todos: Todo[], newStatus: Status) { - switch (newStatus) { - case Status.ACTIVE: - return todos.filter(todo => !todo.completed); - - case Status.COMPLETED: - return todos.filter(todo => todo.completed); - - default: - return todos; - } -} +import React from 'react'; +import { TodoApp } from './TodoApp'; +import { TodosProvider } from './TodoContext'; export const App: React.FC = () => { - // #region loadTodos - const [todos, setTodos] = useState([]); - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(Status.ALL); - const [isLoading, setIsLoading] = useState(false); - const [selectedId, setSelectedId] = useState(null); - const [tempTodo, setTempTodo] = useState(null); - const [newTitle, setNewTitle] = useState(''); - const visibleTodos = getVisibleTodos(todos, status); - const [isFocused, setIsFocused] = useState(true); - const [isEditing, setIsEditing] = useState(false); - const [updatedTitle, setUpdatedTitle] = useState(''); - - const completedTodos = todos.filter(todo => todo.completed); - const activeTodos = todos.filter(todo => !todo.completed); - const idsOfCompletedTodos = completedTodos.map(todo => todo.id); - - const clearError = () => { - if (errorMessage) { - setTimeout(() => { - setErrorMessage(''); - }, 3000); - } - }; - - useEffect(clearError, [errorMessage]); - - function loadTodos() { - todoService.getTodos(USER_ID) - .then(response => { - setTodos(response); - }) - .catch(() => { - setErrorMessage('Unable to load todos'); - }); - } - - useEffect(loadTodos, []); - // #endregion - - // #region add, delete, update - const addTodo = ({ userId, title, completed }: Todo) => { - setIsLoading(true); - setIsFocused(false); - - const promise = todoService.createTodo({ - userId, title: title.trim(), completed, - }) - .then(newTodo => { - setTodos(currentTodos => [...currentTodos, newTodo]); - setNewTitle(''); - }) - .catch(() => { - setErrorMessage('Unable to add a todo'); - clearError(); - }) - .finally(() => { - setIsLoading(false); - setTempTodo(null); - setIsFocused(true); - }); - - setTempTodo({ - id: 0, userId: USER_ID, title, completed, - }); - - return promise; - }; - - const deleteTodo = (id: number) => { - setIsLoading(true); - setSelectedId([id]); - setIsFocused(false); - - return todoService.deleteTodo(id) - .then(() => { - setTodos(currentTodos => currentTodos.filter(todo => todo.id !== id)); - }) - .catch(() => { - setErrorMessage('Unable to delete a todo'); - }) - .finally(() => { - setIsLoading(false); - setSelectedId(null); - setIsFocused(true); - }); - }; - - const deleteCompleted = () => { - setIsLoading(true); - setIsFocused(false); - setSelectedId(idsOfCompletedTodos); - - return Promise.all( - idsOfCompletedTodos.map(id => todoService.deleteTodo(id)), - ) - .then(() => { - setTodos( - currentTodos => currentTodos.filter( - todo => !idsOfCompletedTodos.includes(todo.id), - ), - ); - }) - .catch(() => { - setErrorMessage('Unable to delete a todo'); - setTodos( - currentTodos => currentTodos.filter( - todo => !idsOfCompletedTodos.includes(todo.id), - ), - ); - }) - .finally(() => { - setIsLoading(false); - setSelectedId(null); - setIsFocused(true); - }); - }; - - const toggleStatus = (todo: Todo) => { - const { - id, userId, title, completed, - } = todo; - - setSelectedId([id]); - setIsLoading(true); - - return todoService.updateTodo({ - id, userId, title, completed: !completed, - }) - .then((newTodo) => { - setTodos(currentTodos => { - const newTodos = [...currentTodos]; - const index = newTodos.findIndex(item => item.id === id); - - newTodos.splice(index, 1, newTodo); - - return newTodos; - }); - }) - .catch(() => { - setErrorMessage('Unable to update a todo'); - }) - .finally(() => { - setSelectedId(null); - setIsLoading(false); - }); - }; - - const toggleAll = () => { - setIsLoading(true); - const activeTodo = todos.filter(todo => !todo.completed); - - let updatingTodo = [...todos]; - - if (activeTodo.length > 0) { - updatingTodo = activeTodo; - } - - const arrOfUpdatingIds = updatingTodo.map(i => i.id); - - setSelectedId(arrOfUpdatingIds); - - return Promise.all( - updatingTodo.map(todo => { - const { - id, userId, title, completed, - } = todo; - - return todoService.updateTodo({ - id, userId, title, completed: !completed, - }); - }), - ) - .then((newTodos) => { - setTodos((currentTodos) => { - const updatedTodos = [...currentTodos]; - - newTodos.forEach(item => { - const index = updatedTodos.findIndex(i => i.id === item.id); - - updatedTodos.splice(index, 1, item); - }); - - return updatedTodos; - }); - }) - .catch(() => { - setErrorMessage('Unable to update a todo'); - }) - .finally(() => { - setIsLoading(false); - setSelectedId(null); - }); - }; - - const updateTodo = (todo: Todo) => { - const { - id, userId, completed, - } = todo; - - if (!updatedTitle) { - return deleteTodo(id); - } - - setSelectedId([id]); - setIsLoading(true); - - return todoService.updateTodo({ - id, userId, title: updatedTitle.trim(), completed, - }) - .then((newTodo) => { - setTodos(currentTodos => { - const newTodos = [...currentTodos]; - const index = newTodos.findIndex(item => item.id === id); - - newTodos.splice(index, 1, newTodo); - - return newTodos; - }); - setSelectedId(null); - setIsEditing(false); - }) - .catch(() => { - setIsLoading(false); - setErrorMessage('Unable to update a todo'); - }) - .finally(() => { - setIsLoading(false); - }); - }; - - // #endregion - if (!USER_ID) { - return ; - } - return ( -
-

todos

- -
- -
- - {visibleTodos && ( -
- )} - - {(todos.length > 0 || tempTodo) && ( -
- )} - -
- - -
+ + + ); }; diff --git a/src/TodoApp.tsx b/src/TodoApp.tsx new file mode 100644 index 0000000000..192c2f11b3 --- /dev/null +++ b/src/TodoApp.tsx @@ -0,0 +1,294 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ +// #region import +import React, { useEffect, useState } from 'react'; +import { UserWarning } from './UserWarning'; +import { Footer, Status } from './components/Footer/Footer'; +import { Header } from './components/Header/Header'; +import { Section } from './components/Section/Section'; +import { Error } from './components/Error/Error'; +import { Todo } from './types/Todo'; +import * as todoService from './api/todos'; +import { useTodos } from './TodoContext'; +// #endregion +const USER_ID = 11449; + +function getVisibleTodos(todos: Todo[], newStatus: Status) { + switch (newStatus) { + case Status.ACTIVE: + return todos.filter(todo => !todo.completed); + + case Status.COMPLETED: + return todos.filter(todo => todo.completed); + + default: + return todos; + } +} + +export const TodoApp: React.FC = () => { + // #region loadTodos + const { + todos, + setTodos, + errorMessage, + setErrorMessage, + setNewTitle, + setSelectedId, + setIsEditing, + updatedTitle, + } = useTodos(); + + const [status, setStatus] = useState(Status.ALL); + const [isLoading, setIsLoading] = useState(false); + const [tempTodo, setTempTodo] = useState(null); + const visibleTodos = getVisibleTodos(todos, status); + const [isFocused, setIsFocused] = useState(true); + + const completedTodos = todos.filter(todo => todo.completed); + const activeTodos = todos.filter(todo => !todo.completed); + const idsOfCompletedTodos = completedTodos.map(todo => todo.id); + + const clearError = () => { + if (errorMessage) { + setTimeout(() => { + setErrorMessage(''); + }, 3000); + } + }; + + useEffect(clearError, [errorMessage]); + + function loadTodos() { + todoService.getTodos(USER_ID) + .then(response => { + setTodos(response); + }) + .catch(() => { + setErrorMessage('Unable to load todos'); + }); + } + + useEffect(loadTodos, []); + // #endregion + + // #region add, delete, update + const addTodo = ({ userId, title, completed }: Todo) => { + setIsLoading(true); + setIsFocused(false); + + const promise = todoService.createTodo({ + userId, title: title.trim(), completed, + }) + .then(newTodo => { + setTodos([...todos, newTodo]); + setNewTitle(''); + }) + .catch(() => { + setErrorMessage('Unable to add a todo'); + clearError(); + }) + .finally(() => { + setIsLoading(false); + setTempTodo(null); + setIsFocused(true); + }); + + setTempTodo({ + id: 0, userId: USER_ID, title, completed, + }); + + return promise; + }; + + const deleteTodo = (id: number) => { + setIsLoading(true); + setSelectedId([id]); + setIsFocused(false); + + return todoService.deleteTodo(id) + .then(() => { + setTodos(todos.filter(todo => todo.id !== id)); + }) + .catch(() => { + setErrorMessage('Unable to delete a todo'); + }) + .finally(() => { + setIsLoading(false); + setSelectedId(null); + setIsFocused(true); + }); + }; + + const deleteCompleted = () => { + setIsLoading(true); + setIsFocused(false); + setSelectedId(idsOfCompletedTodos); + + return Promise.all( + idsOfCompletedTodos.map(id => todoService.deleteTodo(id)), + ) + .then(() => { + setTodos(todos.filter(todo => !idsOfCompletedTodos.includes(todo.id))); + }) + .catch(() => { + setErrorMessage('Unable to delete a todo'); + setTodos(todos.filter(todo => !idsOfCompletedTodos.includes(todo.id))); + }) + .finally(() => { + setIsLoading(false); + setSelectedId(null); + setIsFocused(true); + }); + }; + + const toggleStatus = (todo: Todo) => { + const { + id, userId, title, completed, + } = todo; + + setSelectedId([id]); + setIsLoading(true); + + return todoService.updateTodo({ + id, userId, title, completed: !completed, + }) + .then((newTodo) => { + const newTodos = [...todos]; + const index = newTodos.findIndex(item => item.id === id); + + newTodos.splice(index, 1, newTodo); + setTodos(newTodos); + }) + .catch(() => { + setErrorMessage('Unable to update a todo'); + }) + .finally(() => { + setSelectedId(null); + setIsLoading(false); + }); + }; + + const toggleAll = () => { + setIsLoading(true); + const activeTodo = todos.filter(todo => !todo.completed); + + let updatingTodo = [...todos]; + + if (activeTodo.length > 0) { + updatingTodo = activeTodo; + } + + const arrOfUpdatingIds = updatingTodo.map(i => i.id); + + setSelectedId(arrOfUpdatingIds); + + return Promise.all( + updatingTodo.map(todo => { + const { + id, userId, title, completed, + } = todo; + + return todoService.updateTodo({ + id, userId, title, completed: !completed, + }); + }), + ) + .then((newTodos) => { + const updatedTodos = [...todos]; + + newTodos.forEach(item => { + const index = updatedTodos.findIndex(i => i.id === item.id); + + updatedTodos.splice(index, 1, item); + }); + + setTodos(updatedTodos); + }) + .catch(() => { + setErrorMessage('Unable to update a todo'); + }) + .finally(() => { + setIsLoading(false); + setSelectedId(null); + }); + }; + + const updateTodo = (todo: Todo) => { + const { + id, userId, completed, + } = todo; + + if (!updatedTitle) { + return deleteTodo(id); + } + + setSelectedId([id]); + setIsLoading(true); + + return todoService.updateTodo({ + id, userId, title: updatedTitle.trim(), completed, + }) + .then((newTodo) => { + const newTodos = [...todos]; + const index = newTodos.findIndex(item => item.id === id); + + newTodos.splice(index, 1, newTodo); + + setTodos(newTodos); + setSelectedId(null); + setIsEditing(false); + }) + .catch(() => { + setIsLoading(false); + setErrorMessage('Unable to update a todo'); + }) + .finally(() => { + setIsLoading(false); + }); + }; + + // #endregion + if (!USER_ID) { + return ; + } + + return ( +
+

todos

+ +
+ +
+ + {visibleTodos && ( +
+ )} + + {(todos.length > 0 || tempTodo) && ( +
+ )} + +
+ + +
+ ); +}; diff --git a/src/TodoContext.tsx b/src/TodoContext.tsx new file mode 100644 index 0000000000..7e647fbb17 --- /dev/null +++ b/src/TodoContext.tsx @@ -0,0 +1,68 @@ +import React, { useMemo, useState } from 'react'; +import { Todo } from './types/Todo'; + +interface ITodosContext { + todos: Todo[], + setTodos: (newTodos: Todo[]) => void, + errorMessage: string, + setErrorMessage: (newMessage: string) => void, + newTitle: string, + setNewTitle: (newTitle: string) => void, + selectedId: number[] | null, + setSelectedId: (arrOfIds: number[] | null) => void, + isEditing: boolean, + setIsEditing: (status: boolean) => void, + updatedTitle: string, + setUpdatedTitle: (newTitle: string) => void, +} + +export const TodosContext = React.createContext({ + todos: [], + setTodos: () => {}, + errorMessage: '', + setErrorMessage: () => {}, + newTitle: '', + setNewTitle: () => {}, + selectedId: null, + setSelectedId: () => {}, + isEditing: false, + setIsEditing: () => {}, + updatedTitle: '', + setUpdatedTitle: () => {}, +}); + +export const useTodos = (): ITodosContext => React.useContext(TodosContext); + +type Props = { + children: React.ReactNode; +}; + +export const TodosProvider: React.FC = ({ children }) => { + const [todos, setTodos] = useState([]); + const [errorMessage, setErrorMessage] = useState(''); + const [newTitle, setNewTitle] = useState(''); + const [selectedId, setSelectedId] = useState(null); + const [isEditing, setIsEditing] = useState(false); + const [updatedTitle, setUpdatedTitle] = useState(''); + + const value = useMemo(() => ({ + todos, + setTodos, + errorMessage, + setErrorMessage, + newTitle, + setNewTitle, + selectedId, + setSelectedId, + isEditing, + setIsEditing, + updatedTitle, + setUpdatedTitle, + }), [todos, errorMessage, newTitle, selectedId, isEditing, updatedTitle]); + + return ( + + {children} + + ); +}; diff --git a/src/components/Error/Error.tsx b/src/components/Error/Error.tsx index d6f09583cb..247197e95b 100644 --- a/src/components/Error/Error.tsx +++ b/src/components/Error/Error.tsx @@ -1,16 +1,11 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ import classNames from 'classnames'; +import { useTodos } from '../../TodoContext'; -type Props = { - errorMessage: string; - setErrorMessage: (newMessage: string) => void; -}; +export const Error: React.FC = () => { + const { errorMessage, setErrorMessage } = useTodos(); -export const Error: React.FC = ({ - errorMessage, - setErrorMessage = () => { }, -}) => { return (
= ({ className="todoapp__clear-completed" data-cy="ClearCompletedButton" onClick={onClearCompleted} - disabled={completedTodos.length === 0} + disabled={!completedTodos.length} > Clear completed diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index c645c318c9..a5215c651b 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -3,28 +3,27 @@ import { useEffect, useRef } from 'react'; import classNames from 'classnames'; import { Todo } from '../../types/Todo'; +import { useTodos } from '../../TodoContext'; type Props = { onSubmit: (todo: Todo) => Promise; - todos: Todo[]; - setErrorMessage: (message: string) => void; userId: number; isFocused: boolean; - newTitle: string; - setNewTitle: (title: string) => void; toggleAll: () => void; }; export const Header: React.FC = ({ - todos, onSubmit, - setErrorMessage = () => { }, userId, isFocused, - newTitle, - setNewTitle = () => { }, toggleAll = () => { }, }) => { + const { + todos, + setErrorMessage, + newTitle, + setNewTitle, + } = useTodos(); // #region state const inputReference = useRef(null); const amountCompletedTodo = todos.filter(todo => todo.completed).length; diff --git a/src/components/Section/Section.tsx b/src/components/Section/Section.tsx index 201a4b4fee..893719d8e4 100644 --- a/src/components/Section/Section.tsx +++ b/src/components/Section/Section.tsx @@ -5,36 +5,34 @@ import { } from 'react-transition-group'; import { useEffect, useRef } from 'react'; import { Todo } from '../../types/Todo'; +import { useTodos } from '../../TodoContext'; type Props = { visibleTodos: Todo[]; onDelete?: (id: number) => void; - selectedId: number[] | null; isLoading: boolean; tempTodo: Todo | null; toggleStatus: (todo: Todo) => void; - isEditing: boolean; - setIsEditing: React.Dispatch>; onSubmit: (todo: Todo) => Promise; - updatedTitle: string; - setUpdatedTitle: (title: string) => void; - setSelectedId: (id: number[] | null) => void; }; export const Section: React.FC = ({ visibleTodos, onDelete = () => { }, - selectedId, isLoading, tempTodo, toggleStatus = () => { }, - isEditing, - setIsEditing = () => { }, onSubmit, - updatedTitle, - setUpdatedTitle = () => { }, - setSelectedId = () => { }, }) => { + const { + selectedId, + setSelectedId, + isEditing, + setIsEditing, + updatedTitle, + setUpdatedTitle, + } = useTodos(); + const inputReference = useRef(null); useEffect(() => { From 416ed147c116cc563a08ef372c6b5c95a47eae38 Mon Sep 17 00:00:00 2001 From: Vitalii Fedusov Date: Thu, 5 Oct 2023 22:29:22 +0300 Subject: [PATCH 6/9] fix bugs --- src/TodoApp.tsx | 64 +++++++++++++++++++++++++----- src/TodoContext.tsx | 17 +++++++- src/components/Footer/Footer.tsx | 5 ++- src/components/Section/Section.tsx | 5 +-- 4 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src/TodoApp.tsx b/src/TodoApp.tsx index 192c2f11b3..2924f9c03f 100644 --- a/src/TodoApp.tsx +++ b/src/TodoApp.tsx @@ -36,10 +36,11 @@ export const TodoApp: React.FC = () => { setSelectedId, setIsEditing, updatedTitle, + setIsLoading, + isLoading, } = useTodos(); const [status, setStatus] = useState(Status.ALL); - const [isLoading, setIsLoading] = useState(false); const [tempTodo, setTempTodo] = useState(null); const visibleTodos = getVisibleTodos(todos, status); const [isFocused, setIsFocused] = useState(true); @@ -100,12 +101,33 @@ export const TodoApp: React.FC = () => { return promise; }; + const deleteWithEmptyTitle = (id: number) => { + setIsLoading(true); + setSelectedId([id]); + setIsFocused(false); + + const promise = todoService.deleteTodo(id) + .then(() => { + setTodos(todos.filter(todo => todo.id !== id)); + setSelectedId(null); + setIsFocused(false); + }) + .catch(() => { + setErrorMessage('Unable to delete a todo'); + }) + .finally(() => { + setIsLoading(false); + }); + + return promise; + }; + const deleteTodo = (id: number) => { setIsLoading(true); setSelectedId([id]); setIsFocused(false); - return todoService.deleteTodo(id) + const promise = todoService.deleteTodo(id) .then(() => { setTodos(todos.filter(todo => todo.id !== id)); }) @@ -117,6 +139,8 @@ export const TodoApp: React.FC = () => { setSelectedId(null); setIsFocused(true); }); + + return promise; }; const deleteCompleted = () => { @@ -124,15 +148,29 @@ export const TodoApp: React.FC = () => { setIsFocused(false); setSelectedId(idsOfCompletedTodos); - return Promise.all( + return Promise.allSettled( idsOfCompletedTodos.map(id => todoService.deleteTodo(id)), ) - .then(() => { - setTodos(todos.filter(todo => !idsOfCompletedTodos.includes(todo.id))); + .then((response) => { + const arrDeleted: number[] = []; + + idsOfCompletedTodos.forEach((id, index) => { + if (response[index].status === 'fulfilled') { + arrDeleted.push(id); + } + + if (response[index].status === 'rejected') { + setErrorMessage('Unable to delete a todo'); + } + + const updatedTodos + = todos.filter(todo => !arrDeleted.includes(todo.id)); + + setTodos(updatedTodos); + }); }) .catch(() => { setErrorMessage('Unable to delete a todo'); - setTodos(todos.filter(todo => !idsOfCompletedTodos.includes(todo.id))); }) .finally(() => { setIsLoading(false); @@ -218,14 +256,20 @@ export const TodoApp: React.FC = () => { id, userId, completed, } = todo; - if (!updatedTitle) { - return deleteTodo(id); + if (!updatedTitle.trim()) { + if (isLoading) { + return; + } + + deleteWithEmptyTitle(id); + + return; } setSelectedId([id]); setIsLoading(true); - return todoService.updateTodo({ + todoService.updateTodo({ id, userId, title: updatedTitle.trim(), completed, }) .then((newTodo) => { @@ -241,6 +285,7 @@ export const TodoApp: React.FC = () => { .catch(() => { setIsLoading(false); setErrorMessage('Unable to update a todo'); + setIsLoading(false); }) .finally(() => { setIsLoading(false); @@ -270,7 +315,6 @@ export const TodoApp: React.FC = () => { tempTodo={tempTodo} visibleTodos={visibleTodos} onDelete={deleteTodo} - isLoading={isLoading} toggleStatus={toggleStatus} onSubmit={updateTodo} /> diff --git a/src/TodoContext.tsx b/src/TodoContext.tsx index 7e647fbb17..bac317317f 100644 --- a/src/TodoContext.tsx +++ b/src/TodoContext.tsx @@ -14,6 +14,8 @@ interface ITodosContext { setIsEditing: (status: boolean) => void, updatedTitle: string, setUpdatedTitle: (newTitle: string) => void, + isLoading: boolean, + setIsLoading: (status: boolean) => void, } export const TodosContext = React.createContext({ @@ -29,6 +31,8 @@ export const TodosContext = React.createContext({ setIsEditing: () => {}, updatedTitle: '', setUpdatedTitle: () => {}, + isLoading: false, + setIsLoading: () => {}, }); export const useTodos = (): ITodosContext => React.useContext(TodosContext); @@ -44,6 +48,7 @@ export const TodosProvider: React.FC = ({ children }) => { const [selectedId, setSelectedId] = useState(null); const [isEditing, setIsEditing] = useState(false); const [updatedTitle, setUpdatedTitle] = useState(''); + const [isLoading, setIsLoading] = useState(false); const value = useMemo(() => ({ todos, @@ -58,7 +63,17 @@ export const TodosProvider: React.FC = ({ children }) => { setIsEditing, updatedTitle, setUpdatedTitle, - }), [todos, errorMessage, newTitle, selectedId, isEditing, updatedTitle]); + isLoading, + setIsLoading, + }), [ + todos, + errorMessage, + newTitle, + selectedId, + isEditing, + updatedTitle, + isLoading, + ]); return ( diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index fade5462a8..50e9e2e85b 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; import { Todo } from '../../types/Todo'; +import { useTodos } from '../../TodoContext'; type Props = { setStatus: (newStatus: Status) => void, @@ -22,6 +23,8 @@ export const Footer: React.FC = ({ setStatus = () => { }, onClearCompleted = () => { }, }) => { + const { isLoading } = useTodos(); + const handleClick = (event: React.MouseEvent) => { const newStatus = event.currentTarget.textContent as Status; @@ -74,7 +77,7 @@ export const Footer: React.FC = ({ className="todoapp__clear-completed" data-cy="ClearCompletedButton" onClick={onClearCompleted} - disabled={!completedTodos.length} + disabled={!completedTodos.length && !isLoading} > Clear completed diff --git a/src/components/Section/Section.tsx b/src/components/Section/Section.tsx index 893719d8e4..6a7bac9395 100644 --- a/src/components/Section/Section.tsx +++ b/src/components/Section/Section.tsx @@ -10,16 +10,14 @@ import { useTodos } from '../../TodoContext'; type Props = { visibleTodos: Todo[]; onDelete?: (id: number) => void; - isLoading: boolean; tempTodo: Todo | null; toggleStatus: (todo: Todo) => void; - onSubmit: (todo: Todo) => Promise; + onSubmit: (todo: Todo) => void; }; export const Section: React.FC = ({ visibleTodos, onDelete = () => { }, - isLoading, tempTodo, toggleStatus = () => { }, onSubmit, @@ -31,6 +29,7 @@ export const Section: React.FC = ({ setIsEditing, updatedTitle, setUpdatedTitle, + isLoading, } = useTodos(); const inputReference = useRef(null); From 3971900815cd52e635e4594dcb63e8869058c234 Mon Sep 17 00:00:00 2001 From: Vitalii Fedusov Date: Sat, 7 Oct 2023 14:00:28 +0300 Subject: [PATCH 7/9] start implement registration --- src/TodoApp.tsx | 12 ++- src/TodoContext.tsx | 8 ++ src/api/todos.ts | 14 ++++ src/components/LoginPage/LoginPage.tsx | 106 +++++++++++++++++++++++++ src/types/User.ts | 7 ++ src/types/UserData.ts | 6 ++ 6 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 src/components/LoginPage/LoginPage.tsx create mode 100644 src/types/User.ts create mode 100644 src/types/UserData.ts diff --git a/src/TodoApp.tsx b/src/TodoApp.tsx index 2924f9c03f..9f8d373f6b 100644 --- a/src/TodoApp.tsx +++ b/src/TodoApp.tsx @@ -1,7 +1,7 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ // #region import import React, { useEffect, useState } from 'react'; -import { UserWarning } from './UserWarning'; +// import { UserWarning } from './UserWarning'; import { Footer, Status } from './components/Footer/Footer'; import { Header } from './components/Header/Header'; import { Section } from './components/Section/Section'; @@ -9,8 +9,10 @@ import { Error } from './components/Error/Error'; import { Todo } from './types/Todo'; import * as todoService from './api/todos'; import { useTodos } from './TodoContext'; +import { LoginPage } from './components/LoginPage/LoginPage'; // #endregion -const USER_ID = 11449; +// const USER_ID = 11449; +// const USER_ID = 0; function getVisibleTodos(todos: Todo[], newStatus: Status) { switch (newStatus) { @@ -38,6 +40,7 @@ export const TodoApp: React.FC = () => { updatedTitle, setIsLoading, isLoading, + USER_ID, } = useTodos(); const [status, setStatus] = useState(Status.ALL); @@ -69,7 +72,7 @@ export const TodoApp: React.FC = () => { }); } - useEffect(loadTodos, []); + useEffect(loadTodos, [USER_ID]); // #endregion // #region add, delete, update @@ -294,7 +297,8 @@ export const TodoApp: React.FC = () => { // #endregion if (!USER_ID) { - return ; + // return ; + return ; } return ( diff --git a/src/TodoContext.tsx b/src/TodoContext.tsx index bac317317f..4e81af7f32 100644 --- a/src/TodoContext.tsx +++ b/src/TodoContext.tsx @@ -16,6 +16,8 @@ interface ITodosContext { setUpdatedTitle: (newTitle: string) => void, isLoading: boolean, setIsLoading: (status: boolean) => void, + USER_ID: number, + setUserId: (id: number) => void, } export const TodosContext = React.createContext({ @@ -33,6 +35,8 @@ export const TodosContext = React.createContext({ setUpdatedTitle: () => {}, isLoading: false, setIsLoading: () => {}, + USER_ID: 0, + setUserId: () => {}, }); export const useTodos = (): ITodosContext => React.useContext(TodosContext); @@ -49,6 +53,7 @@ export const TodosProvider: React.FC = ({ children }) => { const [isEditing, setIsEditing] = useState(false); const [updatedTitle, setUpdatedTitle] = useState(''); const [isLoading, setIsLoading] = useState(false); + const [USER_ID, setUserId] = useState(0); const value = useMemo(() => ({ todos, @@ -65,6 +70,8 @@ export const TodosProvider: React.FC = ({ children }) => { setUpdatedTitle, isLoading, setIsLoading, + USER_ID, + setUserId, }), [ todos, errorMessage, @@ -73,6 +80,7 @@ export const TodosProvider: React.FC = ({ children }) => { isEditing, updatedTitle, isLoading, + USER_ID, ]); return ( diff --git a/src/api/todos.ts b/src/api/todos.ts index d76c17def9..9272b163d9 100644 --- a/src/api/todos.ts +++ b/src/api/todos.ts @@ -1,4 +1,6 @@ import { Todo } from '../types/Todo'; +import { User } from '../types/User'; +import { UserData } from '../types/UserData'; import { client } from '../utils/fetchClient'; export const getTodos = (userId: number) => { @@ -18,3 +20,15 @@ export const updateTodo = ({ }: Todo) => { return client.patch(`/todos/${id}`, { userId, title, completed }); }; + +export const getAllUsers = () => { + return client.get('/users'); +}; + +export const createUser = ({ + name, username, email, phone, +} : UserData) => { + return client.post('/users', { + name, username, email, phone, + }); +}; diff --git a/src/components/LoginPage/LoginPage.tsx b/src/components/LoginPage/LoginPage.tsx new file mode 100644 index 0000000000..c30689f428 --- /dev/null +++ b/src/components/LoginPage/LoginPage.tsx @@ -0,0 +1,106 @@ +import classNames from 'classnames'; +import { useState } from 'react'; +import { useTodos } from '../../TodoContext'; +import { Error } from '../Error/Error'; +import * as todoService from '../../api/todos'; +import { User } from '../../types/User'; + +export const LoginPage = () => { + const { + isLoading, + setUserId, + setErrorMessage, + setIsLoading, + } = useTodos(); + const [userEmail, setUserEmail] = useState(''); + const [allUsers, setAllUsers] = useState([]); + const name = 'user'; + const username = 'username'; + const phone = '0123456789'; + + const handleChange = (event: React.ChangeEvent) => { + setUserEmail(event.target.value); + }; + + const getUserId = () => { + const isUserExist = allUsers.find(user => user.email === userEmail); + + if (isUserExist) { + return isUserExist.id; + } + + return 0; + }; + + function createUser() { + todoService.createUser({ + name, username, email: userEmail, phone, + }) + .then((user) => { + const newUsers = [...allUsers, user]; + + setAllUsers(newUsers); + setUserId(user.id); + }) + .catch(() => setErrorMessage('Unable to load todos')) + .finally(() => setIsLoading(false)); + } + + function getAllUsers() { + todoService.getAllUsers() + .then((existingUsers) => { + setAllUsers(existingUsers); + }) + .catch(() => setErrorMessage('Unable to load todos')); + } + + const handleSubmit = () => { + setIsLoading(true); + getAllUsers(); + const newId = getUserId(); + + if (!newId) { + createUser(); + } + + setUserId(getUserId()); + }; + + return ( + <> +
+

Log in to open todos

+
+ +
+ + + + +
+
+
+ +
+ + + + + ); +}; diff --git a/src/types/User.ts b/src/types/User.ts new file mode 100644 index 0000000000..7ad005144f --- /dev/null +++ b/src/types/User.ts @@ -0,0 +1,7 @@ +export interface User { + id: number; + name: string, + username: string, + email: string, + phone: string, +} diff --git a/src/types/UserData.ts b/src/types/UserData.ts new file mode 100644 index 0000000000..5f708b99a6 --- /dev/null +++ b/src/types/UserData.ts @@ -0,0 +1,6 @@ +export interface UserData { + name: string, + username: string, + email: string, + phone: string, +} From 1c0a4ac9a7d1b5156764951fea33edc6d5956533 Mon Sep 17 00:00:00 2001 From: Vitalii Fedusov Date: Sat, 7 Oct 2023 23:44:48 +0300 Subject: [PATCH 8/9] continue registration --- README.md | 2 +- src/TodoApp.tsx | 14 ++- src/TodoContext.tsx | 36 +++++-- src/api/todos.ts | 14 +-- src/components/LoginPage/LoginPage.tsx | 125 ++++++++++++++----------- src/hooks/UseLocalStorege.ts | 27 ++++++ src/types/User.ts | 20 ++++ 7 files changed, 156 insertions(+), 82 deletions(-) create mode 100644 src/hooks/UseLocalStorege.ts diff --git a/README.md b/README.md index af7dae81f6..6b1f4100ed 100644 --- a/README.md +++ b/README.md @@ -41,4 +41,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 `` with your Github username in the [DEMO LINK](https://.github.io/react_todo-app-with-api/) and add it to the PR description. +- Replace `` with your Github username in the [DEMO LINK](https://vitalii-fedusov.github.io/react_todo-app-with-api/) and add it to the PR description. diff --git a/src/TodoApp.tsx b/src/TodoApp.tsx index 9f8d373f6b..199b5560c2 100644 --- a/src/TodoApp.tsx +++ b/src/TodoApp.tsx @@ -11,8 +11,6 @@ import * as todoService from './api/todos'; import { useTodos } from './TodoContext'; import { LoginPage } from './components/LoginPage/LoginPage'; // #endregion -// const USER_ID = 11449; -// const USER_ID = 0; function getVisibleTodos(todos: Todo[], newStatus: Status) { switch (newStatus) { @@ -40,7 +38,7 @@ export const TodoApp: React.FC = () => { updatedTitle, setIsLoading, isLoading, - USER_ID, + user, } = useTodos(); const [status, setStatus] = useState(Status.ALL); @@ -63,7 +61,7 @@ export const TodoApp: React.FC = () => { useEffect(clearError, [errorMessage]); function loadTodos() { - todoService.getTodos(USER_ID) + todoService.getTodos(user.id) .then(response => { setTodos(response); }) @@ -72,7 +70,7 @@ export const TodoApp: React.FC = () => { }); } - useEffect(loadTodos, [USER_ID]); + useEffect(loadTodos, [user]); // #endregion // #region add, delete, update @@ -98,7 +96,7 @@ export const TodoApp: React.FC = () => { }); setTempTodo({ - id: 0, userId: USER_ID, title, completed, + id: 0, userId: user.id, title, completed, }); return promise; @@ -296,7 +294,7 @@ export const TodoApp: React.FC = () => { }; // #endregion - if (!USER_ID) { + if (!user.id) { // return ; return ; } @@ -309,7 +307,7 @@ export const TodoApp: React.FC = () => {
diff --git a/src/TodoContext.tsx b/src/TodoContext.tsx index 4e81af7f32..b4edaa9232 100644 --- a/src/TodoContext.tsx +++ b/src/TodoContext.tsx @@ -1,5 +1,7 @@ import React, { useMemo, useState } from 'react'; import { Todo } from './types/Todo'; +import { useLocalStorage } from './hooks/UseLocalStorege'; +import { User } from './types/User'; interface ITodosContext { todos: Todo[], @@ -16,10 +18,28 @@ interface ITodosContext { setUpdatedTitle: (newTitle: string) => void, isLoading: boolean, setIsLoading: (status: boolean) => void, - USER_ID: number, - setUserId: (id: number) => void, + user: User, + setUser: (user: User) => void, } +const defaultUser: User = { + id: 0, + name: 'dafaultName', + username: 'defaultUserName', + email: 'defaultEmail', + phone: 'dafaultPhone', +}; +// const defaultUser: User = { +// createdAt: '', +// email: '', +// id: 0, +// name: '', +// phone: null, +// updatedAt: '', +// username: null, +// website: null, +// }; + export const TodosContext = React.createContext({ todos: [], setTodos: () => {}, @@ -35,8 +55,8 @@ export const TodosContext = React.createContext({ setUpdatedTitle: () => {}, isLoading: false, setIsLoading: () => {}, - USER_ID: 0, - setUserId: () => {}, + user: defaultUser, + setUser: () => {}, }); export const useTodos = (): ITodosContext => React.useContext(TodosContext); @@ -53,7 +73,7 @@ export const TodosProvider: React.FC = ({ children }) => { const [isEditing, setIsEditing] = useState(false); const [updatedTitle, setUpdatedTitle] = useState(''); const [isLoading, setIsLoading] = useState(false); - const [USER_ID, setUserId] = useState(0); + const [user, setUser] = useLocalStorage('user', defaultUser); const value = useMemo(() => ({ todos, @@ -70,8 +90,8 @@ export const TodosProvider: React.FC = ({ children }) => { setUpdatedTitle, isLoading, setIsLoading, - USER_ID, - setUserId, + user, + setUser, }), [ todos, errorMessage, @@ -80,7 +100,7 @@ export const TodosProvider: React.FC = ({ children }) => { isEditing, updatedTitle, isLoading, - USER_ID, + user, ]); return ( diff --git a/src/api/todos.ts b/src/api/todos.ts index 9272b163d9..f0f656fd86 100644 --- a/src/api/todos.ts +++ b/src/api/todos.ts @@ -1,6 +1,6 @@ import { Todo } from '../types/Todo'; import { User } from '../types/User'; -import { UserData } from '../types/UserData'; +// import { UserData } from '../types/UserData'; import { client } from '../utils/fetchClient'; export const getTodos = (userId: number) => { @@ -21,14 +21,6 @@ export const updateTodo = ({ return client.patch(`/todos/${id}`, { userId, title, completed }); }; -export const getAllUsers = () => { - return client.get('/users'); -}; - -export const createUser = ({ - name, username, email, phone, -} : UserData) => { - return client.post('/users', { - name, username, email, phone, - }); +export const getUser = (email: string) => { + return client.get(`/users?email=${email}`); }; diff --git a/src/components/LoginPage/LoginPage.tsx b/src/components/LoginPage/LoginPage.tsx index c30689f428..7e28f73001 100644 --- a/src/components/LoginPage/LoginPage.tsx +++ b/src/components/LoginPage/LoginPage.tsx @@ -3,73 +3,54 @@ import { useState } from 'react'; import { useTodos } from '../../TodoContext'; import { Error } from '../Error/Error'; import * as todoService from '../../api/todos'; -import { User } from '../../types/User'; export const LoginPage = () => { const { isLoading, - setUserId, + setUser, setErrorMessage, setIsLoading, + // user, } = useTodos(); const [userEmail, setUserEmail] = useState(''); - const [allUsers, setAllUsers] = useState([]); - const name = 'user'; - const username = 'username'; - const phone = '0123456789'; + const [userName, setUserName] = useState(''); + const [userNotRegistred, setUserNotRegistred] = useState(false); const handleChange = (event: React.ChangeEvent) => { setUserEmail(event.target.value); }; - const getUserId = () => { - const isUserExist = allUsers.find(user => user.email === userEmail); - - if (isUserExist) { - return isUserExist.id; - } - - return 0; + const handleUserName = (event: React.ChangeEvent) => { + setUserName(event.target.value); }; - function createUser() { - todoService.createUser({ - name, username, email: userEmail, phone, - }) - .then((user) => { - const newUsers = [...allUsers, user]; + // console.log(user.id); - setAllUsers(newUsers); - setUserId(user.id); - }) - .catch(() => setErrorMessage('Unable to load todos')) - .finally(() => setIsLoading(false)); - } + const handleSubmit = () => { - function getAllUsers() { - todoService.getAllUsers() - .then((existingUsers) => { - setAllUsers(existingUsers); - }) - .catch(() => setErrorMessage('Unable to load todos')); - } + }; - const handleSubmit = () => { + const getUser = (event: React.ChangeEvent) => { + event.preventDefault(); setIsLoading(true); - getAllUsers(); - const newId = getUserId(); - - if (!newId) { - createUser(); - } - setUserId(getUserId()); + return todoService.getUser(userEmail) + .then((response) => setUser(response)) + .catch(() => { + setUserNotRegistred(true); + }) + .finally(() => setIsLoading(false)); }; return ( <> -
-

Log in to open todos

+ +

+ {userNotRegistred ? 'You need to register' : 'Log in to open todos'} +

@@ -81,23 +62,59 @@ export const LoginPage = () => { value={userEmail} placeholder="Enter your email" required + disabled={userNotRegistred || isLoading} />
-
- -
+ {!userNotRegistred ? ( +
+ +
+ ) : ( + <> +
+ +
+ + + + +
+
+
+ +
+ + )} diff --git a/src/hooks/UseLocalStorege.ts b/src/hooks/UseLocalStorege.ts new file mode 100644 index 0000000000..4c0b639178 --- /dev/null +++ b/src/hooks/UseLocalStorege.ts @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +export +function useLocalStorage(key: string, startValue: T): [T, (v: T) => void] { + const [value, setValue] = useState(() => { + const data = localStorage.getItem(key); + + if (data === null) { + return startValue; + } + + try { + return JSON.parse(data); + } catch (e) { + localStorage.removeItem(key); + + return startValue; + } + }); + + const save = (newValue: T) => { + localStorage.setItem(key, JSON.stringify(newValue)); + setValue(newValue); + }; + + return [value, save]; +} diff --git a/src/types/User.ts b/src/types/User.ts index 7ad005144f..bc509b726b 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -5,3 +5,23 @@ export interface User { email: string, phone: string, } +// export interface User { +// createdAt: Date, +// email: string, +// id: number, +// name: string, +// phone: string | null, +// updatedAt: Date, +// username: string | null, +// website: string | null, +// } +// export interface User { +// createdAt: string, +// email: string, +// id: number, +// name: string, +// phone: null, +// updatedAt: string, +// username: null, +// website: null, +// } From acb7b497408ebf8036f8225c4826442c369a68d8 Mon Sep 17 00:00:00 2001 From: Vitalii Fedusov Date: Sun, 8 Oct 2023 12:11:29 +0300 Subject: [PATCH 9/9] finish registration --- src/TodoApp.tsx | 28 ++++++++++---------- src/TodoContext.tsx | 24 +++-------------- src/api/todos.ts | 11 ++++++-- src/components/LoginPage/LoginPage.tsx | 36 +++++++++++++++++--------- src/components/Section/Section.tsx | 1 + src/types/User.ts | 20 -------------- src/types/UserData.ts | 6 ----- 7 files changed, 52 insertions(+), 74 deletions(-) delete mode 100644 src/types/UserData.ts diff --git a/src/TodoApp.tsx b/src/TodoApp.tsx index 199b5560c2..31bf6b55b1 100644 --- a/src/TodoApp.tsx +++ b/src/TodoApp.tsx @@ -1,7 +1,6 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ // #region import import React, { useEffect, useState } from 'react'; -// import { UserWarning } from './UserWarning'; import { Footer, Status } from './components/Footer/Footer'; import { Header } from './components/Header/Header'; import { Section } from './components/Section/Section'; @@ -61,13 +60,15 @@ export const TodoApp: React.FC = () => { useEffect(clearError, [errorMessage]); function loadTodos() { - todoService.getTodos(user.id) - .then(response => { - setTodos(response); - }) - .catch(() => { - setErrorMessage('Unable to load todos'); - }); + if (user) { + todoService.getTodos(user.id) + .then(response => { + setTodos(response); + }) + .catch(() => { + setErrorMessage('Unable to load todos'); + }); + } } useEffect(loadTodos, [user]); @@ -95,9 +96,11 @@ export const TodoApp: React.FC = () => { setIsFocused(true); }); - setTempTodo({ - id: 0, userId: user.id, title, completed, - }); + if (user) { + setTempTodo({ + id: 0, userId: user.id, title, completed, + }); + } return promise; }; @@ -294,8 +297,7 @@ export const TodoApp: React.FC = () => { }; // #endregion - if (!user.id) { - // return ; + if (!user) { return ; } diff --git a/src/TodoContext.tsx b/src/TodoContext.tsx index b4edaa9232..4730b52d5f 100644 --- a/src/TodoContext.tsx +++ b/src/TodoContext.tsx @@ -18,28 +18,10 @@ interface ITodosContext { setUpdatedTitle: (newTitle: string) => void, isLoading: boolean, setIsLoading: (status: boolean) => void, - user: User, + user: User | null, setUser: (user: User) => void, } -const defaultUser: User = { - id: 0, - name: 'dafaultName', - username: 'defaultUserName', - email: 'defaultEmail', - phone: 'dafaultPhone', -}; -// const defaultUser: User = { -// createdAt: '', -// email: '', -// id: 0, -// name: '', -// phone: null, -// updatedAt: '', -// username: null, -// website: null, -// }; - export const TodosContext = React.createContext({ todos: [], setTodos: () => {}, @@ -55,7 +37,7 @@ export const TodosContext = React.createContext({ setUpdatedTitle: () => {}, isLoading: false, setIsLoading: () => {}, - user: defaultUser, + user: null, setUser: () => {}, }); @@ -73,7 +55,7 @@ export const TodosProvider: React.FC = ({ children }) => { const [isEditing, setIsEditing] = useState(false); const [updatedTitle, setUpdatedTitle] = useState(''); const [isLoading, setIsLoading] = useState(false); - const [user, setUser] = useLocalStorage('user', defaultUser); + const [user, setUser] = useLocalStorage('user', null); const value = useMemo(() => ({ todos, diff --git a/src/api/todos.ts b/src/api/todos.ts index f0f656fd86..196290fde7 100644 --- a/src/api/todos.ts +++ b/src/api/todos.ts @@ -1,6 +1,5 @@ import { Todo } from '../types/Todo'; import { User } from '../types/User'; -// import { UserData } from '../types/UserData'; import { client } from '../utils/fetchClient'; export const getTodos = (userId: number) => { @@ -22,5 +21,13 @@ export const updateTodo = ({ }; export const getUser = (email: string) => { - return client.get(`/users?email=${email}`); + return client.get(`/users?email=${email}`); +}; + +export const createUser = ({ + name, username, email, phone, +}: Omit) => { + return client.post('/users', { + name, username, email, phone, + }); }; diff --git a/src/components/LoginPage/LoginPage.tsx b/src/components/LoginPage/LoginPage.tsx index 7e28f73001..ab83f7434d 100644 --- a/src/components/LoginPage/LoginPage.tsx +++ b/src/components/LoginPage/LoginPage.tsx @@ -10,7 +10,6 @@ export const LoginPage = () => { setUser, setErrorMessage, setIsLoading, - // user, } = useTodos(); const [userEmail, setUserEmail] = useState(''); const [userName, setUserName] = useState(''); @@ -24,20 +23,35 @@ export const LoginPage = () => { setUserName(event.target.value); }; - // console.log(user.id); - - const handleSubmit = () => { - - }; - - const getUser = (event: React.ChangeEvent) => { + const handleSubmit = (event: React.ChangeEvent) => { event.preventDefault(); setIsLoading(true); + if (userNotRegistred) { + return todoService.createUser({ + name: userName, + username: '', + email: userEmail, + phone: '', + }) + .then((response) => { + setUser(response); + setUserNotRegistred(false); + }) + .catch(() => setErrorMessage('Unable to load todos')) + .finally(() => setIsLoading(false)); + } + return todoService.getUser(userEmail) - .then((response) => setUser(response)) + .then((response) => { + if (response[0]) { + setUser(response[0]); + } else { + setUserNotRegistred(true); + } + }) .catch(() => { - setUserNotRegistred(true); + setErrorMessage('Unable to load todo'); }) .finally(() => setIsLoading(false)); }; @@ -76,7 +90,6 @@ export const LoginPage = () => { className={classNames('button is-primary', { 'is-loading': isLoading, })} - onClick={handleSubmit} > Login @@ -108,7 +121,6 @@ export const LoginPage = () => { className={classNames('button is-primary', { 'is-loading': isLoading, })} - onClick={handleSubmit} > Register diff --git a/src/components/Section/Section.tsx b/src/components/Section/Section.tsx index 6a7bac9395..f9699d53bc 100644 --- a/src/components/Section/Section.tsx +++ b/src/components/Section/Section.tsx @@ -180,6 +180,7 @@ export const Section: React.FC = ({ type="checkbox" className="todo__status" checked={tempTodo.completed} + readOnly /> diff --git a/src/types/User.ts b/src/types/User.ts index bc509b726b..7ad005144f 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -5,23 +5,3 @@ export interface User { email: string, phone: string, } -// export interface User { -// createdAt: Date, -// email: string, -// id: number, -// name: string, -// phone: string | null, -// updatedAt: Date, -// username: string | null, -// website: string | null, -// } -// export interface User { -// createdAt: string, -// email: string, -// id: number, -// name: string, -// phone: null, -// updatedAt: string, -// username: null, -// website: null, -// } diff --git a/src/types/UserData.ts b/src/types/UserData.ts deleted file mode 100644 index 5f708b99a6..0000000000 --- a/src/types/UserData.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface UserData { - name: string, - username: string, - email: string, - phone: string, -}