Skip to content

Commit

Permalink
add task solution
Browse files Browse the repository at this point in the history
  • Loading branch information
zhudim committed Jan 4, 2025
1 parent 44b1d40 commit 7fedbc3
Show file tree
Hide file tree
Showing 15 changed files with 197 additions and 164 deletions.
119 changes: 16 additions & 103 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,117 +1,30 @@
import './App.scss';

import { useState } from 'react';
import usersFromServer from './api/users';
import todosFromServer from './api/todos';
import { useState } from 'react';
import { FormData, Todo } from './types';
import { TodoList } from './components/TodoList';
import { getNewTodoId } from './services';
import { getUserById } from './utils';
import { TodoList } from './components/TodoList/TodoList';
import { NewTodoForm } from './components/NewTodoForm';

const initialForm: FormData = {
title: '',
userId: 0,
};
const todoRoster = todosFromServer.map(todo => ({
...todo,
user: getUserById(usersFromServer, todo.userId),
}));

export const App = () => {
const [todos, setTodos] = useState<Todo[]>(todosFromServer);
const [formData, setFormData] = useState(initialForm);
const [isTitleError, setIsTitleError] = useState(false);
const [isUserError, setIsUserError] = useState(false);

const { title, userId } = formData;

const isDisabledAdd = !title.trim() || !userId;

const reset = () => {
setFormData({ ...initialForm });
};

const handleAddTodo = (inputData: FormData) => {
const newTodo = {
...inputData,
id: getNewTodoId(todos),
completed: false,
};

setTodos(currentTodos => [...currentTodos, newTodo]);
};

const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prevData => ({
...prevData,
title: event.target.value,
}));
setIsTitleError(false);
};

const handleUserChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setFormData(prevData => ({
...prevData,
userId: +event.target.value,
}));
setIsUserError(false);
};

const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
if (isDisabledAdd) {
setIsTitleError(!title);
setIsUserError(!userId);

return;
}

handleAddTodo(formData);
reset();
};
const [todos, setTodos] = useState(todoRoster);

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

<form onSubmit={handleSubmit}>
<div className="field">
<label className="label" htmlFor="title">
Title:
</label>
<input
id="title"
type="text"
data-cy="titleInput"
placeholder="Enter title"
value={title}
onChange={handleTitleChange}
/>
{isTitleError && <span className="error">Please enter a title</span>}
</div>

<div className="field">
<label className="label" htmlFor="user">
User:
</label>
<select
id="user"
data-cy="userSelect"
value={userId}
onChange={handleUserChange}
>
<option value="0" disabled>
Choose a user
</option>
{usersFromServer.map(user => (
<option key={user.id} value={user.id}>
{user.name}
</option>
))}
</select>

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

<button type="submit" data-cy="submitButton">
Add
</button>
</form>
<NewTodoForm
todos={todos}
users={usersFromServer}
handleAdd={newTodo => {
setTodos(prevTodos => [...prevTodos, newTodo]);
}}
/>

<TodoList todos={todos} />
</div>
Expand Down
101 changes: 101 additions & 0 deletions src/components/NewTodoForm/NewTodoForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { useState } from 'react';
import { Todo, User } from '../../types';
import { createNewId, getUserById, removeInvalidCharacters } from '../../utils';

type Props = {
todos: Todo[];
users: User[];
handleAdd: (todo: Todo) => void;
};

export const NewTodoForm: React.FC<Props> = ({ todos, users, handleAdd }) => {
const [title, setTitle] = useState('');
const [hasTitleInputError, setHasTitleInputError] = useState(false);
const [userId, setUserId] = useState(0);
const [hasUserSelectError, setHasUserSelectError] = useState(false);

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();

if (!title) {
setHasTitleInputError(true);
}

if (!userId) {
setHasUserSelectError(true);
}

if (title && userId) {
setTitle('');
setUserId(0);
handleAdd({
id: createNewId(todos),
title: removeInvalidCharacters(title),
completed: false,
userId,
user: getUserById(users, userId),
});
}
}

function handleTitleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
setTitle(event.target.value);
setHasTitleInputError(false);
}

function handleUserSelect(event: React.ChangeEvent<HTMLSelectElement>) {
setUserId(Number(event.target.value));
setHasUserSelectError(false);
}

return (
<form action="/api/todos" method="POST" onSubmit={handleSubmit}>
<div className="field">
<label>
{`Title: `}
<input
type="text"
name="title"
placeholder="Enter a title"
value={title}
onChange={handleTitleInputChange}
data-cy="titleInput"
/>
</label>
{hasTitleInputError && (
<span className="error">Please enter a title</span>
)}
</div>

<div className="field">
<label>
{`User: `}
<select
name="user"
required
value={userId}
onChange={handleUserSelect}
data-cy="userSelect"
>
<option value="0" disabled>
Choose a user
</option>
{users.map(user => (
<option value={user.id} key={user.id}>
{user.name}
</option>
))}
</select>
</label>

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

<button type="submit" data-cy="submitButton">
Add
</button>
</form>
);
};
1 change: 1 addition & 0 deletions src/components/NewTodoForm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './NewTodoForm';
33 changes: 13 additions & 20 deletions src/components/TodoInfo/TodoInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
import { getUserById } from '../../services';
import React from 'react';
import classNames from 'classnames';
import { Todo } from '../../types';
import { UserInfo } from '../UserInfo';

import classNames from 'classnames';

type Props = {
todo: Todo;
};

export const TodoInfo: React.FC<Props> = ({ todo }) => {
const user = getUserById(todo.userId);

return (
<article
data-id={todo.id}
className={classNames('TodoInfo', {
'TodoInfo--completed': todo.completed,
})}
key={todo.id}
>
<h2 className="TodoInfo__title">{todo.title}</h2>

{user && <UserInfo user={user} />}
</article>
);
};
export const TodoInfo: React.FC<Props> = ({ todo }) => (
<article
data-id={todo.id}
className={classNames('TodoInfo', {
'TodoInfo--completed': todo.completed,
})}
>
<h2 className="TodoInfo__title">{todo.title}</h2>
{todo.user && <UserInfo user={todo.user} />}
</article>
);
17 changes: 8 additions & 9 deletions src/components/TodoList/TodoList.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import React from 'react';
import { Todo } from '../../types';
import { TodoInfo } from '../TodoInfo';

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

export const TodoList: React.FC<Props> = ({ todos }) => {
return (
<section className="TodoList">
{todos.map(todo => (
<TodoInfo todo={todo} key={todo.id} />
))}
</section>
);
};
export const TodoList: React.FC<Props> = ({ todos }) => (
<section className="TodoList">
{todos.map(todo => (
<TodoInfo todo={todo} key={todo.id} />
))}
</section>
);
13 changes: 6 additions & 7 deletions src/components/UserInfo/UserInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React from 'react';
import { User } from '../../types';

type Props = {
user: User;
};

export const UserInfo: React.FC<Props> = ({ user }) => {
return (
<a className="UserInfo" href={`mailto:${user.email}`}>
{user.name}
</a>
);
};
export const UserInfo: React.FC<Props> = ({ user }) => (
<a className="UserInfo" href={`mailto:${user.email}`}>
{user.name}
</a>
);
10 changes: 0 additions & 10 deletions src/services.ts

This file was deleted.

15 changes: 0 additions & 15 deletions src/types.ts

This file was deleted.

9 changes: 9 additions & 0 deletions src/types/Todo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { User } from './User';

export interface Todo {
id: number;
title: string;
completed: boolean;
userId: number;
user: User | null;
}
6 changes: 6 additions & 0 deletions src/types/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface User {
id: number;
name: string;
username: string;
email: string;
}
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Todo';
export * from './User';
9 changes: 9 additions & 0 deletions src/utils/createNewId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Todo } from '../types';

export function createNewId(todos: Todo[]) {
const maxId = todos.reduce((acc: number, todo: Todo) => {
return Math.max(todo.id, acc);
}, 0);

return maxId + 1;
}
5 changes: 5 additions & 0 deletions src/utils/getUserById.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { User } from '../types';

export function getUserById(users: User[], userId: number) {
return users.find(user => user.id === userId) || null;
}
3 changes: 3 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './createNewId';
export * from './getUserById';
export * from './removeInvalidCharacters';
Loading

0 comments on commit 7fedbc3

Please sign in to comment.