Skip to content

Commit

Permalink
solution
Browse files Browse the repository at this point in the history
  • Loading branch information
Daryna-Kukharets committed Nov 21, 2024
1 parent f1536f7 commit 56e7e61
Show file tree
Hide file tree
Showing 16 changed files with 580 additions and 252 deletions.
271 changes: 166 additions & 105 deletions package-lock.json

Large diffs are not rendered by default.

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.8.5",
"@mate-academy/scripts": "^1.9.12",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
Expand Down
148 changes: 6 additions & 142 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,156 +1,20 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import { Header } from './components/Header';
import { TodoList } from './components/TodoList';
import { Footer } from './components/Footer';

export const App: React.FC = () => {
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"
/>
<Header />

{/* 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>
<TodoList />

<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>
<Footer />
</div>
</div>
);
Expand Down
66 changes: 66 additions & 0 deletions src/GlobalProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Action } from './types/Action';
import { Filter } from './types/Filter';
import { State } from './types/State';
import React, { useEffect, useReducer } from 'react';

const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'addTodo':
return {
...state,
todos: [...state.todos, action.payload],
};

case 'deleteTodo':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload),
};

case 'updateTodo':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id ? action.payload : todo,
),
};

case 'setFilter':
return {
...state,
filter: action.payload,
};

default:
return state;
}
};

const loadedTodos = localStorage.getItem('todos');
const initialState: State = {
todos: loadedTodos ? JSON.parse(loadedTodos) : [],
filter: Filter.All,
};

export const StateContext = React.createContext<State>(initialState);
export const DispatchContext = React.createContext<React.Dispatch<Action>>(
() => {},
);

type Props = {
children: React.ReactNode;
};

export const GlobalProvider: React.FC<Props> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);

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

return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
);
};
64 changes: 64 additions & 0 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useContext, useMemo } from 'react';
import { DispatchContext, StateContext } from '../GlobalProvider';
import { Filter } from '../types/Filter';
import classNames from 'classnames';

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

const activeTodosCount = useMemo(
() => todos.filter(todo => !todo.completed).length,
[todos],
);

const deleteCompleted = () => {
todos.forEach(todo => {
if (todo.completed) {
dispatch({ type: 'deleteTodo', payload: todo.id });
}
});
};

if (!todos.length) {
return;
}

return (
<>
<footer className="todoapp__footer" data-cy="Footer">
<span className="todo-count" data-cy="TodosCounter">
{`${activeTodosCount} items left`}
</span>

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

<button
type="button"
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
disabled={activeTodosCount === todos.length}
onClick={deleteCompleted}
>
Clear completed
</button>
</footer>
</>
);
};
91 changes: 91 additions & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { DispatchContext, StateContext } from '../GlobalProvider';
import { Todo } from '../types/Todo';
import classNames from 'classnames';

export const Header = () => {
const [title, setTitle] = useState('');

const titleField = useRef<HTMLInputElement>(null);

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

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

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

if (!title.trim()) {
return;
}

const newTodo: Todo = {
id: Date.now(),
title: title.trim(),
completed: false,
};

dispatch({ type: 'addTodo', payload: newTodo });
setTitle('');
};

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

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

const handleToggleAllButtonClick = () => {
let todosToChange = [];

if (allTodosCompleted) {
todosToChange = [...todos];
} else {
todosToChange = todos.filter(todo => !todo.completed);
}

todosToChange.forEach(todo => {
const { id, title: todoTitle, completed } = todo;

dispatch({
type: 'updateTodo',
payload: { id, title: todoTitle, completed: !completed },
});
});
};

return (
<header className="todoapp__header">
{!!todos.length && (
<button
type="button"
className={classNames('todoapp__toggle-all', {
active: allTodosCompleted,
})}
data-cy="ToggleAllButton"
onClick={handleToggleAllButtonClick}
/>
)}

<form onSubmit={handleSubmit}>
<input
ref={titleField}
data-cy="NewTodoField"
type="text"
className="todoapp__new-todo"
placeholder="What needs to be done?"
value={title}
onChange={handleTitleChange}
autoFocus
/>
</form>
</header>
);
};
Loading

0 comments on commit 56e7e61

Please sign in to comment.