From b218dd90a6a5816205345f35a4bf671b629a1e9e Mon Sep 17 00:00:00 2001 From: Andrey Mekshun Date: Thu, 19 Dec 2024 13:13:43 +0200 Subject: [PATCH 1/2] add task solution --- README.md | 4 +- src/App.tsx | 205 ++++++++++++++++-- src/api/todos.ts | 20 ++ .../ErrorNotification/ErrorNotification.tsx | 41 ++++ src/components/Footer/Footer.tsx | 59 +++++ src/components/Header/Header.tsx | 94 ++++++++ src/components/TempTodo/TempTodo.tsx | 33 +++ src/components/TodoItem/TodoItem.tsx | 128 +++++++++++ src/components/TodoList/TodoList.tsx | 39 ++++ src/types/Filter.ts | 5 + src/types/Todo.ts | 6 + src/utils/fetchClient.ts | 46 ++++ src/utils/getFilteredTodos.ts | 13 ++ 13 files changed, 678 insertions(+), 15 deletions(-) create mode 100644 src/api/todos.ts create mode 100644 src/components/ErrorNotification/ErrorNotification.tsx create mode 100644 src/components/Footer/Footer.tsx create mode 100644 src/components/Header/Header.tsx create mode 100644 src/components/TempTodo/TempTodo.tsx create mode 100644 src/components/TodoItem/TodoItem.tsx create mode 100644 src/components/TodoList/TodoList.tsx create mode 100644 src/types/Filter.ts create mode 100644 src/types/Todo.ts create mode 100644 src/utils/fetchClient.ts create mode 100644 src/utils/getFilteredTodos.ts diff --git a/README.md b/README.md index d3c3756ab9..46ba20f8a1 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ and implement the ability to toggle and rename todos. ## Toggling a todo status Toggle the `completed` status on `TodoStatus` change: + - Install Prettier Extention and use this [VSCode settings](https://mate-academy.github.io/fe-program/tools/vscode/settings.json) to enable format on save. - covered the todo with a loader overlay while waiting for API response; - the status should be changed on success; @@ -38,6 +39,7 @@ Implement the ability to edit a todo title on double click: - or the deletion error message if we tried to delete the todo. ## If you want to enable tests + - open `cypress/integration/page.spec.js` - replace `describe.skip` with `describe` for the root `describe` @@ -47,4 +49,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://Welbrn.github.io/react_todo-app-with-api/) and add it to the PR description. diff --git a/src/App.tsx b/src/App.tsx index 81e011f432..123151955f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,203 @@ /* eslint-disable max-len */ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; +/* eslint-disable jsx-a11y/label-has-associated-control */ +import React, { useEffect, useState } from 'react'; import { UserWarning } from './UserWarning'; - -const USER_ID = 0; +import { + addTodos, + deleteTodos, + getTodos, + updateTodos, + USER_ID, +} from './api/todos'; +import { Todo } from './types/Todo'; +import { Filter } from './types/Filter'; +import { Header } from './components/Header/Header'; +import { TodoList } from './components/TodoList/TodoList'; +import { Footer } from './components/Footer/Footer'; +import { ErrorNotification } from './components/ErrorNotification/ErrorNotification'; +import { filterTodos } from './utils/getFilteredTodos'; export const App: React.FC = () => { + const [todos, setTodos] = useState([]); + const [errorMessage, setErrorMessage] = useState(''); + const [filterStatus, setFilterStatus] = useState(Filter.All); + const [tempTodo, setTempTodo] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [editTodos, setEditTodos] = useState([]); + + const filteredTodos = filterTodos(todos, filterStatus); + + const handleCreateTodo = async (newTodo: Omit) => { + setErrorMessage(''); + setTempTodo({ id: 0, ...newTodo }); + setIsLoading(true); + try { + const createdTodo = await addTodos(newTodo); + + setTodos(prev => [...prev, createdTodo]); + } catch (error) { + setErrorMessage('Unable to add a todo'); + + throw error; + } finally { + setTempTodo(null); + setIsLoading(false); + } + }; + + const handleDeleteTodo = async (id: number) => { + setErrorMessage(''); + setIsLoading(true); + setEditTodos(current => [...current, id]); + try { + await deleteTodos(id); + setTodos(prev => prev.filter(todo => todo.id !== id)); + } catch { + setEditTodos([]); + setErrorMessage('Unable to delete a todo'); + } finally { + setIsLoading(false); + } + }; + + const handleClearCompleted = async () => { + const completedTodos = todos.filter(todo => todo.completed); + + await Promise.all(completedTodos.map(todo => handleDeleteTodo(todo.id))); + }; + + const handleToggleStatus = async (id: number, completed: boolean) => { + setIsLoading(true); + setEditTodos(current => [...current, id]); + try { + const todoToUpdate = todos.find(todo => todo.id === id); + + if (!todoToUpdate) { + setErrorMessage('Todo not found'); + + return; + } + + const updatedTodo = await updateTodos(id, { + completed, + userId: todoToUpdate.userId, + title: todoToUpdate.title, + }); + + setTodos(prev => + prev.map(todo => (todo.id === updatedTodo.id ? updatedTodo : todo)), + ); + } catch { + setErrorMessage('Unable to update a todo'); + } finally { + setEditTodos(prev => prev.filter(todoId => todoId !== id)); + setIsLoading(false); + } + }; + + const handleToggleAll = async () => { + const allCompleted = todos.every(todo => todo.completed); + const newStatus = !allCompleted; + + setIsLoading(true); + const promises = todos + .filter(todo => todo.completed !== newStatus) + .map(todo => handleToggleStatus(todo.id, newStatus)); + + try { + await Promise.all(promises); + setTodos(prev => + prev.map(todo => + todo.completed !== newStatus + ? { ...todo, completed: newStatus } + : todo, + ), + ); + } catch { + setErrorMessage('Unable to update todos'); + } finally { + setIsLoading(false); + } + }; + + const handleUpdateTitle = async (id: number, newTitle: string) => { + setErrorMessage(''); + setIsLoading(true); + setEditTodos(current => [...current, id]); + + try { + const todoToUpdate = todos.find(todo => todo.id === id); + + if (!todoToUpdate) { + return; + } + + const updatedTodo = await updateTodos(id, { + ...todoToUpdate, + title: newTitle, + }); + + setTodos(prev => + prev.map(todo => (todo.id === updatedTodo.id ? updatedTodo : todo)), + ); + } catch { + setErrorMessage('Unable to update a todo'); + throw new Error('Unable to update a todo'); + } finally { + setIsLoading(false); + setEditTodos(prev => prev.filter(todoId => todoId !== id)); + } + }; + + useEffect(() => { + getTodos() + .then(setTodos) + .catch(() => setErrorMessage('Unable to load todos')); + }, []); + if (!USER_ID) { return ; } return ( -
-

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

- -

Styles are already copied

-
+
+

todos

+ +
+
+ + {todos.length > 0 && ( + + )} + + {!!todos.length && ( +
+ )} +
+ + +
); }; diff --git a/src/api/todos.ts b/src/api/todos.ts new file mode 100644 index 0000000000..1335e636d8 --- /dev/null +++ b/src/api/todos.ts @@ -0,0 +1,20 @@ +import { Todo } from '../types/Todo'; +import { client } from '../utils/fetchClient'; + +export const USER_ID = 2125; + +export const getTodos = () => { + return client.get(`/todos?userId=${USER_ID}`); +}; + +export const addTodos = (newTodo: Omit) => { + return client.post(`/todos`, newTodo); +}; + +export const deleteTodos = (id: number) => { + return client.delete(`/todos/${id}`); +}; + +export const updateTodos = (id: number, newTodo: Omit) => { + return client.patch(`/todos/${id}`, newTodo); +}; diff --git a/src/components/ErrorNotification/ErrorNotification.tsx b/src/components/ErrorNotification/ErrorNotification.tsx new file mode 100644 index 0000000000..6d6878f57e --- /dev/null +++ b/src/components/ErrorNotification/ErrorNotification.tsx @@ -0,0 +1,41 @@ +import React, { useEffect } from 'react'; +import cn from 'classnames'; + +type Props = { + errorMessage: string; + setErrorMessage: (message: string) => void; +}; + +export const ErrorNotification: React.FC = ({ + errorMessage, + setErrorMessage, +}) => { + useEffect(() => { + if (errorMessage) { + const timer = setTimeout(() => { + setErrorMessage(''); + }, 3000); + + return () => clearTimeout(timer); + } + + return undefined; + }, [errorMessage, setErrorMessage]); + + return ( +
+
+ ); +}; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 0000000000..b3de9715a2 --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Todo } from '../../types/Todo'; +import { Filter } from '../../types/Filter'; +import cn from 'classnames'; + +type Props = { + todos: Todo[]; + filterStatus: Filter; + setFilterStatus: (filter: Filter) => void; + handleClearCompleted: () => void; +}; + +const filterLinks = [ + { label: 'All', value: Filter.All }, + { label: 'Active', value: Filter.Active }, + { label: 'Completed', value: Filter.Completed }, +]; + +export const Footer: React.FC = ({ + setFilterStatus, + todos, + filterStatus, + handleClearCompleted, +}) => { + const todosLeft = todos.filter(todo => !todo.completed); + const isClearCompletedDisabled = todos.every(todo => !todo.completed); + + return ( + + ); +}; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 0000000000..9116f3b1e3 --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,94 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Todo } from '../../types/Todo'; +import cn from 'classnames'; +import { USER_ID } from '../../api/todos'; + +type Props = { + todos: Todo[]; + setErrorMessage: (message: string) => void; + handleCreateTodo: (newTodo: Omit) => Promise; + isLoading: boolean; + onToggleAll: () => void; +}; + +export const Header: React.FC = ({ + todos, + setErrorMessage, + handleCreateTodo, + isLoading, + onToggleAll, +}) => { + const [title, setTitle] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + const focusInput = useRef(null); + + useEffect(() => { + if (focusInput.current && !isLoading) { + focusInput.current.focus(); + } + }, [isLoading]); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + const trimmedTitle = title.trim(); + + if (!trimmedTitle) { + setErrorMessage('Title should not be empty'); + + return; + } + + setIsSubmitting(true); + + try { + await handleCreateTodo({ + userId: USER_ID, + title: trimmedTitle, + completed: false, + }); + + setTitle(''); + } catch (error) { + throw error; + } finally { + setIsSubmitting(false); + if (focusInput.current) { + focusInput.current.focus(); + } + } + }; + + const handleTitleChange = (event: React.ChangeEvent) => { + setTitle(event.target.value); + }; + + return ( +
+ {todos.length > 0 && ( +
+ ); +}; diff --git a/src/components/TempTodo/TempTodo.tsx b/src/components/TempTodo/TempTodo.tsx new file mode 100644 index 0000000000..a279764805 --- /dev/null +++ b/src/components/TempTodo/TempTodo.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Todo } from '../../types/Todo'; +import cn from 'classnames'; + +type Props = { + todo: Todo; +}; + +export const TempTodo: React.FC = ({ todo }) => { + return ( +
+ + + + {todo.title} + + +
+
+
+
+
+ ); +}; diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx new file mode 100644 index 0000000000..4a19642951 --- /dev/null +++ b/src/components/TodoItem/TodoItem.tsx @@ -0,0 +1,128 @@ +import React, { useState } from 'react'; +import { Todo } from '../../types/Todo'; +import cn from 'classnames'; + +type Props = { + todo: Todo; + onDelete: (id: number) => void; + editTodos: number[]; + onToggle: (id: number, completed: boolean) => void; + onUpdate: (id: number, newTitle: string) => Promise; +}; + +export const TodoItem: React.FC = ({ + todo, + onDelete, + editTodos, + onToggle, + onUpdate, +}) => { + const [isEditing, setIsEditing] = useState(false); + const [newTitle, setNewTitle] = useState(todo.title); + + const isLoading = editTodos.includes(todo.id); + + const handleDelete = () => { + onDelete(todo.id); + }; + + const handleStatusChange = async ( + event: React.ChangeEvent, + ) => { + const completed = event.target.checked; + + await onToggle(todo.id, completed); + }; + + const handleDoubleClick = () => { + setIsEditing(true); + }; + + const handleInputChange = (event: React.ChangeEvent) => { + setNewTitle(event.target.value); + }; + + const handleBlur = async () => { + if (newTitle.trim() === todo.title) { + setIsEditing(false); + + return; + } + + if (!newTitle.trim()) { + handleDelete(); + + return; + } + + try { + await onUpdate(todo.id, newTitle.trim()); + setIsEditing(false); + } catch (error) {} + }; + + const handleKeyUp = async (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + handleBlur(); + } else if (event.key === 'Escape') { + setNewTitle(todo.title); + setIsEditing(false); + } + }; + + return ( +
+ + {!isEditing ? ( + + {todo.title} + + ) : ( + + )} + {!isEditing && ( + + )} + +
+
+
+
+
+ ); +}; diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx new file mode 100644 index 0000000000..92c0688189 --- /dev/null +++ b/src/components/TodoList/TodoList.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { TodoItem } from '../TodoItem/TodoItem'; +import { Todo } from '../../types/Todo'; +import { TempTodo } from '../TempTodo/TempTodo'; + +type Props = { + todos: Todo[]; + onDelete: (id: number) => void; + tempTodo: Todo | null; + editTodos: number[]; + onToggle: (id: number, completed: boolean) => void; + onUpdate: (id: number, newTitle: string) => Promise; +}; + +export const TodoList: React.FC = ({ + todos, + onDelete, + tempTodo, + editTodos, + onToggle, + onUpdate, +}) => { + return ( +
+ {todos.map(todo => ( + + ))} + + {tempTodo && } +
+ ); +}; diff --git a/src/types/Filter.ts b/src/types/Filter.ts new file mode 100644 index 0000000000..66887875b7 --- /dev/null +++ b/src/types/Filter.ts @@ -0,0 +1,5 @@ +export enum Filter { + All = 'All', + Active = 'Active', + Completed = 'Completed', +} 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..708ac4c17b --- /dev/null +++ b/src/utils/fetchClient.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +const BASE_URL = 'https://mate.academy/students-api'; + +// returns a promise resolved after a given delay +function wait(delay: number) { + return new Promise(resolve => { + setTimeout(resolve, delay); + }); +} + +// To have autocompletion and avoid mistypes +type RequestMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE'; + +function request( + url: string, + method: RequestMethod = 'GET', + data: any = null, // we can send any data to the server +): Promise { + const options: RequestInit = { method }; + + if (data) { + // We add body and Content-Type only for the requests with data + options.body = JSON.stringify(data); + options.headers = { + 'Content-Type': 'application/json; charset=UTF-8', + }; + } + + // DON'T change the delay it is required for tests + return wait(100) + .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'), +}; diff --git a/src/utils/getFilteredTodos.ts b/src/utils/getFilteredTodos.ts new file mode 100644 index 0000000000..aa5052a7fd --- /dev/null +++ b/src/utils/getFilteredTodos.ts @@ -0,0 +1,13 @@ +import { Filter } from '../types/Filter'; +import { Todo } from '../types/Todo'; + +export const filterTodos = (todos: Todo[], filterStatus: Filter) => { + return todos.filter(todo => { + const matchesStatus = + filterStatus === Filter.All || + (filterStatus === Filter.Active && !todo.completed) || + (filterStatus === Filter.Completed && todo.completed); + + return matchesStatus; + }); +}; From ccaed118ba02843cee86025cab580010f9485cd8 Mon Sep 17 00:00:00 2001 From: Andrey Mekshun Date: Fri, 20 Dec 2024 14:37:47 +0200 Subject: [PATCH 2/2] changed a few things --- src/App.tsx | 28 ++++++++----------- .../ErrorNotification/ErrorNotification.tsx | 11 ++++---- src/components/Header/Header.tsx | 16 ++++++++--- src/components/TodoItem/TodoItem.tsx | 17 +++++++++-- src/types/ErrorMessage.ts | 7 +++++ 5 files changed, 50 insertions(+), 29 deletions(-) create mode 100644 src/types/ErrorMessage.ts diff --git a/src/App.tsx b/src/App.tsx index 123151955f..32147d4286 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,10 +17,11 @@ import { TodoList } from './components/TodoList/TodoList'; import { Footer } from './components/Footer/Footer'; import { ErrorNotification } from './components/ErrorNotification/ErrorNotification'; import { filterTodos } from './utils/getFilteredTodos'; +import { ErrorMessage } from './types/ErrorMessage'; export const App: React.FC = () => { const [todos, setTodos] = useState([]); - const [errorMessage, setErrorMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(null); const [filterStatus, setFilterStatus] = useState(Filter.All); const [tempTodo, setTempTodo] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -29,7 +30,7 @@ export const App: React.FC = () => { const filteredTodos = filterTodos(todos, filterStatus); const handleCreateTodo = async (newTodo: Omit) => { - setErrorMessage(''); + setErrorMessage(null); setTempTodo({ id: 0, ...newTodo }); setIsLoading(true); try { @@ -37,8 +38,7 @@ export const App: React.FC = () => { setTodos(prev => [...prev, createdTodo]); } catch (error) { - setErrorMessage('Unable to add a todo'); - + setErrorMessage(ErrorMessage.UnableToAdd); throw error; } finally { setTempTodo(null); @@ -47,7 +47,6 @@ export const App: React.FC = () => { }; const handleDeleteTodo = async (id: number) => { - setErrorMessage(''); setIsLoading(true); setEditTodos(current => [...current, id]); try { @@ -55,7 +54,7 @@ export const App: React.FC = () => { setTodos(prev => prev.filter(todo => todo.id !== id)); } catch { setEditTodos([]); - setErrorMessage('Unable to delete a todo'); + setErrorMessage(ErrorMessage.UnableToDelete); } finally { setIsLoading(false); } @@ -74,8 +73,6 @@ export const App: React.FC = () => { const todoToUpdate = todos.find(todo => todo.id === id); if (!todoToUpdate) { - setErrorMessage('Todo not found'); - return; } @@ -89,7 +86,7 @@ export const App: React.FC = () => { prev.map(todo => (todo.id === updatedTodo.id ? updatedTodo : todo)), ); } catch { - setErrorMessage('Unable to update a todo'); + setErrorMessage(ErrorMessage.UnableToUpdate); } finally { setEditTodos(prev => prev.filter(todoId => todoId !== id)); setIsLoading(false); @@ -115,15 +112,13 @@ export const App: React.FC = () => { ), ); } catch { - setErrorMessage('Unable to update todos'); + setErrorMessage(ErrorMessage.UnableToUpdate); } finally { setIsLoading(false); } }; const handleUpdateTitle = async (id: number, newTitle: string) => { - setErrorMessage(''); - setIsLoading(true); setEditTodos(current => [...current, id]); try { @@ -141,11 +136,10 @@ export const App: React.FC = () => { setTodos(prev => prev.map(todo => (todo.id === updatedTodo.id ? updatedTodo : todo)), ); - } catch { - setErrorMessage('Unable to update a todo'); - throw new Error('Unable to update a todo'); + } catch (error) { + setErrorMessage(ErrorMessage.UnableToUpdate); + throw error; } finally { - setIsLoading(false); setEditTodos(prev => prev.filter(todoId => todoId !== id)); } }; @@ -153,7 +147,7 @@ export const App: React.FC = () => { useEffect(() => { getTodos() .then(setTodos) - .catch(() => setErrorMessage('Unable to load todos')); + .catch(() => setErrorMessage(ErrorMessage.UnableToLoad)); }, []); if (!USER_ID) { diff --git a/src/components/ErrorNotification/ErrorNotification.tsx b/src/components/ErrorNotification/ErrorNotification.tsx index 6d6878f57e..ce69f2ff70 100644 --- a/src/components/ErrorNotification/ErrorNotification.tsx +++ b/src/components/ErrorNotification/ErrorNotification.tsx @@ -1,9 +1,10 @@ -import React, { useEffect } from 'react'; +import React, { Dispatch, SetStateAction, useEffect } from 'react'; import cn from 'classnames'; +import { ErrorMessage } from '../../types/ErrorMessage'; type Props = { - errorMessage: string; - setErrorMessage: (message: string) => void; + errorMessage: ErrorMessage | null; + setErrorMessage: Dispatch>; }; export const ErrorNotification: React.FC = ({ @@ -13,7 +14,7 @@ export const ErrorNotification: React.FC = ({ useEffect(() => { if (errorMessage) { const timer = setTimeout(() => { - setErrorMessage(''); + setErrorMessage(null); }, 3000); return () => clearTimeout(timer); @@ -33,7 +34,7 @@ export const ErrorNotification: React.FC = ({ data-cy="HideErrorButton" type="button" className="delete" - onClick={() => setErrorMessage('')} + onClick={() => setErrorMessage(null)} /> {errorMessage}
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 9116f3b1e3..9d6ac1889c 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,11 +1,18 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { + Dispatch, + SetStateAction, + useEffect, + useRef, + useState, +} from 'react'; import { Todo } from '../../types/Todo'; import cn from 'classnames'; import { USER_ID } from '../../api/todos'; +import { ErrorMessage } from '../../types/ErrorMessage'; type Props = { todos: Todo[]; - setErrorMessage: (message: string) => void; + setErrorMessage: Dispatch>; handleCreateTodo: (newTodo: Omit) => Promise; isLoading: boolean; onToggleAll: () => void; @@ -21,6 +28,7 @@ export const Header: React.FC = ({ const [title, setTitle] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); + const activeButton = todos.length > 0 && todos.every(todo => todo.completed); const focusInput = useRef(null); useEffect(() => { @@ -35,7 +43,7 @@ export const Header: React.FC = ({ const trimmedTitle = title.trim(); if (!trimmedTitle) { - setErrorMessage('Title should not be empty'); + setErrorMessage(ErrorMessage.TitleNotEmpty); return; } @@ -70,7 +78,7 @@ export const Header: React.FC = ({