From 968a7cbb50ecb8682acc8ac69ad6fac64e50d9ef Mon Sep 17 00:00:00 2001 From: Vladyslav Kolisnyk Date: Sun, 22 Oct 2023 00:30:28 +0300 Subject: [PATCH 1/4] add task --- README.md | 4 +- src/App.tsx | 11 +++- src/components/NewMovie/NewMovie.tsx | 78 +++++++++++++++++++++----- src/components/TextField/TextField.tsx | 32 ++++++++--- 4 files changed, 101 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 6001d15be..70943b47d 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://VladyslavKolisnyk.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..7ad24da06 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,23 @@ import './App.scss'; +import { useState } from 'react'; import { MoviesList } from './components/MoviesList'; import { NewMovie } from './components/NewMovie'; import moviesFromServer from './api/movies.json'; +import { Movie } from './types/Movie'; + export const App = () => { + const [allMovies, setAllMovies] = useState(moviesFromServer); + return (
- +
- {}} */ /> + setAllMovies([...allMovies, movie])} + />
); diff --git a/src/components/NewMovie/NewMovie.tsx b/src/components/NewMovie/NewMovie.tsx index 34f22fb0a..ebc4f5444 100644 --- a/src/components/NewMovie/NewMovie.tsx +++ b/src/components/NewMovie/NewMovie.tsx @@ -1,45 +1,96 @@ import { 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); +interface Props { + onAdd: (movie: Movie) => void; +} + +export const NewMovie: React.FC = ({ onAdd }) => { + const [count, setCount] = useState(0); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [imageUrl, setImageUrl] = useState(''); + const [imdbUrl, setImdbUrl] = useState(''); + const [imdbId, setImdbId] = useState(''); + + const checkInput = () => { + // 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]*))?)$/; + const allInputReceived + = name.trim() && imageUrl.trim() && imdbUrl.trim() && imdbId.trim(); + const regexMatched = imdbUrl.match(pattern) && imageUrl.match(pattern); + + return allInputReceived && regexMatched; + }; + + const clear = () => { + setName(''); + setDescription(''); + setImageUrl(''); + setImdbUrl(''); + setImdbId(''); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + if (checkInput()) { + onAdd({ + title: name, + description, + imgUrl: imageUrl, + imdbUrl, + imdbId, + }); + clear(); + setCount(count + 1); + } + }; return ( -
+

Add a movie

{}} + value={name} + onChange={setName} required />
@@ -48,6 +99,7 @@ export const NewMovie = () => { type="submit" data-cy="submit-button" className="button is-link" + disabled={!checkInput()} > Add diff --git a/src/components/TextField/TextField.tsx b/src/components/TextField/TextField.tsx index 307b19865..90a81787b 100644 --- a/src/components/TextField/TextField.tsx +++ b/src/components/TextField/TextField.tsx @@ -24,12 +24,30 @@ export const TextField: React.FC = ({ required = false, onChange = () => {}, }) => { - // generage a unique id once on component load + // 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]*))?)$/; const [id] = useState(() => `${name}-${getRandomDigits()}`); - + const [errorMessage, setErrorMessage] = useState(`${label} is required`); // To show errors only if the field was touched (onBlur) const [touched, setTouched] = useState(false); - const hasError = touched && required && !value; + const hasError = () => { + if ((name === 'imgUrl' || name === 'imdbUrl') && value) { + return touched && required && !value.match(pattern); + } + + return touched && required && !value; + }; + + const handleOnBlur = () => { + if ((name === 'imgUrl' || name === 'imdbUrl') + && value && !value.match(pattern)) { + setErrorMessage('Wrong URL input'); + } else { + setErrorMessage(`${label} is required`); + } + + setTouched(true); + }; return (
@@ -43,17 +61,17 @@ export const TextField: React.FC = ({ id={id} data-cy={`movie-${name}`} className={classNames('input', { - 'is-danger': hasError, + 'is-danger': hasError(), })} placeholder={placeholder} value={value} onChange={event => onChange(event.target.value)} - onBlur={() => setTouched(true)} + onBlur={handleOnBlur} />
- {hasError && ( -

{`${label} is required`}

+ {hasError() && ( +

{errorMessage}

)}
); From 143871b0b73dfdbee97deb0c2b0f3260f7707039 Mon Sep 17 00:00:00 2001 From: Vladyslav Kolisnyk Date: Sun, 22 Oct 2023 18:44:55 +0300 Subject: [PATCH 2/4] add --- src/components/NewMovie/NewMovie.tsx | 109 ++++++++++++++------------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/src/components/NewMovie/NewMovie.tsx b/src/components/NewMovie/NewMovie.tsx index ebc4f5444..0b4a3cb8b 100644 --- a/src/components/NewMovie/NewMovie.tsx +++ b/src/components/NewMovie/NewMovie.tsx @@ -1,96 +1,99 @@ -import { useState } from 'react'; +import React, { useState } from 'react'; import { TextField } from '../TextField'; import { Movie } from '../../types/Movie'; -interface Props { +type MovieProps = { onAdd: (movie: Movie) => void; -} +}; -export const NewMovie: React.FC = ({ onAdd }) => { +export const NewMovie = ({ onAdd }: MovieProps) => { const [count, setCount] = useState(0); - const [name, setName] = useState(''); - const [description, setDescription] = useState(''); - const [imageUrl, setImageUrl] = useState(''); - const [imdbUrl, setImdbUrl] = useState(''); - const [imdbId, setImdbId] = useState(''); - - const checkInput = () => { - // 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]*))?)$/; - const allInputReceived - = name.trim() && imageUrl.trim() && imdbUrl.trim() && imdbId.trim(); - const regexMatched = imdbUrl.match(pattern) && imageUrl.match(pattern); - - return allInputReceived && regexMatched; + + const [formValues, setFormValues] = useState({ + title: '', + description: '', + imgUrl: '', + imdbUrl: '', + imdbId: '', + }); + + const isButtonDisabled = () => { + const { + title, + imgUrl, + imdbUrl, + imdbId, + } = formValues; + + return !(title.trim() && imgUrl.trim() + && imdbUrl.trim() && imdbId.trim()); }; - const clear = () => { - setName(''); - setDescription(''); - setImageUrl(''); - setImdbUrl(''); - setImdbId(''); + const handleChange = (value: string, name: string) => { + setFormValues((prevState) => ({ + ...prevState, + [name]: value, + })); }; - const handleSubmit = (event: React.FormEvent) => { + const reset = () => { + setFormValues({ + title: '', + description: '', + imgUrl: '', + imdbUrl: '', + imdbId: '', + }); + }; + + const handleSubmit = ((event: React.FormEvent) => { event.preventDefault(); - if (checkInput()) { - onAdd({ - title: name, - description, - imgUrl: imageUrl, - imdbUrl, - imdbId, - }); - clear(); - setCount(count + 1); - } - }; + onAdd(formValues); + + setCount(count + 1); + reset(); + }); return ( - +

Add a movie

handleChange(value, 'title')} required /> handleChange(value, 'description')} /> handleChange(value, 'imgUrl')} required - onChange={setImageUrl} /> handleChange(value, 'imdbUrl')} required - onChange={setImdbUrl} /> handleChange(value, 'imdbId')} required - onChange={setImdbId} />
@@ -99,7 +102,7 @@ export const NewMovie: React.FC = ({ onAdd }) => { type="submit" data-cy="submit-button" className="button is-link" - disabled={!checkInput()} + disabled={isButtonDisabled()} > Add From d72e4430e59460c786077302e728be9728ababe2 Mon Sep 17 00:00:00 2001 From: Vladyslav Kolisnyk Date: Mon, 23 Oct 2023 02:17:19 +0300 Subject: [PATCH 3/4] add task corrected --- src/components/NewMovie/NewMovie.tsx | 105 +++++++++++++------------ src/components/TextField/TextField.tsx | 41 ++++------ 2 files changed, 67 insertions(+), 79 deletions(-) diff --git a/src/components/NewMovie/NewMovie.tsx b/src/components/NewMovie/NewMovie.tsx index 0b4a3cb8b..c8d393e99 100644 --- a/src/components/NewMovie/NewMovie.tsx +++ b/src/components/NewMovie/NewMovie.tsx @@ -2,97 +2,93 @@ import React, { useState } from 'react'; import { TextField } from '../TextField'; import { Movie } from '../../types/Movie'; -type MovieProps = { - onAdd: (movie: Movie) => void; +type Props = { + onAdd: (value: Movie) => void, }; -export const NewMovie = ({ onAdd }: MovieProps) => { +export const NewMovie: React.FC = ({ onAdd }) => { const [count, setCount] = useState(0); - const [formValues, setFormValues] = useState({ - title: '', + const [state, setState] = useState({ + title: ''.trim(), description: '', imgUrl: '', imdbUrl: '', imdbId: '', }); - const isButtonDisabled = () => { - const { - title, - imgUrl, - imdbUrl, - imdbId, - } = formValues; - - return !(title.trim() && imgUrl.trim() - && imdbUrl.trim() && imdbId.trim()); - }; - - const handleChange = (value: string, name: string) => { - setFormValues((prevState) => ({ - ...prevState, - [name]: value, - })); - }; - - const reset = () => { - setFormValues({ - title: '', - description: '', - imgUrl: '', - imdbUrl: '', - imdbId: '', - }); - }; - - const handleSubmit = ((event: React.FormEvent) => { - event.preventDefault(); + // 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]*))?)$/; - onAdd(formValues); + const [errorImgUrl, setErrorImgUrl] = useState(''); + const [errorImdbUrl, setErrorImdbUrl] = useState(''); - setCount(count + 1); - reset(); - }); + function handleReset(event: React.FormEvent) { + event.preventDefault(); + + const newErrorImgUrl = !pattern.test(state.imgUrl) ? 'error' : ''; + const newErrorImdbUrl = !pattern.test(state.imdbUrl) ? 'error' : ''; + + setErrorImgUrl(newErrorImgUrl); + setErrorImdbUrl(newErrorImdbUrl); + + if (!newErrorImgUrl && !newErrorImdbUrl) { + onAdd(state); + setState({ + title: '', + description: '', + imgUrl: '', + imdbUrl: '', + imdbId: '', + }); + setCount((prev) => prev + 1); + } + } return ( - +

Add a movie

handleChange(value, 'title')} + value={state.title} + onChange={value => setState({ ...state, title: value })} required /> handleChange(value, 'description')} + value={state.description} + onChange={value => setState({ ...state, description: value })} /> handleChange(value, 'imgUrl')} + value={state.imgUrl} + errorImgUrl={errorImgUrl} + onChange={value => setState({ ...state, imgUrl: value })} required /> handleChange(value, 'imdbUrl')} + value={state.imdbUrl} + errorImdbUrl={errorImdbUrl} + onChange={value => setState({ ...state, imdbUrl: value })} required /> handleChange(value, 'imdbId')} + value={state.imdbId} + onChange={value => setState({ ...state, imdbId: value })} required /> @@ -102,7 +98,12 @@ export const NewMovie = ({ onAdd }: MovieProps) => { type="submit" data-cy="submit-button" className="button is-link" - disabled={isButtonDisabled()} + disabled={ + !state.title + || !state.imgUrl + || !state.imdbUrl + || !state.imdbId + } > Add diff --git a/src/components/TextField/TextField.tsx b/src/components/TextField/TextField.tsx index 90a81787b..27e9f6340 100644 --- a/src/components/TextField/TextField.tsx +++ b/src/components/TextField/TextField.tsx @@ -7,6 +7,8 @@ type Props = { label?: string, placeholder?: string, required?: boolean, + errorImgUrl?: string, + errorImdbUrl?: string, onChange?: (newValue: string) => void, }; @@ -22,56 +24,41 @@ export const TextField: React.FC = ({ label = name, placeholder = `Enter ${label}`, required = false, - onChange = () => {}, + errorImgUrl, + errorImdbUrl, + onChange = () => { }, }) => { - // 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]*))?)$/; const [id] = useState(() => `${name}-${getRandomDigits()}`); - const [errorMessage, setErrorMessage] = useState(`${label} is required`); - // To show errors only if the field was touched (onBlur) - const [touched, setTouched] = useState(false); - const hasError = () => { - if ((name === 'imgUrl' || name === 'imdbUrl') && value) { - return touched && required && !value.match(pattern); - } - - return touched && required && !value; - }; - const handleOnBlur = () => { - if ((name === 'imgUrl' || name === 'imdbUrl') - && value && !value.match(pattern)) { - setErrorMessage('Wrong URL input'); - } else { - setErrorMessage(`${label} is required`); - } + const [touched, setTouched] = useState(false); - setTouched(true); - }; + const hasError = touched && required && !value; return (
-
onChange(event.target.value)} - onBlur={handleOnBlur} + onBlur={() => setTouched(true)} />
+ {hasError && ( +

{`${label} is required`}

+ )} - {hasError() && ( -

{errorMessage}

+ {(errorImgUrl || errorImdbUrl) && ( +

{`${label} is not valid`}

)}
); From 08805b9c196f5fc9c7b648644699c30e2ee95e21 Mon Sep 17 00:00:00 2001 From: Vladyslav Kolisnyk Date: Mon, 23 Oct 2023 16:29:19 +0300 Subject: [PATCH 4/4] add new correced task --- src/components/NewMovie/NewMovie.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NewMovie/NewMovie.tsx b/src/components/NewMovie/NewMovie.tsx index c8d393e99..37615e2f3 100644 --- a/src/components/NewMovie/NewMovie.tsx +++ b/src/components/NewMovie/NewMovie.tsx @@ -10,7 +10,7 @@ export const NewMovie: React.FC = ({ onAdd }) => { const [count, setCount] = useState(0); const [state, setState] = useState({ - title: ''.trim(), + title: '', description: '', imgUrl: '', imdbUrl: '',