Skip to content

Commit

Permalink
add solution
Browse files Browse the repository at this point in the history
  • Loading branch information
Valentyn-Radobenko committed Oct 20, 2024
1 parent f1536f7 commit 7f550c8
Show file tree
Hide file tree
Showing 24 changed files with 818 additions and 162 deletions.
181 changes: 40 additions & 141 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,157 +1,56 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import React, { useEffect, useRef } from 'react';
import { Filter } from './types/Filter';
import { TodoList } from './components/TodoList';
import { Header } from './components/Header';
import { Footer } from './components/Footer';
import { ErrorNotification } from './components/ErrorNotification';
import { TodoItem } from './components/TodoItem';
import { useTodoContext } from './components/context/TodoContext';

export const App: React.FC = () => {
const { state } = useTodoContext();
const { todos, filter, tempTodo } = state;
const textField = useRef<HTMLInputElement>(null);

const completedTodos = todos.filter(todo => todo.completed);
const activeTodos = todos.filter(todo => !todo.completed);

const filteredTodos = () => {
switch (filter) {
case Filter.Completed:
return completedTodos;
case Filter.Active:
return activeTodos;
default:
return todos;
}
};

useEffect(() => {
if (textField.current) {
textField.current.focus();
}
}, [todos.length]);

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>
<Header completedTodos={completedTodos} textField={textField} />

<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>
<TodoList todos={filteredTodos()} />

{/* 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>
{tempTodo && <TodoItem todo={tempTodo} isLoading={true} />}
</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>
{todos.length > 0 && (
<Footer activeTodos={activeTodos} completedTodos={completedTodos} />
)}
</div>

<ErrorNotification />
</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>
);
24 changes: 24 additions & 0 deletions src/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Todo } from '../types/Todo';
import { client } from '../utils/fetchClient';

export const USER_ID = 1011;

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

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

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

export const updateTodo = (todo: Todo) => {
return client.patch<Todo>(`/todos/${todo.id}`, todo);
};
42 changes: 42 additions & 0 deletions src/components/ErrorNotification/ErrorNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { useEffect } from 'react';
import classNames from 'classnames';
import { useTodoContext } from '../context/TodoContext';

export const ErrorNotification: React.FC = () => {
const { state, dispatch } = useTodoContext();
const { errorText } = state;

useEffect(() => {
if (errorText) {
const timer = setTimeout(() => {
dispatch({ type: 'SET_ERROR', payload: null });
}, 3000);

return () => clearTimeout(timer);
}

return () => {};
}, [errorText, dispatch]);

if (!errorText) {
return null;
}

return (
<div
data-cy="ErrorNotification"
className={classNames(
'notification is-danger is-light has-text-weight-normal',
{ hidden: errorText },
)}
>
<button
onClick={() => dispatch({ type: 'SET_ERROR', payload: null })}
data-cy="HideErrorButton"
type="button"
className="delete"
/>
{errorText}
</div>
);
};
1 change: 1 addition & 0 deletions src/components/ErrorNotification/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ErrorNotification';
75 changes: 75 additions & 0 deletions src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import classNames from 'classnames';
import { Filter } from '../../types/Filter';
import { Error } from '../../types/Error';
import { useTodoContext } from '../context/TodoContext';
import { Todo } from '../../types/Todo';

type Props = {
activeTodos: Todo[];
completedTodos: Todo[];
};

export const Footer: React.FC<Props> = ({ activeTodos, completedTodos }) => {
const { state, dispatch } = useTodoContext();
const { filter } = state;

const handleClearCompleted = async () => {
const completedIds = completedTodos.map(todo => todo.id);

dispatch({ type: 'SET_DELETING_IDS', payload: completedIds });

try {
dispatch({ type: 'CLEAR_COMPLETED' });
} catch {
dispatch({
type: 'SET_ERROR',
payload: Error.unableToDelete,
});
setTimeout(() => {
dispatch({ type: 'SET_ERROR', payload: null });
}, 3000);
} finally {
dispatch({ type: 'SET_DELETING_IDS', payload: [] });
}
};

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

<nav className="filter" data-cy="Filter">
{Object.values(Filter).map(filterOption => (
<a
key={filterOption}
href={`#/${filterOption.toLowerCase()}`}
onClick={() =>
dispatch({
type: 'SET_FILTER',
payload: filterOption,
})
}
className={classNames('filter__link', {
selected: filter === filterOption,
})}
data-cy={`FilterLink${filterOption}`}
>
{filterOption}
</a>
))}
</nav>

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

0 comments on commit 7f550c8

Please sign in to comment.