Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
dynamic-list-of-todos
Browse files Browse the repository at this point in the history
Alex-redman committed Jan 29, 2025
1 parent 8bf0f2b commit fb519c9
Showing 5 changed files with 185 additions and 115 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -29,4 +29,4 @@ loaded and show them using `TodoList` (check the code in the `api.ts`);
- 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_dynamic-list-of-todos/) and add it to the PR description.
- Replace `<your_account>` with your Github username in the [DEMO LINK](https://Alex-redman.github.io/react_dynamic-list-of-todos/) and add it to the PR description.
62 changes: 53 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,78 @@
/* eslint-disable max-len */
import React from 'react';
import React, { useEffect, useState } from 'react';
import 'bulma/css/bulma.css';
import '@fortawesome/fontawesome-free/css/all.css';

import { TodoList } from './components/TodoList';
import { TodoFilter } from './components/TodoFilter';
import { TodoModal } from './components/TodoModal';
import { Loader } from './components/Loader';
import { Todo } from './types/Todo';
import { getTodos } from './api';

export const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [filteredTodos, setFilteredTodos] = useState<Todo[]>([]);
const [selectedTodo, setSelectedTodo] = useState<Todo | null>(null);
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState('');
const [status, setStatus] = useState<'all' | 'completed' | 'active'>('all');

useEffect(() => {
setLoading(true);
getTodos()
.then(data => {
setTodos(data);
setFilteredTodos(data);
})
.finally(() => setLoading(false));
}, []);

useEffect(() => {
let result = todos;

if (status !== 'all') {
result = todos.filter(
todo => todo.completed === (status === 'completed'),
);
}

if (query) {
result = result.filter(todo =>
todo.title.toLowerCase().includes(query.toLowerCase()),
);
}

setFilteredTodos(result);
}, [todos, status, query]);

return (
<>
<div className="section">
<div className="container">
<div className="box">
<h1 className="title">Todos:</h1>

<div className="block">
<TodoFilter />
<TodoFilter
query={query}
setQuery={setQuery}
status={status}
setStatus={setStatus}
/>
</div>

<div className="block">
<Loader />
<TodoList />
{loading && <Loader />}
<TodoList
todos={filteredTodos}
selectedTodoId={selectedTodo?.id ?? null}
onSelectTodo={setSelectedTodo}
/>
</div>
</div>
</div>
</div>

<TodoModal />
{selectedTodo && (
<TodoModal todo={selectedTodo} onClose={() => setSelectedTodo(null)} />
)}
</>
);
};
41 changes: 34 additions & 7 deletions src/components/TodoFilter/TodoFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
export const TodoFilter = () => (
import React from 'react';

interface Props {
query: string;
setQuery: (value: string) => void;
status: 'all' | 'completed' | 'active';
setStatus: (value: 'all' | 'completed' | 'active') => void;
}

export const TodoFilter: React.FC<Props> = ({
query,
setQuery,
status,
setStatus,
}) => (
<form className="field has-addons">
<p className="control">
<span className="select">
<select data-cy="statusSelect">
<select
data-cy="statusSelect"
value={status}
onChange={e =>
setStatus(e.target.value as 'all' | 'completed' | 'active')
}
>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
@@ -16,15 +36,22 @@ export const TodoFilter = () => (
type="text"
className="input"
placeholder="Search..."
value={query}
onChange={e => setQuery(e.target.value)}
/>
<span className="icon is-left">
<i className="fas fa-magnifying-glass" />
</span>

<span className="icon is-right" style={{ pointerEvents: 'all' }}>
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<button data-cy="clearSearchButton" type="button" className="delete" />
</span>
{query && (
<span className="icon is-right" style={{ pointerEvents: 'all' }}>
<button
data-cy="clearSearchButton"
type="button"
className="delete"
onClick={() => setQuery('')}
/>
</span>
)}
</p>
</form>
);
134 changes: 54 additions & 80 deletions src/components/TodoList/TodoList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import React from 'react';
import { Todo } from '../../types/Todo';

export const TodoList: React.FC = () => (
interface Props {
todos: Todo[];
selectedTodoId: number | null;
onSelectTodo: (todo: Todo | null) => void;
}

export const TodoList: React.FC<Props> = ({
todos,
selectedTodoId,
onSelectTodo,
}) => (
<table className="table is-narrow is-fullwidth">
<thead>
<tr>
@@ -14,87 +25,50 @@ export const TodoList: React.FC = () => (
<th> </th>
</tr>
</thead>

<tbody>
<tr data-cy="todo" className="">
<td className="is-vcentered">1</td>
<td className="is-vcentered" />
<td className="is-vcentered is-expanded">
<p className="has-text-danger">delectus aut autem</p>
</td>
<td className="has-text-right is-vcentered">
<button data-cy="selectButton" className="button" type="button">
<span className="icon">
<i className="far fa-eye" />
</span>
</button>
</td>
</tr>
<tr data-cy="todo" className="has-background-info-light">
<td className="is-vcentered">2</td>
<td className="is-vcentered" />
<td className="is-vcentered is-expanded">
<p className="has-text-danger">quis ut nam facilis et officia qui</p>
</td>
<td className="has-text-right is-vcentered">
<button data-cy="selectButton" className="button" type="button">
<span className="icon">
<i className="far fa-eye-slash" />
</span>
</button>
</td>
</tr>

<tr data-cy="todo" className="">
<td className="is-vcentered">1</td>
<td className="is-vcentered" />
<td className="is-vcentered is-expanded">
<p className="has-text-danger">delectus aut autem</p>
</td>
<td className="has-text-right is-vcentered">
<button data-cy="selectButton" className="button" type="button">
<span className="icon">
<i className="far fa-eye" />
</span>
</button>
</td>
</tr>
{todos.map(todo => {
const isSelected = todo.id === selectedTodoId;

<tr data-cy="todo" className="">
<td className="is-vcentered">6</td>
<td className="is-vcentered" />
<td className="is-vcentered is-expanded">
<p className="has-text-danger">
qui ullam ratione quibusdam voluptatem quia omnis
</p>
</td>
<td className="has-text-right is-vcentered">
<button data-cy="selectButton" className="button" type="button">
<span className="icon">
<i className="far fa-eye" />
</span>
</button>
</td>
</tr>

<tr data-cy="todo" className="">
<td className="is-vcentered">8</td>
<td className="is-vcentered">
<span className="icon" data-cy="iconCompleted">
<i className="fas fa-check" />
</span>
</td>
<td className="is-vcentered is-expanded">
<p className="has-text-success">quo adipisci enim quam ut ab</p>
</td>
<td className="has-text-right is-vcentered">
<button data-cy="selectButton" className="button" type="button">
<span className="icon">
<i className="far fa-eye" />
</span>
</button>
</td>
</tr>
return (
<tr
key={todo.id}
data-cy="todo"
className={todo.completed ? 'has-background-info-light' : ''}
>
<td className="is-vcentered">{todo.id}</td>
<td className="is-vcentered">
{todo.completed && (
<span className="icon" data-cy="iconCompleted">
<i className="fas fa-check" />
</span>
)}
</td>
<td className="is-vcentered is-expanded">
<p
className={
todo.completed ? 'has-text-success' : 'has-text-danger'
}
>
{todo.title}
</p>
</td>
<td className="has-text-right is-vcentered">
<button
onClick={() => onSelectTodo(isSelected ? null : todo)}
data-cy="selectButton"
className="button"
type="button"
>
<span className="icon">
<i
className={`far ${isSelected ? 'fa-eye-slash' : 'fa-eye'}`}
/>
</span>
</button>
</td>
</tr>
);
})}
</tbody>
</table>
);
61 changes: 43 additions & 18 deletions src/components/TodoModal/TodoModal.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Loader } from '../Loader';
import { User } from '../../types/User';
import { Todo } from '../../types/Todo';
import { getUser } from '../../api';

export const TodoModal: React.FC = () => {
return (
<div className="modal is-active" data-cy="modal">
<div className="modal-background" />
interface Props {
todo: Todo;
onClose: () => void;
}

export const TodoModal: React.FC<Props> = ({ todo, onClose }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);

useEffect(() => {
setLoading(true);
getUser(todo.userId)
.then(setUser)
.finally(() => setLoading(false));
}, [todo.userId]);

{true ? (
return (
<div
className={`modal ${todo ? 'is-active' : ''}`}
data-cy="modal"
aria-hidden={!todo}
>
<div className="modal-background" onClick={onClose} />
{loading ? (
<Loader />
) : (
<div className="modal-card">
@@ -15,25 +36,29 @@ export const TodoModal: React.FC = () => {
className="modal-card-title has-text-weight-medium"
data-cy="modal-header"
>
Todo #2
Todo #{todo.id}
</div>

{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<button type="button" className="delete" data-cy="modal-close" />
<button
onClick={onClose}
type="button"
className="delete"
data-cy="modal-close"
/>
</header>

<div className="modal-card-body">
<p className="block" data-cy="modal-title">
quis ut nam facilis et officia qui
{todo.title}
</p>

<p className="block" data-cy="modal-user">
{/* <strong className="has-text-success">Done</strong> */}
<strong className="has-text-danger">Planned</strong>

<strong
className={
todo.completed ? 'has-text-success' : 'has-text-danger'
}
>
{todo.completed ? 'Done' : 'Planned'}
</strong>
{' by '}

<a href="mailto:Sincere@april.biz">Leanne Graham</a>
{user && <a href={`mailto:${user.email}`}>{user.name}</a>}
</p>
</div>
</div>

0 comments on commit fb519c9

Please sign in to comment.