Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Wesses committed Sep 19, 2023
1 parent 6532826 commit 43f1a3c
Show file tree
Hide file tree
Showing 23 changed files with 619 additions and 19 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://Wesses.github.io/react_todo-app-with-api/) and add it to the PR description.
89 changes: 72 additions & 17 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,79 @@
/* eslint-disable max-len */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import { UserWarning } from './UserWarning';
import React, {
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { getTodos } from './api/todos';

const USER_ID = 0;
import { Todo } from './types/Todo';
import { Status } from './types/Status';

import { TodoList } from './components/TodoList';
import { TodoHeader } from './components/TodoHeader';
import { TodoFooter } from './components/TodoFooter';
import { TodoError } from './components/TodoError';

import { USER_ID } from './utils/userId';
import { TodoContext } from './components/TodoContext';
import { ErrorContext } from './components/ErrorContext';

export const App: React.FC = () => {
if (!USER_ID) {
return <UserWarning />;
}
const { todos, setTodos } = useContext(TodoContext);
const { setError } = useContext(ErrorContext);
const [status, setStatus] = useState(Status.All);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [globalLoader, setGlobalLoader] = useState(false);

const filtredTodos = useMemo(() => todos.filter(({ completed }) => {
switch (status) {
case Status.Active:
return !completed;
case Status.Completed:
return completed;
default:
return true;
}
}), [status, todos]);

useEffect(() => {
setError('');
getTodos(USER_ID)
.then(setTodos)
.catch(() => {
setError('Unable to download todos');
});
}, []);

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>
<div className="todoapp">
<h1 className="todoapp__title">todos</h1>

<div className="todoapp__content">

<TodoHeader
onTempTodoAdd={setTempTodo}
tempTodo={tempTodo}
/>

{!!todos.length && (
<>
<TodoList
todos={filtredTodos}
tempTodo={tempTodo}
globalLoader={globalLoader}
/>
<TodoFooter
status={status}
onStatusChange={setStatus}
onGlobalLoaderChange={setGlobalLoader}
/>
</>
)}
</div>

<TodoError />

</div>
);
};
14 changes: 14 additions & 0 deletions src/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Todo } from '../types/Todo';
import { client } from '../utils/fetchClient';

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

export const addTodo = (userId: number, todo: Todo) => {
return client.post<Todo>(`/todos?userId=${userId}`, todo);
};

export const deleteTodo = (id: number) => {
return client.delete<Todo>(`/todos/${id}`);
};
31 changes: 31 additions & 0 deletions src/components/ErrorContext/ErrorContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { useEffect, useState } from 'react';

type TE = {
error: string;
setError: (error: string) => void;
};

const DEFAULT_ERRORCONTEXT: TE = {
error: '',
setError: () => {},
};

export const ErrorContext = React.createContext(DEFAULT_ERRORCONTEXT);

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

export const ErrorContextProvider: React.FC<Props> = ({ children }) => {
const [error, setError] = useState('');

useEffect(() => {
setTimeout(() => setError(''), 3000);
}, [error]);

return (
<ErrorContext.Provider value={{ error, setError }}>
{children}
</ErrorContext.Provider>
);
};
1 change: 1 addition & 0 deletions src/components/ErrorContext/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ErrorContext';
28 changes: 28 additions & 0 deletions src/components/TodoContext/TodoContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { useState } from 'react';
import { Todo } from '../../types/Todo';

type TC = {
todos: Todo[];
setTodos: (todos: Todo[] | ((prevState: Todo[]) => Todo[])) => void;
};

const DEFAULT_TODOSCONTEXT: TC = {
todos: [],
setTodos: () => {},
};

export const TodoContext = React.createContext(DEFAULT_TODOSCONTEXT);

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

export const TodoContextProvider: React.FC<Props> = ({ children }) => {
const [todos, setTodos] = useState<Todo[]>([]);

return (
<TodoContext.Provider value={{ todos, setTodos }}>
{children}
</TodoContext.Provider>
);
};
1 change: 1 addition & 0 deletions src/components/TodoContext/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TodoContext';
38 changes: 38 additions & 0 deletions src/components/TodoError/TodoError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import classNames from 'classnames';
import React, { useContext } from 'react';
import { ErrorContext } from '../ErrorContext';

export const TodoError: React.FC = () => {
const { error, setError } = useContext(ErrorContext);

return (
<div
className={classNames(
'notification',
'is-danger',
'is-light',
'has-text-weight-normal',
{
hidden: !error,
},
)}
>
{/* Notification is shown in case of any error */ }
{/* Add the 'hidden' class to hide the message smoothly */ }
<button
aria-label="delete-error-button"
type="button"
className="delete"
onClick={() => setError('')}
/>

{error}

{/* Unable to add a todo
<br />
Unable to delete a todo
<br />
Unable to update a todo */}
</div>
);
};
1 change: 1 addition & 0 deletions src/components/TodoError/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TodoError';
87 changes: 87 additions & 0 deletions src/components/TodoFooter/TodoFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import classNames from 'classnames';
import { useContext, useMemo } from 'react';
import { Status } from '../../types/Status';
import { TodoContext } from '../TodoContext';
import { deleteTodo } from '../../api/todos';
import { ErrorContext } from '../ErrorContext';

type Props = {
status: Status;
onStatusChange: (status: Status) => void;
onGlobalLoaderChange: (globalLoader: boolean) => void;
};

export const TodoFooter: React.FC<Props> = (props) => {
const {
status,
onStatusChange,
onGlobalLoaderChange,
} = props;

const { todos, setTodos } = useContext(TodoContext);
const { setError } = useContext(ErrorContext);

const uncomplitedTodos = useMemo(() => todos
.filter(({ completed }) => !completed), [todos]);

const handleComplDelete = () => {
onGlobalLoaderChange(true);

const deletedTodos: Promise<number>[] = [];

todos.forEach(({ id, completed }) => {
if (completed) {
deletedTodos.push(deleteTodo(id)
.then(() => id)
.catch(error => {
throw error;
}));
}
});

Promise.all(deletedTodos)
.then((res) => {
setTodos(prevState => prevState
.filter(todo => !res.includes(todo.id)));
})
.catch(() => setError('Unable to delete a todo'))
.finally(() => onGlobalLoaderChange(false));
};

return (
<footer className="todoapp__footer">
<span className="todo-count">
{`${uncomplitedTodos.length} item${uncomplitedTodos.length === 1 ? '' : 's'} left`}
</span>

<nav className="filter">
{Object.keys(Status).map((key) => {
const value = Status[key as keyof typeof Status];

return (
<a
key={key}
href={`#/${value}`}
className={classNames('filter__link', {
selected: value === status,
})}
onClick={() => onStatusChange(value)}
>
{key}
</a>
);
})}
</nav>

<button
type="button"
className="todoapp__clear-completed"
disabled={uncomplitedTodos.length === todos.length}
onClick={handleComplDelete}
>
Clear completed
</button>

</footer>
);
};
1 change: 1 addition & 0 deletions src/components/TodoFooter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TodoFooter';
75 changes: 75 additions & 0 deletions src/components/TodoHeader/TodoHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useContext, useEffect, useState } from 'react';
import { Todo } from '../../types/Todo';
import { USER_ID } from '../../utils/userId';
import { addTodo } from '../../api/todos';
import { TodoContext } from '../TodoContext';
import { ErrorContext } from '../ErrorContext';

type Props = {
onTempTodoAdd: (todo: Todo | null) => void;
tempTodo: Todo | null;
};

export const TodoHeader: React.FC<Props> = (props) => {
const {
onTempTodoAdd,
tempTodo,
} = props;

const { setError } = useContext(ErrorContext);
const { setTodos } = useContext(TodoContext);
const [title, setTitle] = useState('');

const handleSubmitForm = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();

if (!title) {
setError('Title can\'t be empty');

return;
}

onTempTodoAdd({
id: 0,
userId: USER_ID,
title,
completed: false,
});
};

useEffect(() => {
if (tempTodo) {
addTodo(USER_ID, tempTodo)
.then((res) => {
setTodos(prevState => [...prevState, res]);
setTitle('');
})
.catch(() => setError('Unable to add a todo'))
.finally(() => onTempTodoAdd(null));
}
}, [tempTodo]);

return (
<header className="todoapp__header">
{/* this buttons is active only if there are some active todos */}
<button
type="button"
className="todoapp__toggle-all active"
aria-label="button_toggle_active"
/>

<form
onSubmit={handleSubmitForm}
>
<input
disabled={!!tempTodo}
type="text"
className="todoapp__new-todo"
placeholder="What needs to be done?"
value={title}
onChange={event => setTitle(event.target.value)}
/>
</form>
</header>
);
};
1 change: 1 addition & 0 deletions src/components/TodoHeader/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TodoHeader';
Loading

0 comments on commit 43f1a3c

Please sign in to comment.