Skip to content

Commit

Permalink
Solution
Browse files Browse the repository at this point in the history
  • Loading branch information
deandre25 committed Sep 18, 2023
1 parent 6532826 commit bd4ddcd
Show file tree
Hide file tree
Showing 19 changed files with 630 additions and 18 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ Implement the ability to edit a todo title on double click:

- 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).
- Replace `<your_account>` with your Github username in the [DEMO LINK](https://<your_account>.github.io/react_todo-app-with-api/) and add it to the PR description.
- Replace `<your_account>` with your Github username in the [DEMO LINK](https://deandre25.github.io/react_todo-app-with-api/) and add it to the PR description.
22 changes: 5 additions & 17 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
/* eslint-disable max-len */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import { UserWarning } from './UserWarning';

const USER_ID = 0;
import { TodoProvider } from './components/TodoContext/TodoContext';
import TodoApp from './components/TodoApp';

export const App: React.FC = () => {
if (!USER_ID) {
return <UserWarning />;
}

return (
<section className="section container">
<p className="title is-4">
Copy all you need from the prev task:
<br />
<a href="https://github.com/mate-academy/react_todo-app-add-and-delete#react-todo-app-add-and-delete">React Todo App - Add and Delete</a>
</p>

<p className="subtitle">Styles are already copied</p>
</section>
<TodoProvider>
<TodoApp />
</TodoProvider>
);
};
8 changes: 8 additions & 0 deletions src/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Todo } from '../types/Todo';
import { client } from '../utils/fetchClient';

export const getTodos = (userId: number) => {
return client.get<Todo[]>(`/todos?userId=${userId}`);
};

// Add more methods here
37 changes: 37 additions & 0 deletions src/components/TodoApp/TodoApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { useState } from 'react';
import TodoHeader from '../TodoHeader';
import TodoFooter from '../TodoFooter';
import TodoList from '../TodoList';
import { useTodo } from '../TodoContext/TodoContext';
import { Filter } from '../../types/Filter';
import TodoNotification from '../TodoNotification';

export const TodoApp: React.FC = () => {
const { todos } = useTodo();
const [selectedFilter, setSelectedFilter] = useState<Filter>(Filter.all);

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

<div className="todoapp__content">
<TodoHeader />

{todos.length > 0 && (
<>
<TodoList
todos={todos}
selectedFilter={selectedFilter}
/>
<TodoFooter
selectedFilter={selectedFilter}
setSelectedFilter={setSelectedFilter}
/>
</>
)}
</div>

<TodoNotification />
</div>
);
};
3 changes: 3 additions & 0 deletions src/components/TodoApp/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { TodoApp } from './TodoApp';

export default TodoApp;
155 changes: 155 additions & 0 deletions src/components/TodoContext/TodoContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Todo } from '../../types/Todo';
import { getTodos } from '../../api/todos';
import { UserWarning } from '../../UserWarning';

const USER_ID = 11453;

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

type TodoContextValue = {
todos: Todo[],
todosUncompleted: number,
todosCompleted: boolean,
addTodo: (title: string) => void,
toggleTodo: (id: number) => void,
toogleAll: () => void,
deleteTodo: (id: number) => void,
deleteCompletedTodo: () => void,
updateTodo: (updatedTitle: string, id: number) => void,
isError: boolean,
setIsError: (isError: boolean) => void,
errorMessage: string,
setErrorMessage: (errorMessage: string) => void,
};

export const TodoContext = React.createContext<TodoContextValue>({
todos: [],
todosUncompleted: 0,
todosCompleted: false,
addTodo: () => { },
toggleTodo: () => { },
toogleAll: () => { },
deleteTodo: () => { },
deleteCompletedTodo: () => { },
updateTodo: () => { },
isError: false,
setIsError: () => { },
errorMessage: '',
setErrorMessage: () => { },
});

export const TodoProvider: React.FC<Props> = ({ children }) => {
const [todos, setTodos] = useState<Todo[] | []>([]);
const [isError, setIsError] = useState(false);
const [errorMessage, setErrorMessage] = useState('');

useEffect(() => {
if (USER_ID) {
getTodos(USER_ID)
.then(setTodos)
.catch((error) => {
throw error;
});
}
}, []);

const addTodo = (title: string) => {
const newTodo: Todo = {
id: +new Date(),
userId: USER_ID,
title,
completed: false,
};

setTodos([...todos, newTodo]);
};

const toggleTodo = (id: number) => {
const updatedTodos = [...todos];
const index = todos.findIndex(todo => todo.id === id);

if (index !== -1) {
updatedTodos[index].completed = !updatedTodos[index].completed;
}

setTodos(updatedTodos);
};

const toogleAll = () => {
const allCompleted = todos.every(todo => todo.completed === true);

const updatedTodos = todos.map(todo => ({
...todo,
completed: !allCompleted,
}));

setTodos(updatedTodos);
};

const deleteTodo = (id: number) => {
const updatedTodos = [...todos];
const index = todos.findIndex(todo => todo.id === id);

if (index !== -1) {
updatedTodos.splice(index, 1);
}

setTodos(updatedTodos);
};

const deleteCompletedTodo = () => {
const updatedTodos = todos.filter(todo => !todo.completed);

setTodos(updatedTodos);
};

const updateTodo = (updatedTitle: string, id: number) => {
const updatedTodos = [...todos];
const todoToUpdate = updatedTodos.find(todo => todo.id === id);

if (todoToUpdate) {
todoToUpdate.title = updatedTitle;
}

setTodos(updatedTodos);
};

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

const todosCompleted = useMemo(
() => todos.some(todo => todo.completed), [todos],
);

const contextValue: TodoContextValue = {
todos,
todosUncompleted,
todosCompleted,
addTodo,
toggleTodo,
toogleAll,
deleteTodo,
deleteCompletedTodo,
updateTodo,
isError,
setIsError,
errorMessage,
setErrorMessage,
};

if (!USER_ID) {
return <UserWarning />;
}

return (
<TodoContext.Provider value={contextValue}>
{children}
</TodoContext.Provider>
);
};

export const useTodo = (): TodoContextValue => React.useContext(TodoContext);
78 changes: 78 additions & 0 deletions src/components/TodoFooter/TodoFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import classNames from 'classnames';
import { Filter } from '../../types/Filter';
import { useTodo } from '../TodoContext/TodoContext';

type Props = {
selectedFilter: Filter,
setSelectedFilter: (filter: Filter) => void,
};

export const TodoFooter: React.FC<Props> = ({
selectedFilter,
setSelectedFilter,
}) => {
const {
todosCompleted,
todosUncompleted,
deleteCompletedTodo,
} = useTodo();

return (
<footer className="todoapp__footer">
<span className="todo-count">
{todosUncompleted === 1 ? (
'1 item left'
) : (
`${todosUncompleted} items left`
)}
</span>

{/* Active filter should have a 'selected' class */}
<nav className="filter">
<a
href="#/"
className={classNames(
'filter__link',
{ selected: selectedFilter === Filter.all },
)}
onClick={() => setSelectedFilter(Filter.all)}
>
All
</a>

<a
href="#/active"
className={classNames(
'filter__link',
{ selected: selectedFilter === Filter.active },
)}
onClick={() => setSelectedFilter(Filter.active)}
>
Active
</a>

<a
href="#/completed"
className={classNames(
'filter__link',
{ selected: selectedFilter === Filter.completed },
)}
onClick={() => setSelectedFilter(Filter.completed)}
>
Completed
</a>
</nav>

{todosCompleted && (
<button
type="button"
className="todoapp__clear-completed"
onClick={deleteCompletedTodo}
>
Clear completed
</button>
)}
</footer>
);
};
3 changes: 3 additions & 0 deletions src/components/TodoFooter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { TodoFooter } from './TodoFooter';

export default TodoFooter;
57 changes: 57 additions & 0 deletions src/components/TodoHeader/TodoHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React, { ChangeEvent, useState } from 'react';
import classNames from 'classnames';
import { useTodo } from '../TodoContext/TodoContext';

export const TodoHeader: React.FC = () => {
const {
todosUncompleted,
toogleAll,
addTodo,
setIsError,
setErrorMessage,
} = useTodo();
const [inputValue, setInputValue] = useState('');

const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};

const handleTodoAdd = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!inputValue.trim()) {
setIsError(true);
setErrorMessage('Unable to add a todo');
} else {
addTodo(inputValue);
}

setTimeout(() => {
setInputValue('');
}, 500);
};

return (
<header className="todoapp__header">
<button
type="button"
className={classNames(
'todoapp__toggle-all',
{ active: todosUncompleted },
)}
title="Togle All"
onClick={toogleAll}
/>

<form onSubmit={handleTodoAdd}>
<input
type="text"
className="todoapp__new-todo"
placeholder="What needs to be done?"
value={inputValue}
onChange={handleInputChange}
/>
</form>
</header>
);
};
3 changes: 3 additions & 0 deletions src/components/TodoHeader/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { TodoHeader } from './TodoHeader';

export default TodoHeader;
Loading

0 comments on commit bd4ddcd

Please sign in to comment.