From 3a89bad95dd6e6d67e6f9c3f28246cca61f23241 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 24 Jan 2024 10:11:31 +0200 Subject: [PATCH] solution --- README.md | 4 +- src/App.tsx | 17 ++++-- src/components/NewMovie/NewMovie.tsx | 73 +++++++++++++++++++++----- src/components/TextField/TextField.tsx | 22 +++++--- 4 files changed, 93 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 6001d15be..59693daba 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ You have the `App` with the `MoviesList` and `NewMovie` form containing ready to use `TextField` components. Learn how it works and implement an ability to add movies from [IMDB](https://www.imdb.com/). -If you want to test your page you can get first image from a [movie page](https://www.imdb.com/title/tt1312171) using `DevTools` -> `Network` -> `Img` +If you want to test your page you can get first image from a [movie page](https://www.imdb.com/title/tt1312171) using `DevTools` -> `Network` -> `Img` > Here is [the demo page](https://mate-academy.github.io/react_movies-list-add-form/) @@ -30,4 +30,4 @@ const pattern = /^((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|( - 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 `` with your Github username in the [DEMO LINK](https://.github.io/react_movies-list-add-form/) and add it to the PR description. +- Replace `` with your Github username in the [DEMO LINK](https://BodyaRespect.github.io/react_movies-list-add-form/) and add it to the PR description. diff --git a/src/App.tsx b/src/App.tsx index 34be670b0..84cd2a511 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,27 @@ +import React, { useState } from 'react'; import './App.scss'; import { MoviesList } from './components/MoviesList'; import { NewMovie } from './components/NewMovie'; import moviesFromServer from './api/movies.json'; +import { Movie } from './types/Movie'; + +export const App: React.FC = () => { + const [movies, setMovies] = useState(moviesFromServer); + + const handleAddMovie = (movie: Movie) => { + setMovies(currentMovie => [ + ...currentMovie, + movie, + ]); + }; -export const App = () => { return (
- +
- {}} */ /> +
); diff --git a/src/components/NewMovie/NewMovie.tsx b/src/components/NewMovie/NewMovie.tsx index 34f22fb0a..3960cc6a4 100644 --- a/src/components/NewMovie/NewMovie.tsx +++ b/src/components/NewMovie/NewMovie.tsx @@ -1,45 +1,93 @@ -import { useState } from 'react'; +import React, { useState } from 'react'; import { TextField } from '../TextField'; +import { Movie } from '../../types/Movie'; -export const NewMovie = () => { - // Increase the count after successful form submission - // to reset touched status of all the `Field`s - const [count] = useState(0); +type Props = { + onAdd: (movie: Movie) => void, +}; + +const defaultMovie = { + title: '', + description: '', + imgUrl: '', + imdbUrl: '', + imdbId: '', +}; + +function urlValidation(str: string): boolean { + // eslint-disable-next-line max-len + const pattern = /^((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www\.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@,.\w_]*)#?(?:[,.!/\\\w]*))?)$/; + + return pattern.test(str); +} + +export const NewMovie: React.FC = ({ onAdd }) => { + const [count, setCount] = useState(0); + const [movie, setMovie] = useState(defaultMovie); + + const hasError = !movie.title.trim() + || !movie.imgUrl.trim() || !movie.imdbUrl.trim() || !movie.imdbId.trim() + || !urlValidation(movie.imgUrl) || !urlValidation(movie.imdbUrl); + + const handleInputChange = (key: string, value: string) => { + setMovie(prevInputs => ({ + ...prevInputs, + [key]: value, + })); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + onAdd(movie); + setCount(count + 1); + setMovie(defaultMovie); + }; return ( -
+

Add a movie

{}} + value={movie.title} + onChange={value => handleInputChange('title', value)} required /> handleInputChange('description', value)} /> handleInputChange('imgUrl', value)} + validate={urlValidation} /> handleInputChange('imdbUrl', value)} + validate={urlValidation} /> handleInputChange('imdbId', value)} + required />
@@ -48,6 +96,7 @@ export const NewMovie = () => { type="submit" data-cy="submit-button" className="button is-link" + disabled={hasError} > Add diff --git a/src/components/TextField/TextField.tsx b/src/components/TextField/TextField.tsx index 307b19865..5fdcfd2be 100644 --- a/src/components/TextField/TextField.tsx +++ b/src/components/TextField/TextField.tsx @@ -8,6 +8,7 @@ type Props = { placeholder?: string, required?: boolean, onChange?: (newValue: string) => void, + validate?: (value: string) => boolean, }; function getRandomDigits() { @@ -23,13 +24,20 @@ export const TextField: React.FC = ({ placeholder = `Enter ${label}`, required = false, onChange = () => {}, + validate, }) => { - // generage 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 [validationError, setValidationError] = useState(false); + + const handleBlur = () => { + setTouched(true); + setValidationError(validate !== undefined && !validate(value)); + }; + const hasError = touched && required && !value; + const showError = hasError || validationError; return (
@@ -43,17 +51,19 @@ export const TextField: React.FC = ({ id={id} data-cy={`movie-${name}`} className={classNames('input', { - 'is-danger': hasError, + 'is-danger': showError, })} placeholder={placeholder} value={value} onChange={event => onChange(event.target.value)} - onBlur={() => setTouched(true)} + onBlur={handleBlur} />
- {hasError && ( -

{`${label} is required`}

+ {showError && ( +

+ {hasError ? `${label} is required` : 'Invalid URL'} +

)}
);