Skip to content

Commit

Permalink
add solution
Browse files Browse the repository at this point in the history
  • Loading branch information
Denys committed Feb 21, 2025
1 parent 9663976 commit 35e0a9f
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 60 deletions.
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.9.12",
"@mate-academy/scripts": "^2.1.0",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
Expand Down
21 changes: 19 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,32 @@ import './App.scss';
import { MoviesList } from './components/MoviesList';
import { NewMovie } from './components/NewMovie';
import moviesFromServer from './api/movies.json';
import { useState } from 'react';

// Тип для Movie
interface Movie {
title: string;
description: string;
imgUrl: string;
imdbUrl: string;
imdbId: string;
}

export const App = () => {
const [movies, setMovies] = useState<Movie[]>(moviesFromServer);

// Указываем тип newMovie как Movie
const handleAddMovie = (newMovie: Movie) => {
setMovies(prevMovies => [...prevMovies, newMovie]);
};

return (
<div className="page">
<div className="page-content">
<MoviesList movies={moviesFromServer} />
<MoviesList movies={movies} />
</div>
<div className="sidebar">
<NewMovie /* onAdd={(movie) => {}} */ />
<NewMovie onAdd={handleAddMovie} />
</div>
</div>
);
Expand Down
173 changes: 157 additions & 16 deletions src/components/NewMovie/NewMovie.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,180 @@
import { useState } from 'react';
import React, { useState } from 'react';
import { TextField } from '../TextField';

export const NewMovie = () => {
// Increase the count after successful form submission
// to reset touched status of all the `Field`s
const [count] = useState(0);
// Тип для Movie
interface Movie {
title: string;
description: string;
imgUrl: string;
imdbUrl: string;
imdbId: string;
}

const pattern =
// eslint-disable-next-line max-len
/^((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www\.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@,.\w_]*)#?(?:[,.!/\\\w]*))?)$/;

export const NewMovie = ({ onAdd }: { onAdd: (newMovie: Movie) => void }) => {
// Состояния для полей формы
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [imgUrl, setImgUrl] = useState('');
const [imdbUrl, setImdbUrl] = useState('');
const [imdbId, setImdbId] = useState('');

// Объект ошибок
const [errors, setErrors] = useState<{ [key: string]: string }>({});

// Функция для валидации URL
const validateUrl = (url: string) => pattern.test(url);

// Функция обработки потери фокуса (onBlur)
const handleBlur = (field: string) => {
switch (field) {
case 'imgUrl':
if (!validateUrl(imgUrl)) {
setErrors(prevErrors => ({
...prevErrors,
imgUrlError: 'Неверный URL изображения',
}));
} else {
setErrors(prevErrors => {
const { imgUrlError, ...rest } = prevErrors;

return rest;
});
}

break;
case 'imdbUrl':
if (!validateUrl(imdbUrl)) {
setErrors(prevErrors => ({
...prevErrors,
imdbUrlError: 'Неверный URL IMDb',
}));
} else {
setErrors(prevErrors => {
const { imdbUrlError, ...rest } = prevErrors;

return rest;
});
}

break;
case 'imdbId':
if (!imdbId) {
setErrors(prevErrors => ({
...prevErrors,
imdbIdError: 'Необходимо указать IMDb ID',
}));
} else {
setErrors(prevErrors => {
const { imdbIdError, ...rest } = prevErrors;

return rest;
});
}

break;
default:
break;
}
};

// Функция для проверки, валидна ли форма
const isFormValid =
title.trim() &&
imgUrl.trim() &&
imdbUrl.trim() &&
imdbId.trim() &&
Object.keys(errors).length === 0;

// Функция для отправки формы
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();

// Проверка валидности формы
if (isFormValid) {
const newMovie: Movie = {
title,
description,
imgUrl,
imdbUrl,
imdbId,
};

// Передаем новый фильм в родительский компонент
onAdd(newMovie);

// Очищаем поля формы после добавления фильма
setTitle('');
setDescription('');
setImgUrl('');
setImdbUrl('');
setImdbId('');
setErrors({});
}
};

return (
<form className="NewMovie" key={count}>
<h2 className="title">Add a movie</h2>
<form className="NewMovie" onSubmit={handleSubmit}>
<h2 className="title">Добавить фильм</h2>

<TextField
name="title"
label="Title"
value=""
onChange={() => {}}
label="Название"
value={title}
onChange={e => setTitle(e.target.value)}
onBlur={() => handleBlur('title')}
required
error={errors.title}
/>

<TextField name="description" label="Description" value="" />
<TextField
name="description"
label="Описание"
value={description}
onChange={e => setDescription(e.target.value)}
/>

<TextField name="imgUrl" label="Image URL" value="" />
<TextField
name="imgUrl"
label="URL изображения"
value={imgUrl}
onChange={e => setImgUrl(e.target.value)}
onBlur={() => handleBlur('imgUrl')}
required
error={errors.imgUrlError}
/>

<TextField name="imdbUrl" label="Imdb URL" value="" />
<TextField
name="imdbUrl"
label="URL IMDb"
value={imdbUrl}
onChange={e => setImdbUrl(e.target.value)}
onBlur={() => handleBlur('imdbUrl')}
required
error={errors.imdbUrlError}
/>

<TextField name="imdbId" label="Imdb ID" value="" />
<TextField
name="imdbId"
label="IMDb ID"
value={imdbId}
onChange={e => setImdbId(e.target.value)}
onBlur={() => handleBlur('imdbId')}
required
error={errors.imdbIdError}
/>

<div className="field is-grouped">
<div className="control">
<button
type="submit"
data-cy="submit-button"
className="button is-link"
disabled={!isFormValid}
>
Add
Добавить
</button>
</div>
</div>
Expand Down
57 changes: 20 additions & 37 deletions src/components/TextField/TextField.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,39 @@
import classNames from 'classnames';
import React, { useState } from 'react';
import React from 'react';

type Props = {
interface TextFieldProps {
name: string;
label: string;
value: string;
label?: string;
placeholder?: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onBlur?: () => void;
required?: boolean;
onChange?: (newValue: string) => void;
};

function getRandomDigits() {
return Math.random().toFixed(16).slice(2);
error?: string;
}

export const TextField: React.FC<Props> = ({
export const TextField: React.FC<TextFieldProps> = ({
name,
label,
value,
label = name,
placeholder = `Enter ${label}`,
required = false,
onChange = () => {},
onChange,
onBlur,
required,
error,
}) => {
// generate a unique id once on component load
const [id] = useState(() => `${name}-${getRandomDigits()}`);

// To show errors only if the field was touched (onBlur)
const [touched, setTouched] = useState(false);
const hasError = touched && required && !value;

return (
<div className="field">
<label className="label" htmlFor={id}>
{label}
</label>

<div className={`field ${error ? 'is-danger' : ''}`}>
<label className="label">{label}</label>
<div className="control">
<input
className={`input ${error ? 'is-danger' : ''}`}
type="text"
id={id}
data-cy={`movie-${name}`}
className={classNames('input', {
'is-danger': hasError,
})}
placeholder={placeholder}
name={name}
value={value}
onChange={event => onChange(event.target.value)}
onBlur={() => setTouched(true)}
onChange={onChange}
onBlur={onBlur}
required={required}
/>
</div>

{hasError && <p className="help is-danger">{`${label} is required`}</p>}
{error && <p className="help is-danger">{error}</p>}
</div>
);
};

0 comments on commit 35e0a9f

Please sign in to comment.