Skip to content

Commit

Permalink
added task solution
Browse files Browse the repository at this point in the history
  • Loading branch information
koros-rk committed Oct 17, 2023
1 parent a258feb commit b8c4de9
Show file tree
Hide file tree
Showing 11 changed files with 464 additions and 85 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://koros-rk.github.io/react_todo-app/) and add it to the PR description.
120 changes: 37 additions & 83 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,93 +1,47 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import React, { useContext, useState } from 'react';

import { Todo } from './types/Todo';
import { FilterBy } from './types/FilterBy';
import { StateContext } from './states/TodosContext';
import { TodoHeader } from './components/TodoHeader';
import { TodoFooter } from './components/TodoFooter';
import { TodoList } from './components/TodoList';

const prepareTodos = (todos: Todo[], filterBy: FilterBy): Todo[] => {
return todos.filter((todo) => {
switch (filterBy) {
case FilterBy.Completed:
return todo.completed;
case FilterBy.Active:
return !todo.completed;
default:
return true;
}
})
.sort((a, b) => a.id - b.id);
};

export const App: React.FC = () => {
const { todos } = useContext(StateContext);
const [filterBy, setFilterBy] = useState(FilterBy.All);
const preparedTodos = prepareTodos(todos, filterBy);

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

<form>
<input
type="text"
data-cy="createTodo"
className="new-todo"
placeholder="What needs to be done?"
{ todos.length !== 0 && (
<>
<TodoList
todos={preparedTodos}
/>
</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>
<TodoFooter
selectedFilter={filterBy}
onFilterSelected={setFilterBy}
/>
</>
)}
</div>
);
};
92 changes: 92 additions & 0 deletions src/components/TodoFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useCallback, useContext, useMemo } from 'react';
import classNames from 'classnames';
import { DispatchContext, StateContext } from '../states/TodosContext';
import { FilterBy } from '../types/FilterBy';

interface Props {
selectedFilter: FilterBy,
onFilterSelected: (value: FilterBy) => void,
}

export const TodoFooter: React.FC<Props> = React.memo(({
selectedFilter,
onFilterSelected,
}) => {
const { todos } = useContext(StateContext);
const dispatch = useContext(DispatchContext);
const isCompletedExists = useMemo(
() => todos.some((todo) => todo.completed),
[todos],
);

const getActiveCount = useCallback(() => {
return todos.reduce((acc, todo) => {
return !todo.completed ? acc + 1 : acc;
}, 0);
}, [todos]);
const clearCompleted = () => {
const completedIds = todos.reduce((acc, todo) => {
return todo.completed ? [...acc, todo.id] : acc;
}, [] as number[]);

completedIds.forEach((todoId) => {
dispatch({ type: 'remove', payload: { id: todoId } });
});
};

return (
<footer className="footer">
<span className="todo-count" data-cy="todosCounter">
{`${getActiveCount()} items left`}
</span>

<ul className="filters" data-cy="todosFilter">
<li>
<a
href="#/"
className={classNames({
selected: selectedFilter === FilterBy.All,
})}
onClick={() => onFilterSelected(FilterBy.All)}
>
All
</a>
</li>

<li>
<a
href="#/active"
className={classNames({
selected: selectedFilter === FilterBy.Active,
})}
onClick={() => onFilterSelected(FilterBy.Active)}
>
Active
</a>
</li>

<li>
<a
href="#/completed"
className={classNames({
selected: selectedFilter === FilterBy.Completed,
})}
onClick={() => onFilterSelected(FilterBy.Completed)}
>
Completed
</a>
</li>
</ul>

{isCompletedExists && (
<button
type="button"
className="clear-completed"
onClick={clearCompleted}
>
Clear completed
</button>
)}
</footer>
);
});
41 changes: 41 additions & 0 deletions src/components/TodoHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useCallback, useContext, useState } from 'react';
import { DispatchContext } from '../states/TodosContext';

export const TodoHeader: React.FC = React.memo(() => {
const dispatch = useContext(DispatchContext);
const [todoContent, setTodoContent] = useState('');

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

if (todoContent !== '') {
dispatch({
type: 'add',
payload: {
id: +new Date(),
title: todoContent,
completed: false,
},
});
}

setTodoContent('');
}, [dispatch, todoContent]);

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

<form onSubmit={handleSubmit} onBlur={handleSubmit}>
<input
type="text"
data-cy="createTodo"
className="new-todo"
placeholder="What needs to be done?"
value={todoContent}
onChange={(event) => setTodoContent(event.target.value)}
/>
</form>
</header>
);
});
122 changes: 122 additions & 0 deletions src/components/TodoItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React, {
useCallback,
useContext, useEffect,
useRef,
useState,
} from 'react';
import classNames from 'classnames';

import { DispatchContext } from '../states/TodosContext';
import { Todo } from '../types/Todo';

interface Props {
todo: Todo
}

export const TodoItem: React.FC<Props> = ({ todo }) => {
const dispatch = useContext(DispatchContext);
const { id, title, completed } = todo;
const [newContent, setNewContent] = useState(title);
const [isEditing, setIsEditing] = useState(false);
const editInput = useRef<HTMLInputElement>(null);

const toggleTodo = useCallback((todoId: number) => {
dispatch({
type: 'toggleCheck',
payload: { id: todoId },
});
}, [dispatch]);

const deleteTodo = useCallback((todoId: number) => {
dispatch({
type: 'remove',
payload: { id: todoId },
});
}, [dispatch]);

const handleSubmit = useCallback(() => {
if (newContent !== '') {
dispatch({
type: 'update',
payload: {
id,
content: newContent,
},
});
}

if (newContent === '') {
dispatch({
type: 'remove',
payload: {
id,
},
});
}

setIsEditing(false);
}, [dispatch, id, newContent]);

useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setNewContent(title);
setIsEditing(false);
}

if (event.key === 'Enter') {
handleSubmit();
}
};

document.addEventListener('keyup', handleEscape);

return () => {
document.removeEventListener('keyup', handleEscape);
};
}, [handleSubmit, title]);

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

return (
<li
className={classNames({
completed,
editing: isEditing,
})}
>
<div className="view">
<input
type="checkbox"
className="toggle"
onChange={() => toggleTodo(id)}
checked={completed}
/>
<label
onDoubleClick={() => setIsEditing(true)}
>
{title}
</label>
<button
type="button"
className="destroy"
data-cy="deleteTodo"
onClick={() => deleteTodo(id)}
/>
</div>
<input
type="text"
className="edit"
ref={editInput}
value={newContent}
onChange={event => setNewContent(event.target.value)}
onBlur={handleSubmit}
/>
</li>
);
};
Loading

0 comments on commit b8c4de9

Please sign in to comment.