Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test #1100

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

test #1100

Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
solution
  • Loading branch information
Vasyl Pylypchynets committed Jan 15, 2025
commit 609c43b49dd8e9856ff37f3ca633d1efc2545b02
7 changes: 5 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import React, { useContext } from 'react';
import { Header } from './components/Header';
import { Footer } from './components/Footer';
import { TodoList } from './components/TodoList';
import { StateContext } from './context/GlobalState';

export const App: React.FC = () => {
const { todos } = useContext(StateContext);

return (
<div className="todoapp">
<h1 className="todoapp__title">todos</h1>
@@ -15,7 +18,7 @@ export const App: React.FC = () => {
<TodoList />

{/* Hide the footer if there are no todos */}
<Footer />
{todos.length !== 0 && <Footer />}
</div>
</div>
);
66 changes: 50 additions & 16 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,68 @@
import { useContext, useMemo } from 'react';
import { DispatchContext, StateContext } from '../context/GlobalState';
import { FilterType } from '../enum/filter';
import classNames from 'classnames';

export function Footer() {
const dispatch = useContext(DispatchContext);
const { todos, filter: activeFilter } = useContext(StateContext);

const TodosLeft: number = useMemo(() => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const TodosLeft: number = useMemo(() => {
const todosLeft: number = useMemo(() => {

return todos.filter(todo => {
return !todo.completed;
}).length;
}, [todos]);

const hasCompletedTodo: boolean = useMemo(() => {
return (
todos.filter(todo => {
return todo.completed;
}).length > 0
);
}, [todos]);

function handleDeleteTodo() {
const updatedTodos = todos.filter(todo => {
return !todo.completed;
});

dispatch({ type: 'update', payload: updatedTodos });
}

return (
<footer className="todoapp__footer" data-cy="Footer">
<span className="todo-count" data-cy="TodosCounter">
3 items left
{`${TodosLeft} 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>
{Object.values(FilterType).map(filter => (
<a
key={filter}
href={`#/${filter}`}
className={classNames('filter__link', {
selected: filter === activeFilter,
})}
data-cy={`FilterLink${filter.charAt(0).toUpperCase() + filter.slice(1)}`}
onClick={e => {
e.preventDefault();
dispatch({ type: filter });
}}
>
{filter.charAt(0).toUpperCase() + filter.slice(1)}
</a>
))}
</nav>

{/* this button should be disabled if there are no completed todos */}

<button
type="button"
className="todoapp__clear-completed"
disabled={!hasCompletedTodo}
data-cy="ClearCompletedButton"
onClick={handleDeleteTodo}
>
Clear completed
</button>
90 changes: 68 additions & 22 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,92 @@
// import { useContext, useState } from 'react';
// import { DispatchContext } from '../context/GlobalState';
// import { Todo } from '../types/Todo';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { DispatchContext, StateContext } from '../context/GlobalState';
import { Todo } from '../types/Todo';
import classNames from 'classnames';

export function Header() {
// const [query, setQuery] = useState<string>('');
// const { dispatch } = useContext(DispatchContext);
const [query, setQuery] = useState<string>('');

// Ensure dispatch is not null
const dispatch = useContext(DispatchContext);
const { todos } = useContext(StateContext);

const inputRef = useRef<HTMLInputElement | null>(null);

const hasAllCompleted = useMemo(() => {
return (
todos.findIndex(todo => {
return !todo.completed;
}) < 0
);
}, [todos]);

function handleToggleStatusAll() {
if (hasAllCompleted) {
const updatedTodos = todos.map(todo => {
return { ...todo, completed: !todo.completed };
});

dispatch({ type: 'update', payload: updatedTodos });
}

if (!hasAllCompleted) {
const updatedTodos = todos.map(todo => {
return { ...todo, completed: true };
});

dispatch({ type: 'update', payload: updatedTodos });
}
}

function handleCreateTodo(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();

// const todo: Todo = {
// id: +new Date(),
// title: query.trim(),
// completed: false,
// };
if (query.trim() !== '') {
const todo: Todo = {
id: +new Date(),
title: query.trim(),
completed: false,
};

const newTodosList = [...todos, todo];

// dispatch({ type: 'create', payload: todo });
// setQuery('');
dispatch({ type: 'update', payload: newTodosList });
setQuery('');
}
}

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

return (
<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"
/>

{todos.length > 0 && (
<button
type="button"
className={classNames('todoapp__toggle-all', {
active: hasAllCompleted,
})}
data-cy="ToggleAllButton"
onClick={handleToggleStatusAll}
/>
)}

{/* Add a todo on form submit */}
<form onSubmit={handleCreateTodo}>
<input
ref={inputRef}
data-cy="NewTodoField"
type="text"
// value={query}
value={query}
className="todoapp__new-todo"
placeholder="What needs to be done?"
// onChange={e => {
// setQuery(e.target.value);
// }}
onChange={e => {
setQuery(e.target.value);
}}
/>
</form>
</header>
146 changes: 146 additions & 0 deletions src/components/TodoItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import classNames from 'classnames';
import { Todo } from '../types/Todo';
import { useContext, useEffect, useRef, useState } from 'react';
import { DispatchContext, StateContext } from '../context/GlobalState';

interface TodoItemProps {
todoItem: Todo;
}

export function TodoItem({ todoItem }: TodoItemProps) {
const [itemEditingId, setItemEditingId] = useState<number | null>(null);
const [newTitle, setNewTitle] = useState<string>('');

const { todos } = useContext(StateContext);
const dispatch = useContext(DispatchContext);

const inputRef = useRef<HTMLInputElement | null>(null);

function handleDeleteTodo(id: number) {
const updatedTodos = todos.filter(todo => {
return todo.id !== id;
});

dispatch({ type: 'update', payload: updatedTodos });
}

function handleEditSubmit() {
const trimmedTitle = newTitle.trim();

if (trimmedTitle === '') {
handleDeleteTodo(todoItem.id);

return;
}

const updatedTodos = todos.map(todo => {
if (todo.id === todoItem.id) {
return { ...todo, title: trimmedTitle };
}

return todo;
});

dispatch({ type: 'update', payload: updatedTodos });

setItemEditingId(null);
setNewTitle('');
}

function handleToggleStatus(id: number) {
const updatedTodos = todos.map(todo => {
if (todo.id === id) {
return { ...todo, completed: !todo.completed };
}

return todo;
});

dispatch({ type: 'update', payload: updatedTodos });
}

function handleDoubleClick() {
setNewTitle(todoItem.title);
setItemEditingId(todoItem.id);
}

useEffect(() => {
if (itemEditingId === todoItem.id && inputRef.current) {
inputRef.current.focus();
setNewTitle(todoItem.title);
}
}, [itemEditingId, todoItem.id, todoItem.title]);

function handleCancel(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Escape') {
setItemEditingId(null);
setNewTitle(todoItem.title);
}
}

return (
<div
data-cy="Todo"
className={classNames('todo', { completed: todoItem.completed })}
key={todoItem.id}
>
<label className="todo__status-label" htmlFor={`${todoItem.id}`}>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<input
id={`${todoItem.id}`}
data-cy="TodoStatus"
type="checkbox"
className="todo__status"
checked={todoItem.completed}
onClick={() => {
handleToggleStatus(todoItem.id);
}}
/>
</label>
{todoItem.id !== itemEditingId && (
<>
<span
data-cy="TodoTitle"
className="todo__title"
onDoubleClick={() => handleDoubleClick()}
>
{todoItem.title}
</span>
<button
type="button"
className="todo__remove"
data-cy="TodoDelete"
onClick={() => {
handleDeleteTodo(todoItem.id);
}}
>
×
</button>
</>
)}

{todoItem.id === itemEditingId && (
<form
onSubmit={e => {
e.preventDefault();
handleEditSubmit();
}}
>
<input
ref={inputRef}
data-cy="TodoTitleField"
type="text"
className="todo__title-field"
placeholder="Empty todo will be deleted"
value={newTitle}
onBlur={handleEditSubmit}
onChange={e => {
setNewTitle(e.target.value);
}}
onKeyUp={handleCancel}
/>
</form>
)}
</div>
);
}
Loading