Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@ Implement the ability to add TODOs to the `TodoList` implemented in the **Static

1. Create an `App` component storing the `todos` array and displaying it using the `TodoList`.
1. Create a form to add new TODOs:
- there should be a text input for the `title` with `data-cy="titleInput"` attribute;
- add a `<select>` with `data-cy="userSelect"` attribute showing all the given users;
- add labels and placeholders where they are needed;
- add a new todo to the list after clicking the `Add` button;
- each TODO item must have the following fields:
- there should be a text input for the `title` with `data-cy="titleInput"` attribute; +
- add a `<select>` with `data-cy="userSelect"` attribute showing all the given users; +
- add labels and placeholders where they are needed; +
- add a new todo to the list after clicking the `Add` button; +
- each TODO item must have the following fields: +
- `id`,
- `title`,
- `userId`,
- completed (`false` by default),
- and a user object containing: `id`, `name`, `username`, `email`
- `id` is the largest `id` in the array + 1 (add `data-id={todo.id}` attribute to each `.TodoInfo`).
- `id` is the largest `id` in the array + 1 (add `data-id={todo.id}` attribute to each `.TodoInfo`). +
1. Add validation to the form:
- add a default empty option `Choose a user` to the select;
- before creating a todo, check if a `user` was selected; if not, show an error message next to the `select` (`Please choose a user`);
- if the `title` is empty, show an error message next to the `title` field (`Please enter a title`);
- errors should appear only after clicking the `Add` button;
- hide the message immediately after any change of the field with an error;
1. If the form is valid, add a todo to the list and clear the form.
- add a default empty option `Choose a user` to the select; +
- before creating a todo, check if a `user` was selected; if not, show an error message next to the `select` (`Please choose a user`); +
- if the `title` is empty, show an error message next to the `title` field (`Please enter a title`); +
- errors should appear only after clicking the `Add` button;+
- hide the message immediately after any change of the field with an error; +
1. If the form is valid, add a todo to the list and clear the form. +
1. (* **Optional**) Allow entering only letters (`ua` and `en`), digits, and `spaces` in the `title` field. Just remove any other characters from the `title`.

## Instructions
Expand Down
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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": "^2.1.3",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
Expand Down
138 changes: 104 additions & 34 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,131 @@
import './App.scss';

// import usersFromServer from './api/users';
// import todosFromServer from './api/todos';
import usersFromServer from './api/users';
Comment thread
0-nira-0 marked this conversation as resolved.
import todosFromServer from './api/todos';
import { useState } from 'react';
import { TodoList } from './components/TodoList';

export interface ToDo {
id: number;
title: string;
completed: boolean;
userId: number;
user: User;
}
Comment thread
0-nira-0 marked this conversation as resolved.

export interface User {
id: number;
name: string;
username: string;
email: string;
}

export const App = () => {
const usersToDos = todosFromServer
.map(todo => {
return {
...todo,
user: usersFromServer.find(user => user.id === todo.userId),
};
})
.filter((todo): todo is ToDo => todo.user !== undefined);

const [title, setTitle] = useState('');
const [userId, setUserId] = useState(0);
const [visibleToDos, setVisibleToDos] = useState([...usersToDos]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This violates checklist item #3: "3. [CODE KNOWLEDGE] - If you are using a non-mutating array method, you don't need to create a copy of the array". You create an unnecessary copy when initializing state: useState([...usersToDos]). Use useState(usersToDos) instead to avoid an unneeded spread copy. Suggest changing the initialization at this line to const [visibleToDos, setVisibleToDos] = useState(usersToDos);


const [titleError, setTitleError] = useState(false);
const [userIdError, setUserIdError] = useState(false);

const userOfToDo = usersFromServer.find(user => user.id === userId);

function handleSubmision() {
if (title === '') {
setTitleError(() => true);
}

if (userId === 0) {
setUserIdError(() => true);
}

if (title !== '' && userId !== 0 && userOfToDo) {
setVisibleToDos(previousToDos => {
return [
...previousToDos,
{
id: previousToDos.reduce((max, t) => Math.max(max, t.id), 0) + 1,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This violates checklist item #1: "1. [CODE STYLE] - Don't use one-letter variable naming." The reducer uses a one-letter variable t in previousToDos.reduce((max, t) => Math.max(max, t.id), 0). Use a descriptive name such as todo or item to satisfy the checklist and improve readability.

title: title,
completed: false,
userId: userId,
user: userOfToDo,
},
Comment thread
0-nira-0 marked this conversation as resolved.
];
});

setTitle('');
setUserId(0);
}
}

return (
<div className="App">
<h1>Add todo form</h1>

<form action="/api/todos" method="POST">
<form
action="/api/todos"
method="POST"
onSubmit={event => {
handleSubmision();
event.preventDefault();
}}
>
<div className="field">
<input type="text" data-cy="titleInput" />
<span className="error">Please enter a title</span>
<label htmlFor="title-input">Title: </label>
<input
id="title-input"
type="text"
data-cy="titleInput"
placeholder={'Enter a title'}
value={title}
onChange={event => {
setTitle(event.target.value);
setTitleError(false);
}}
/>
{titleError && <span className="error">Please enter a title</span>}
</div>

<div className="field">
<select data-cy="userSelect">
<label htmlFor="user-select">User: </label>
<select
id="user-select"
data-cy="userSelect"
value={userId}
onChange={event => {
setUserId(+event.target.value);
setUserIdError(false);
}}
>
<option value="0" disabled>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor correctness issue: the default option uses a string value "0" while the select's value is the numeric userId (number). This mismatch may cause unexpected behavior. Use a numeric value for the default option (e.g. value={0}) so it consistently matches the numeric state, or make the controlled value a string consistently. Update the default option declaration on this line.

Choose a user
</option>

{usersFromServer.map(user => (
<option value={user.id} key={user.id}>
{user.name}
</option>
))}
</select>

<span className="error">Please choose a user</span>
{userIdError && <span className="error">Please choose a user</span>}
</div>

<button type="submit" data-cy="submitButton">
Add
</button>
</form>

<section className="TodoList">
<article data-id="1" className="TodoInfo TodoInfo--completed">
<h2 className="TodoInfo__title">delectus aut autem</h2>

<a className="UserInfo" href="mailto:Sincere@april.biz">
Leanne Graham
</a>
</article>

<article data-id="15" className="TodoInfo TodoInfo--completed">
<h2 className="TodoInfo__title">delectus aut autem</h2>

<a className="UserInfo" href="mailto:Sincere@april.biz">
Leanne Graham
</a>
</article>

<article data-id="2" className="TodoInfo">
<h2 className="TodoInfo__title">
quis ut nam facilis et officia qui
</h2>

<a className="UserInfo" href="mailto:Julianne.OConner@kory.org">
Patricia Lebsack
</a>
</article>
</section>
<TodoList todos={visibleToDos} />
Comment thread
0-nira-0 marked this conversation as resolved.
</div>
);
};
24 changes: 23 additions & 1 deletion src/components/TodoInfo/TodoInfo.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
export const TodoInfo = () => {};
import { UserInfo } from '../UserInfo';
import { ToDo } from '../../App';
import classNames from 'classnames';

type Props = {
todo: ToDo;
};

export const TodoInfo: React.FC<Props> = ({ todo }) => {
const { user, id, completed, title } = todo;

return (
<article
data-id={id}
className={classNames('TodoInfo', {
'TodoInfo--completed': completed,
})}
>
<h2 className="TodoInfo__title">{title}</h2>
{user && <UserInfo user={user} />}
</article>
);
};
17 changes: 16 additions & 1 deletion src/components/TodoList/TodoList.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
export const TodoList = () => {};
import { ToDo } from '../../App';
import { TodoInfo } from '../TodoInfo';

type Props = {
todos: ToDo[];
};

export const TodoList: React.FC<Props> = ({ todos }) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential type/symbol error: the component is typed as React.FC<Props> but React is not imported in this file. Import React or the FC type to make the annotation explicit. For example: import React from 'react'; or import type { FC } from 'react'; and then use export const TodoList: FC<Props> = ({ todos }) => { ... }. This prevents issues in projects where React is not available as a global type.

return (
<section className="TodoList">
{todos.map(todo => (
<TodoInfo key={todo.id} todo={todo} />
))}
</section>
);
};
14 changes: 13 additions & 1 deletion src/components/UserInfo/UserInfo.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
export const UserInfo = () => {};
import { User } from '../../App';

type Props = {
user: User;
};

export const UserInfo: React.FC<Props> = ({ user }) => {
return (
<a className="UserInfo" href={`mailto:${user.email}`}>
{user.name}
</a>
);
};