Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ Implement a simple [TODO app](https://mate-academy.github.io/react_todo-app/) th
- Implement a solution following the [React task guidelines](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).
- Open another terminal and run tests with `npm test` to ensure your solution is correct.
- Replace `<your_account>` with your GitHub username in the [DEMO LINK](https://<your_account>.github.io/react_todo-app/) and add it to the PR description.
- Replace `<your_account>` with your GitHub username in the [DEMO LINK](https://HiBlurryface.github.io/react_todo-app/) and add it to the PR description.
12 changes: 7 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
"@mate-academy/scripts": "^1.9.12",
"@mate-academy/scripts": "^2.1.3",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
Expand Down
177 changes: 33 additions & 144 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,156 +1,45 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import { useEffect } from 'react';
import { useReducer } from 'react';
import { Footer } from './components/footer/Footer';
import { TodoList } from './components/TodoList/TodoList';
import { Header } from './header/Header';
import { SortContext } from './store/SortContext';
import { SortReducer } from './store/SortReducer';
import { TodoContext } from './store/TodoContext';
import { TodoReducer } from './store/TodoReducer';

export const App: React.FC = () => {
const initTodos = () => {
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
};

const [todos, dispatch] = useReducer(
TodoReducer,
[],
initTodos
);

useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);

const [sortBy, sortDispatch] = useReducer(SortReducer, 'all');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This file uses a raw string literal for the initial filter state: useReducer(SortReducer, 'all'). This violates checklist item #10: "Do not rely on an unknown string; make constants for this (e.g., FILTERS = { all: 'all', completed: 'completed', active: 'active' })." Use a shared FILTERS constant (e.g. FILTERS.all) here so the code is explicit and consistent with the rest of the app.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This uses a raw string 'all' as the initial sort value. The task requires using a shared FILTERS constant instead of unknown string literals. Please use the FILTERS constant (for example FILTERS.all) so all files reference the same values.

Checklist violation (quote):
"Do not rely on the unknown string, make constants for this: [CHECKLIST ITEM #10]

const FILTERS = {
  all: 'all',
  completed: 'completed',
  active: 'active',
};
```","lineStart":30,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This violates checklist item #10: "Do not rely on the unknown string, make constants for this."

The initial state for the filter reducer is hardcoded as 'all'. You should import the Filters constant from src/store/filters.tsx and use a value from it (e.g., Filters[0]) to ensure consistency and avoid magic strings.


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>
<TodoContext.Provider value={{ todos, dispatch }}>
<SortContext.Provider value={{ sortBy, sortDispatch }}>
<Header />
<TodoList />
{todos.length > 0 && <Footer />}
</SortContext.Provider>
</TodoContext.Provider>
</div>
</div>
);
Expand Down
112 changes: 112 additions & 0 deletions src/components/TodoItem.tsx/TodoItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import classNames from 'classnames';
import { useRef } from 'react';
import { useState } from 'react';
import { useEffect } from 'react';
import { useContext } from 'react';
import { TodoContext } from '../../store/TodoContext';
import {
completeTodoAction,
deleteTodoAction,
renameTodoAction,
} from '../../store/TodoReducer';
import { TodoType } from '../../types/TodoType';

type Props = {
todo: TodoType;
};

export const TodoItem = ({ todo }: Props) => {
const { dispatch } = useContext(TodoContext);
const [title, setTitle] = useState(todo.title);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You access todo properties repeatedly instead of destructuring them. The checklist requires: "Use destructuring wherever possible. It makes code more readable. [CHECKLIST ITEM #2]" Consider destructuring at the top of the component: const { id, title: initialTitle, completed } = todo; and update usages accordingly (also adjust state init to use initialTitle).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

title state is initialized from todo.title but never synchronized when todo.title changes after a rename from the context. That can leave the input showing a stale value when re-entering edit mode. Add a useEffect to update title when todo.title changes, e.g. useEffect(() => setTitle(todo.title), [todo.title]), so the local input always reflects the current todo value.

const [isEditing, setIsEditing] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
setTitle(todo.title);
}, [todo.title]);

const cancelEditings = (event: React.KeyboardEvent<HTMLInputElement>) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Handler name cancelEditings is unclear/grammatically odd. This violates the previous request: "In TodoItem.tsx, fix the cancel‑editing handler name (to correctly reflect its purpose)." Rename it to something explicit (e.g. handleCancelEditing or cancelEditing) to make intent obvious.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This function name is a bit unconventional. Per checklist item #6, function names should be clear and follow conventions. A name like handleKeyUp or cancelEdit would be more standard and descriptive, as it handles the key-up event to cancel a single edit.

if (event.key === 'Escape') {
setTitle(todo.title);
setIsEditing(false);
}
};

const editTodo = () => {
const trimmed = title.trim();

if (!trimmed) {
dispatch(deleteTodoAction(todo.id));
} else {
dispatch(renameTodoAction(todo.id, trimmed));
}

setIsEditing(false);
Comment on lines +35 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The editTodo handler uses the raw title value when checking for emptiness and when renaming. This violates the inline-editing requirements: "Trim the saved text." and "Delete the todo if the title is empty." You should trim the title (e.g. const trimmed = title.trim()) before checking trimmed === '' and before dispatching renameTodoAction with the trimmed value.

};

const saveTodoOnEnter = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
editTodo();
};

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

return (
<div
data-cy="Todo"
className={classNames('todo', { completed: todo.completed })}
key={todo.id}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This key prop has no effect here. React requires keys on elements within an array, which are provided in the parent component (TodoList.tsx in this case) where you map over the list. Placing it on the root element inside the child component is redundant and demonstrates a misunderstanding of how keys work. This violates checklist item #8: 'Use key attribute correctly'.

>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label className="todo__status-label" htmlFor={`todo-status-${todo.id}`}>
<input
id={`todo-status-${todo.id}`}
data-cy="TodoStatus"
type="checkbox"
className="todo__status"
checked={todo.completed ? true : false}
onChange={() => dispatch(completeTodoAction(todo.id))}
/>
</label>

{isEditing === false ? (
<>
<span
data-cy="TodoTitle"
className="todo__title"
onDoubleClick={() => setIsEditing(true)}
>
{todo.title}
</span>
<button
type="button"
className="todo__remove"
data-cy="TodoDelete"
onClick={() => dispatch(deleteTodoAction(todo.id))}
>
×
</button>
</>
) : (
<form onSubmit={saveTodoOnEnter}>
<input
ref={inputRef}
data-cy="TodoTitleField"
type="text"
className="todo__title-field"
placeholder="Empty todo will be deleted"
value={title}
onChange={event => setTitle(event.target.value)}
onKeyUp={event => cancelEditings(event)}
onBlur={() => editTodo()}
/>
</form>
)}
</div>
);
};
29 changes: 29 additions & 0 deletions src/components/TodoList/TodoList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useContext } from 'react';
import { SortContext } from '../../store/SortContext';
import { TodoContext } from '../../store/TodoContext';
import { TodoItem } from '../TodoItem.tsx/TodoItem';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Invalid import path will cause module resolution/build errors. The import reads ../TodoItem.tsx/TodoItem which includes the .tsx extension inside the path and likely doesn't match the real folder/file structure. Update the import to the correct module path (for example ../TodoItem/TodoItem) or the actual relative path used in your project so the component can be resolved.


export const TodoList = () => {
const { todos } = useContext(TodoContext);
const { sortBy } = useContext(SortContext);

const filteredTodos = todos.filter(todo => {
if (sortBy === 'active') {
return !todo.completed;
}

if (sortBy === 'completed') {
return todo.completed;
Comment on lines +11 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This uses raw string literals ('active' / 'completed') for filtering. This violates checklist item #10: "Do not rely on an unknown string; make constants for this (e.g., FILTERS = { all: 'all', completed: 'completed', active: 'active' })." Replace these literals by importing and using a FILTERS constant (for example if (sortBy === FILTERS.active)) so the code relies on a single source of truth for filter names.

Comment on lines +11 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This file compares sortBy to raw strings 'active' and 'completed' inside the filter logic. The checklist requires a shared filters constant instead of relying on raw strings: "Do not rely on the unknown string, make constants for this. const FILTERS = { all: 'all', completed: 'completed', active: 'active', };". Please replace the string literals with references to a shared FILTERS constant (import it and use FILTERS.active / FILTERS.completed) so filtering is consistent across the app and properly typed. See the checklist and this file for reference: checklist: ; this component: .

}
Comment on lines +11 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This violates checklist item #10: 'Do not rely on the unknown string, make constants for this.' Instead of hardcoding the strings 'active' and 'completed', you should import the Filters constant from ../../store/filters.tsx and use its values for comparison. This will make your code more robust against typos and align with the requirement to use constants.


return true;
});

return (
<section className="todoapp__main" data-cy="TodoList">
{filteredTodos.map(todo => {
return <TodoItem todo={todo} key={todo.id} />;
})}
</section>
);
};
Loading
Loading