Skip to content

Commit

Permalink
Solution
Browse files Browse the repository at this point in the history
  • Loading branch information
tania-kuzmenko committed Nov 25, 2024
1 parent f1536f7 commit 97246f0
Show file tree
Hide file tree
Showing 14 changed files with 742 additions and 149 deletions.
218 changes: 73 additions & 145 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,157 +1,85 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import React, { useEffect } from 'react';
import { UserWarning } from './UserWarning';
import { getTodos } from './api/todos';
import { Header } from './components/Header';
import { Footer } from './components/Footer';
import { Filter } from './types/Filter';
import { TodoItem } from './components/TodoItem';
import { focusInput } from './utils/services';
import { ErrNotification } from './components/ErrNotification';
import { useTodoContext } from './components/TodoContext';
import { USER_ID } from './utils/constants';

export const App: React.FC = () => {
const {
todos,
setTodos,
isLoading,
setIsLoading,
activeTodoId,
isSubmitting,
tempTodo,
inputRef,
filter,
showError,
} = useTodoContext();

useEffect(() => {
const fetchTodos = async () => {
setIsLoading(true);
try {
const fetchedTodos = await getTodos(USER_ID);

setTodos(fetchedTodos);
} catch {
showError('Unable to load todos');
} finally {
setIsLoading(false);
}
};

fetchTodos();
}, []);

useEffect(() => {
focusInput(inputRef);
}, [isSubmitting, activeTodoId, inputRef]);

const filteredTodos = todos.filter(todo => {
switch (filter) {
case Filter.Active:
return !todo.completed;
case Filter.Completed:
return todo.completed;
default:
return true;
}
});

if (!USER_ID) {
return <UserWarning />;
}

return (
<div className="todoapp">
<h1 className="todoapp__title">todos</h1>

<div className="todoapp__content">
<header className="todoapp__header">
{/* this button should have `active` class only if all todos are completed */}
<button
type="button"
className="todoapp__toggle-all active"
data-cy="ToggleAllButton"
/>

{/* Add a todo on form submit */}
<form>
<input
data-cy="NewTodoField"
type="text"
className="todoapp__new-todo"
placeholder="What needs to be done?"
/>
</form>
</header>

<section className="todoapp__main" data-cy="TodoList">
{/* This is a completed todo */}
<div data-cy="Todo" className="todo completed">
<label className="todo__status-label">
<input
data-cy="TodoStatus"
type="checkbox"
className="todo__status"
checked
/>
</label>

<span data-cy="TodoTitle" className="todo__title">
Completed Todo
</span>

{/* Remove button appears only on hover */}
<button type="button" className="todo__remove" data-cy="TodoDelete">
×
</button>
</div>

{/* This todo is an active todo */}
<div data-cy="Todo" className="todo">
<label className="todo__status-label">
<input
data-cy="TodoStatus"
type="checkbox"
className="todo__status"
/>
</label>

<span data-cy="TodoTitle" className="todo__title">
Not Completed Todo
</span>

<button type="button" className="todo__remove" data-cy="TodoDelete">
×
</button>
</div>

{/* This todo is being edited */}
<div data-cy="Todo" className="todo">
<label className="todo__status-label">
<input
data-cy="TodoStatus"
type="checkbox"
className="todo__status"
/>
</label>

{/* This form is shown instead of the title and remove button */}
<form>
<input
data-cy="TodoTitleField"
type="text"
className="todo__title-field"
placeholder="Empty todo will be deleted"
value="Todo is being edited now"
/>
</form>
</div>

{/* This todo is in loadind state */}
<div data-cy="Todo" className="todo">
<label className="todo__status-label">
<input
data-cy="TodoStatus"
type="checkbox"
className="todo__status"
/>
</label>

<span data-cy="TodoTitle" className="todo__title">
Todo is being saved now
</span>

<button type="button" className="todo__remove" data-cy="TodoDelete">
×
</button>
</div>
</section>

{/* Hide the footer if there are no todos */}
<footer className="todoapp__footer" data-cy="Footer">
<span className="todo-count" data-cy="TodosCounter">
3 items left
</span>

{/* Active link should have the 'selected' class */}
<nav className="filter" data-cy="Filter">
<a
href="#/"
className="filter__link selected"
data-cy="FilterLinkAll"
>
All
</a>

<a
href="#/active"
className="filter__link"
data-cy="FilterLinkActive"
>
Active
</a>

<a
href="#/completed"
className="filter__link"
data-cy="FilterLinkCompleted"
>
Completed
</a>
</nav>

{/* this button should be disabled if there are no completed todos */}
<button
type="button"
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
>
Clear completed
</button>
</footer>
<Header />
{!isLoading && (
<>
<section className="todoapp__main" data-cy="TodoList">
{filteredTodos.map(todo => (
<TodoItem todo={todo} key={todo.id} />
))}
{tempTodo && <TodoItem todo={tempTodo} key={tempTodo.id} />}
</section>
{todos.length > 0 && <Footer />}
</>
)}
</div>
<ErrNotification />
</div>
);
};
15 changes: 15 additions & 0 deletions src/UserWarning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';

export const UserWarning: React.FC = () => (
<section className="section">
<p className="box is-size-3">
Please get your <b> userId </b>{' '}
<a href="https://mate-academy.github.io/react_student-registration">
here
</a>{' '}
and save it in the app <pre>const USER_ID = ...</pre>
All requests to the API must be sent with this
<b> userId.</b>
</p>
</section>
);
22 changes: 22 additions & 0 deletions src/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Todo } from '../types/Todo';
import { client } from '../utils/fetchClient';

// Your userId is 1414
// Please use it for all your requests to the Students API. For example:
// https://mate.academy/students-api/todos?userId=1414

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

export function deleteTodo(todoId: number) {
return client.delete(`/todos/${todoId}`);
}

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

export function updateTodo({ id, title, completed, userId }: Todo) {
return client.patch<Todo>(`/todos/${id}`, { title, completed, userId });
}
26 changes: 26 additions & 0 deletions src/components/ErrNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import classNames from 'classnames';
import { useTodoContext } from './TodoContext';

type Props = {};

export const ErrNotification: React.FC<Props> = () => {
const { error, setError } = useTodoContext();

return (
<div
data-cy="ErrorNotification"
className={classNames(
'notification is-danger is-light has-text-weight-normal',
{ hidden: !error },
)}
>
<button
data-cy="HideErrorButton"
type="button"
className="delete"
onClick={() => setError('')}
/>
<div>{error}</div>
</div>
);
};
83 changes: 83 additions & 0 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import classNames from 'classnames';
import { Filter } from '../types/Filter';
import { useTodoContext } from './TodoContext';
import { deleteTodo } from '../api/todos';
import { focusInput } from '../utils/services';
import { Todo } from '../types/Todo';

type Props = {};

export const Footer: React.FC<Props> = () => {
const {
todos,
setTodos,
showError,
setActiveTodoId,
inputRef,
filter,
setFilter,
} = useTodoContext();

const hasCompleted = todos.some(todo => todo.completed);

const handleFilterChange = (newFilter: Filter) => {
setFilter(newFilter);
};

const handleClearCompleted = async () => {
const completedTodoIds = todos
.filter(todo => todo.completed)
.map(todo => todo.id);

Promise.allSettled(
completedTodoIds.map(async todoId => {
try {
await deleteTodo(todoId);

setTodos((currentTodos: Todo[]) =>
currentTodos.filter(todo => todo.id !== todoId),
);
} catch {
showError('Unable to delete a todo');
} finally {
setActiveTodoId(null);
focusInput(inputRef);
}
}),
);
};

return (
<footer className="todoapp__footer" data-cy="Footer">
<span className="todo-count" data-cy="TodosCounter">
{todos.filter(todo => !todo.completed).length} items left
</span>

<nav className="filter" data-cy="Filter">
{Object.values(Filter).map(filterValue => (
<a
key={filterValue}
href={`#/${filterValue}`}
className={classNames('filter__link', {
selected: filter === filterValue,
})}
data-cy={`FilterLink${filterValue.charAt(0).toUpperCase() + filterValue.slice(1)}`}
onClick={() => handleFilterChange(filterValue)}
>
{filterValue.charAt(0).toUpperCase() + filterValue.slice(1)}
</a>
))}
</nav>

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

0 comments on commit 97246f0

Please sign in to comment.