Skip to content

Commit

Permalink
add task solution
Browse files Browse the repository at this point in the history
  • Loading branch information
DariaFesiun committed Dec 12, 2024
1 parent f1536f7 commit fa55616
Show file tree
Hide file tree
Showing 14 changed files with 415 additions and 150 deletions.
150 changes: 6 additions & 144 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,156 +1,18 @@
/* 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"
/>

{/* 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>
<Header />
<TodoList />
<Footer />
</div>
</div>
);
Expand Down
67 changes: 67 additions & 0 deletions src/GlobalProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Action } from './types/Action';
import { Filter } from './types/Filter';
import { State } from './types/State';
import { useEffect, useReducer } from 'react';
import React 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>
</>
);
};
92 changes: 92 additions & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Todo } from '../types/Todo';
import classNames from 'classnames';
import { DispatchContext, StateContext } from '../GlobalProvider';


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 fa55616

Please sign in to comment.