Skip to content

Commit

Permalink
solution
Browse files Browse the repository at this point in the history
  • Loading branch information
denys2 committed Oct 29, 2023
1 parent 3c2abd6 commit 1740dff
Show file tree
Hide file tree
Showing 13 changed files with 902 additions and 604 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ Implement a simple [TODO app](http://todomvc.com/examples/vanillajs/) working as
- Implement a solution following the [React task guideline](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 one more 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://denys2.github.io/react_todo-app/) and add it to the PR description.
996 changes: 470 additions & 526 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"devDependencies": {
"@cypress/react": "^5.12.4",
"@cypress/webpack-dev-server": "^1.8.4",
"@mate-academy/cypress-tools": "^1.0.5",
"@mate-academy/eslint-config-react-typescript": "^1.0.12",
"@mate-academy/cypress-tools": "^1.0.4",
"@mate-academy/eslint-config-react-typescript": "^1.0.11",
"@mate-academy/scripts": "^1.2.8",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
Expand Down
162 changes: 88 additions & 74 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,93 +1,107 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import React, { useState, useMemo, useContext } from 'react';
import { Filter } from './types/Filter';
import { TodosContext } from './components/TodosContext';
import { Todolist } from './components/TodoList';
import { TodoFilter } from './components/TodoFilter';

export const App: React.FC = () => {
const [title, setTitle] = useState('');
const [filter, setFilter] = useState<Filter>(Filter.All);

const todosContext = useContext(TodosContext);

const {
todos,
addTodo,
deleteCompletedTodos,
handleToggleAll,
todoCount,
completedTodos,
filterTodos,
} = todosContext;

const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setTitle(event.target.value);
};

const handleFormSubmit = (event: React.FormEvent) => {
event.preventDefault();

if (title.trim() === '') {
return;
}

addTodo(title);

setTitle('');
};

const handleClearCompleted = () => {
deleteCompletedTodos();
};

const filteredTodos = useMemo(() => {
return filterTodos(filter);
}, [filter, todos]);

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

<form>
<form onSubmit={handleFormSubmit}>
<input
type="text"
data-cy="createTodo"
className="new-todo"
placeholder="What needs to be done?"
value={title}
onChange={handleTitleChange}
/>
</form>
</header>

<section className="main">
<input
type="checkbox"
id="toggle-all"
className="toggle-all"
data-cy="toggleAll"
/>
<label htmlFor="toggle-all">Mark all as complete</label>

<ul className="todo-list" data-cy="todoList">
<li>
<div className="view">
<input type="checkbox" className="toggle" id="toggle-view" />
<label htmlFor="toggle-view">asdfghj</label>
<button type="button" className="destroy" data-cy="deleteTodo" />
</div>
<input type="text" className="edit" />
</li>

<li className="completed">
<div className="view">
<input type="checkbox" className="toggle" id="toggle-completed" />
<label htmlFor="toggle-completed">qwertyuio</label>
<button type="button" className="destroy" data-cy="deleteTodo" />
</div>
<input type="text" className="edit" />
</li>

<li className="editing">
<div className="view">
<input type="checkbox" className="toggle" id="toggle-editing" />
<label htmlFor="toggle-editing">zxcvbnm</label>
<button type="button" className="destroy" data-cy="deleteTodo" />
</div>
<input type="text" className="edit" />
</li>

<li>
<div className="view">
<input type="checkbox" className="toggle" id="toggle-view2" />
<label htmlFor="toggle-view2">1234567890</label>
<button type="button" className="destroy" data-cy="deleteTodo" />
</div>
<input type="text" className="edit" />
</li>
</ul>
</section>

<footer className="footer">
<span className="todo-count" data-cy="todosCounter">
3 items left
</span>

<ul className="filters">
<li>
<a href="#/" className="selected">All</a>
</li>

<li>
<a href="#/active">Active</a>
</li>

<li>
<a href="#/completed">Completed</a>
</li>
</ul>

<button type="button" className="clear-completed">
Clear completed
</button>
</footer>
{!!todos.length && (
<>
<section className="main">
<input
type="checkbox"
id="toggle-all"
className="toggle-all"
data-cy="toggleAll"
onClick={handleToggleAll}
/>
<label htmlFor="toggle-all">Mark all as complete</label>

<Todolist
todos={filteredTodos}
/>
</section>

<footer className="footer" data-cy="todosFilter">
<span className="todo-count" data-cy="todosCounter">
{todoCount === 1
? `${todoCount} item left`
: `${todoCount} items left`}
</span>

<TodoFilter
filter={filter}
setFilter={setFilter}
/>

{completedTodos && (
<button
type="button"
className="clear-completed"
onClick={handleClearCompleted}
>
Clear completed
</button>
)}
</footer>
</>
)}
</div>
);
};
47 changes: 47 additions & 0 deletions src/components/TodoFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import { Filter } from '../types/Filter';

type Props = {
filter: Filter;
setFilter: (filter: Filter) => void ;
};

export const TodoFilter: React.FC<Props> = ({ filter, setFilter }) => {
const onFilterChange = (status: Filter) => () => {
setFilter(status);
};

return (
<ul className="filters">
<li>
<a
href="#/"
className={filter === 'all' ? 'selected' : ''}
onClick={onFilterChange(Filter.All)}
>
All
</a>
</li>

<li>
<a
href="#/active"
className={filter === 'active' ? 'selected' : ''}
onClick={onFilterChange(Filter.Active)}
>
Active
</a>
</li>

<li>
<a
href="#/completed"
className={filter === 'completed' ? 'selected' : ''}
onClick={onFilterChange(Filter.Completed)}
>
Completed
</a>
</li>
</ul>
);
};
115 changes: 115 additions & 0 deletions src/components/TodoItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, {
useContext,
useEffect,
useRef,
useState,
} from 'react';
import cn from 'classnames';
import { Todo } from '../types/Todo';
import { TodosContext } from './TodosContext';

type Props = {
todo: Todo;
};

export const TodoItem: React.FC<Props> = ({ todo }) => {
const { title, completed, id } = todo;

const todoContext = useContext(TodosContext);
const { toggleTodo, deleteTodo, editingTodo } = todoContext;

const [isEditing, setIsEditing] = useState(false);
const [editedTitle, setEditedTitle] = useState(title);

const handleToggleTodo = () => {
toggleTodo(id);
};

const handleDeleteTodo = () => {
deleteTodo(id);
};

const handleEdit = () => {
setIsEditing(true);
};

const handleEditChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setEditedTitle(event.target.value);
};

const handleEditSubmit = () => {
const trimmedTitle = editedTitle.trim();

if (trimmedTitle !== '') {
setEditedTitle(trimmedTitle);
setIsEditing(false);
editingTodo(id, trimmedTitle);
} else {
deleteTodo(id);
}
};

const handleEditCancel = () => {
setIsEditing(false);
setEditedTitle(title);
};

const handleKeyUp = (event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
handleEditSubmit();
} else if (event.key === 'Escape') {
handleEditCancel();
}
};

const inputField = useRef<HTMLInputElement>(null);

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

return (
<li className={cn({
completed,
editing: isEditing,
})}
>
{!isEditing ? (
<div className="view">
<input
type="checkbox"
className="toggle"
id={`toggle-${id}`}
checked={completed}
onChange={handleToggleTodo}
/>

<label onDoubleClick={handleEdit}>
{title}
</label>

<button
type="button"
className="destroy"
data-cy="deleteTodo"
aria-label="deleteTodo"
onClick={handleDeleteTodo}
/>
</div>
) : (
<input
type="text"
ref={inputField}
className="edit"
value={editedTitle}
placeholder="Empty todo will be deleted"
onChange={handleEditChange}
onBlur={handleEditSubmit}
onKeyUp={handleKeyUp}
/>
)}
</li>
);
};
20 changes: 20 additions & 0 deletions src/components/TodoList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import { TodoItem } from './TodoItem';
import { Todo } from '../types/Todo';

type Props = {
todos: Todo[];
};

export const Todolist: React.FC<Props> = ({ todos }) => {
return (
<ul className="todo-list" data-cy="todosList">
{todos.map((todo: Todo) => (
<TodoItem
key={todo.id}
todo={todo}
/>
))}
</ul>
);
};
Loading

0 comments on commit 1740dff

Please sign in to comment.