Skip to content

Commit

Permalink
add task solution
Browse files Browse the repository at this point in the history
  • Loading branch information
Wesses committed Sep 20, 2023
1 parent 43f1a3c commit 54790b5
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 50 deletions.
7 changes: 6 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getTodos } from './api/todos';

import { Todo } from './types/Todo';
import { Status } from './types/Status';
import { GlobalLoader } from './types/GlobalLoader';

import { TodoList } from './components/TodoList';
import { TodoHeader } from './components/TodoHeader';
Expand All @@ -23,7 +24,10 @@ export const App: React.FC = () => {
const { setError } = useContext(ErrorContext);
const [status, setStatus] = useState(Status.All);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [globalLoader, setGlobalLoader] = useState(false);
const [
globalLoader,
setGlobalLoader,
] = useState<GlobalLoader>(GlobalLoader.Non);

const filtredTodos = useMemo(() => todos.filter(({ completed }) => {
switch (status) {
Expand Down Expand Up @@ -54,6 +58,7 @@ export const App: React.FC = () => {
<TodoHeader
onTempTodoAdd={setTempTodo}
tempTodo={tempTodo}
onGlobalLoaderChange={setGlobalLoader}
/>

{!!todos.length && (
Expand Down
6 changes: 5 additions & 1 deletion src/api/todos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@ export const addTodo = (userId: number, todo: Todo) => {
};

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

export const updateTodo = (id: number, todo: Partial<Todo>) => {
return client.patch<Todo>(`/todos/${id}`, todo);
};
7 changes: 4 additions & 3 deletions src/components/TodoFooter/TodoFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { Status } from '../../types/Status';
import { TodoContext } from '../TodoContext';
import { deleteTodo } from '../../api/todos';
import { ErrorContext } from '../ErrorContext';
import { GlobalLoader } from '../../types/GlobalLoader';

type Props = {
status: Status;
onStatusChange: (status: Status) => void;
onGlobalLoaderChange: (globalLoader: boolean) => void;
onGlobalLoaderChange: (globalLoader: GlobalLoader) => void;
};

export const TodoFooter: React.FC<Props> = (props) => {
Expand All @@ -25,7 +26,7 @@ export const TodoFooter: React.FC<Props> = (props) => {
.filter(({ completed }) => !completed), [todos]);

const handleComplDelete = () => {
onGlobalLoaderChange(true);
onGlobalLoaderChange(GlobalLoader.Completed);

const deletedTodos: Promise<number>[] = [];

Expand All @@ -45,7 +46,7 @@ export const TodoFooter: React.FC<Props> = (props) => {
.filter(todo => !res.includes(todo.id)));
})
.catch(() => setError('Unable to delete a todo'))
.finally(() => onGlobalLoaderChange(false));
.finally(() => onGlobalLoaderChange(GlobalLoader.Non));
};

return (
Expand Down
72 changes: 61 additions & 11 deletions src/components/TodoHeader/TodoHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import { useContext, useEffect, useState } from 'react';
import {
useContext,
useEffect,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import { Todo } from '../../types/Todo';
import { USER_ID } from '../../utils/userId';
import { addTodo } from '../../api/todos';
import { addTodo, updateTodo } from '../../api/todos';
import { TodoContext } from '../TodoContext';
import { ErrorContext } from '../ErrorContext';
import { GlobalLoader } from '../../types/GlobalLoader';

type Props = {
onTempTodoAdd: (todo: Todo | null) => void;
tempTodo: Todo | null;
onGlobalLoaderChange: (globalLoader: GlobalLoader) => void;
};

export const TodoHeader: React.FC<Props> = (props) => {
const {
onTempTodoAdd,
tempTodo,
onGlobalLoaderChange,
} = props;

const { setError } = useContext(ErrorContext);
const { setTodos } = useContext(TodoContext);
const { todos, setTodos } = useContext(TodoContext);
const [title, setTitle] = useState('');
const inputRef = useRef<HTMLInputElement | null>(null);

const isAllCompleted = todos.every(todo => todo.completed);

const handleSubmitForm = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
Expand All @@ -37,6 +49,35 @@ export const TodoHeader: React.FC<Props> = (props) => {
});
};

const handleToggleAll = () => {
const updatedTodos: Promise<Todo>[] = [];

if (isAllCompleted) {
onGlobalLoaderChange(GlobalLoader.Completed);
todos.forEach(todo => {
updatedTodos.push(updateTodo(todo.id, { completed: false })
.then(updatedTodo => updatedTodo)
.catch((error) => {
throw error;
}));
});
} else {
onGlobalLoaderChange(GlobalLoader.Active);
todos.forEach(todo => {
updatedTodos.push(updateTodo(todo.id, { completed: true })
.then(updatedTodo => updatedTodo)
.catch((error) => {
throw error;
}));
});
}

Promise.all(updatedTodos)
.then(setTodos)
.catch(() => setError('Unable to update a todo'))
.finally(() => onGlobalLoaderChange(GlobalLoader.Non));
};

useEffect(() => {
if (tempTodo) {
addTodo(USER_ID, tempTodo)
Expand All @@ -45,23 +86,32 @@ export const TodoHeader: React.FC<Props> = (props) => {
setTitle('');
})
.catch(() => setError('Unable to add a todo'))
.finally(() => onTempTodoAdd(null));
.finally(() => {
onTempTodoAdd(null);
setTimeout(() => {
inputRef.current?.focus();
}, 0);
});
}
}, [tempTodo]);

return (
<header className="todoapp__header">
{/* this buttons is active only if there are some active todos */}
<button
type="button"
className="todoapp__toggle-all active"
aria-label="button_toggle_active"
/>

{!!todos.length && (
<button
type="button"
className={classNames('todoapp__toggle-all', {
active: isAllCompleted,
})}
aria-label="button_toggle_all"
onClick={handleToggleAll}
/>
)}
<form
onSubmit={handleSubmitForm}
>
<input
ref={inputRef}
disabled={!!tempTodo}
type="text"
className="todoapp__new-todo"
Expand Down
167 changes: 137 additions & 30 deletions src/components/TodoItem/TodoItem.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { useContext, useState } from 'react';
import {
useContext,
useEffect,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import { Todo } from '../../types/Todo';
import { deleteTodo } from '../../api/todos';
import { deleteTodo, updateTodo } from '../../api/todos';
import { TodoContext } from '../TodoContext';
import { ErrorContext } from '../ErrorContext';
import { GlobalLoader } from '../../types/GlobalLoader';

type Props = {
todo: Todo;
loader: boolean;
loader: GlobalLoader;
};

export const TodoItem: React.FC<Props> = ({ todo, loader }) => {
Expand All @@ -20,8 +26,16 @@ export const TodoItem: React.FC<Props> = ({ todo, loader }) => {
const { setError } = useContext(ErrorContext);
const { setTodos } = useContext(TodoContext);
const [isLoading, setIsLoading] = useState(false);
const [editing, setEditing] = useState('');
const [isEditing, setIsEditing] = useState(false);
const focusInput = useRef<HTMLInputElement | null>(null);

const handleDeleteClick = () => {
const disableEditing = () => {
setEditing('');
setIsEditing(false);
};

const deleteTodoById = () => {
setIsLoading(true);

deleteTodo(id)
Expand All @@ -33,6 +47,91 @@ export const TodoItem: React.FC<Props> = ({ todo, loader }) => {
.finally(() => setIsLoading(false));
};

const updateTodoById = (data: Partial<Todo>) => {
setIsLoading(true);
updateTodo(id, data)
.then((updatedTodo) => setTodos(prevState => {
return prevState
.map(prevTodo => (prevTodo.id !== id ? prevTodo : updatedTodo));
}))
.catch(() => setError('Unable to update a todo'))
.finally(() => setIsLoading(false));
};

const saveChanges = () => {
if (editing === title) {
disableEditing();

return;
}

if (!editing) {
deleteTodoById();
disableEditing();

return;
}

updateTodoById({ title: editing });
disableEditing();
};

const handleDeleteClick = () => {
deleteTodoById();
};

const handleCheckBoxChange = () => {
updateTodoById({ completed: !completed });
};

const handleTitleDoubleClick = () => {
setEditing(title);
setIsEditing(true);
};

const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
saveChanges();
};

const handleInputBlur = () => {
saveChanges();
};

const handleEscUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Escape') {
disableEditing();
}
};

useEffect(() => {
switch (loader) {
case GlobalLoader.All:
setIsLoading(true);
break;
case GlobalLoader.Active:
if (!completed) {
setIsLoading(true);
}

break;
case GlobalLoader.Completed:
if (completed) {
setIsLoading(true);
}

break;
default:
setIsLoading(false);
}
}, [loader]);

useEffect(() => {
if (isEditing && focusInput.current) {
focusInput.current.focus();
}
}, [isEditing]);

return (
<>
<div
Expand All @@ -45,23 +144,46 @@ export const TodoItem: React.FC<Props> = ({ todo, loader }) => {
type="checkbox"
className="todo__status"
checked={completed}
onChange={handleCheckBoxChange}
/>
</label>

<span className="todo__title">
{title}
</span>
{isEditing ? (
<form
onSubmit={handleFormSubmit}
>
<input
ref={focusInput}
type="text"
className="todo__title-field"
placeholder="Empty todo will be deleted"
value={editing}
onChange={(event) => setEditing(event.target.value)}
onBlur={handleInputBlur}
onKeyUp={handleEscUp}
/>
</form>
) : (
<>
<span
className="todo__title"
onDoubleClick={handleTitleDoubleClick}
>
{title}
</span>

<button
type="button"
className="todo__remove"
onClick={handleDeleteClick}
>
×
</button>
<button
type="button"
className="todo__remove"
onClick={handleDeleteClick}
>
×
</button>
</>
)}

<div className={classNames('modal', 'overlay', {
'is-active': isLoading || loader,
'is-active': isLoading,
})}
>
<div className="modal-background has-background-white-ter" />
Expand All @@ -71,18 +193,3 @@ export const TodoItem: React.FC<Props> = ({ todo, loader }) => {
</>
);
};
/* This todo is in loadind state */
/* <div className="todo">
<label className="todo__status-label">
<input type="checkbox" className="todo__status" />
</label>
<span className="todo__title">Todo is being saved now</span>
<button type="button" className="todo__remove">×</button> */

/* 'is-active' class puts this modal on top of the todo */
/* <div className="modal overlay is-active">
<div className="modal-background has-background-white-ter" />
<div className="loader" />
</div> */
/* </div> */
Loading

0 comments on commit 54790b5

Please sign in to comment.