From a96b11ad6312764759fcf0222670ad251dfe12d4 Mon Sep 17 00:00:00 2001 From: Nazar Pastuschak Date: Sat, 20 Jan 2024 12:58:17 +0200 Subject: [PATCH 1/5] add task react_movies-list-add-form --- README.md | 4 +- src/App.tsx | 14 +++- src/components/NewMovie/NewMovie.tsx | 88 ++++++++++++++++++++++---- src/components/TextField/TextField.tsx | 4 ++ 4 files changed, 93 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 6001d15be..54f4d1efa 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://P-Nazar.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..63405ffe3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,26 @@ 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 [movieList, setMovieList] = useState([...moviesFromServer]); + + const handleAddMovie = (newMovie: Movie) => { + setMovieList((prevMovies) => [...prevMovies, newMovie]); + }; + return (
- +
- {}} */ /> +
); diff --git a/src/components/NewMovie/NewMovie.tsx b/src/components/NewMovie/NewMovie.tsx index 34f22fb0a..5cff8d302 100644 --- a/src/components/NewMovie/NewMovie.tsx +++ b/src/components/NewMovie/NewMovie.tsx @@ -1,45 +1,103 @@ import { useState } from 'react'; import { TextField } from '../TextField'; -export const NewMovie = () => { +interface Props { + onAdd: ( + movie: { + title: string, + description: string, + imgUrl: string, + imdbUrl: string, + imdbId: string, + } + ) => void +} + +export const NewMovie: React.FC = ({ onAdd }) => { // Increase the count after successful form submission // to reset touched status of all the `Field`s - const [count] = useState(0); + const [count, setCount] = useState(0); + + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [imgUrl, setImgUrl] = useState(''); + const [imdbUrl, setimdbUrl] = useState(''); + const [imdbId, setImdbId] = useState(''); + + const urlValidator = (urlString: 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]*))?)$/; + + if (!urlString.match(pattern)) { + return false; + } + + return true; + }; + + function addMovie(event: React.FormEvent) { + event.preventDefault(); + + onAdd({ + title: title.trim(), + description: description.trim(), + imgUrl: imgUrl.trim(), + imdbUrl: imdbUrl.trim(), + imdbId: imdbId.trim(), + }); + + setTitle(''); + setDescription(''); + setImgUrl(''); + setimdbUrl(''); + setImdbId(''); + + setCount(prev => prev + 1); + } return ( -
+

Add a movie

{}} + value={title} + onChange={setTitle} required />
@@ -48,6 +106,10 @@ export const NewMovie = () => { type="submit" data-cy="submit-button" className="button is-link" + disabled={!title + || !urlValidator(imgUrl) + || !urlValidator(imdbUrl) + || !imdbId} > Add diff --git a/src/components/TextField/TextField.tsx b/src/components/TextField/TextField.tsx index 307b19865..0a641b6b6 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, + checkUrl?: (urlString: string) => boolean, }; function getRandomDigits() { @@ -23,6 +24,7 @@ export const TextField: React.FC = ({ placeholder = `Enter ${label}`, required = false, onChange = () => {}, + checkUrl = undefined, }) => { // generage a unique id once on component load const [id] = useState(() => `${name}-${getRandomDigits()}`); @@ -55,6 +57,8 @@ export const TextField: React.FC = ({ {hasError && (

{`${label} is required`}

)} + + {((touched && value) && (checkUrl && !checkUrl(value))) && (

{`${label} incorrect URL`}

)}
); }; From bf6902e011613884ff5bff9f508db979a78430c3 Mon Sep 17 00:00:00 2001 From: Nazar Pastuschak Date: Sat, 20 Jan 2024 14:53:21 +0200 Subject: [PATCH 2/5] combine all states into one and create handleChange --- src/App.scss | 3 ++ src/components/NewMovie/NewMovie.tsx | 75 +++++++++++++++----------- src/components/TextField/TextField.tsx | 5 +- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/App.scss b/src/App.scss index 89b6ef5fb..64693840b 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,3 +1,6 @@ +iframe { + display: none; +} .page { display: grid; grid-template: auto / 1fr 400px; diff --git a/src/components/NewMovie/NewMovie.tsx b/src/components/NewMovie/NewMovie.tsx index 5cff8d302..9481cf546 100644 --- a/src/components/NewMovie/NewMovie.tsx +++ b/src/components/NewMovie/NewMovie.tsx @@ -17,12 +17,19 @@ export const NewMovie: React.FC = ({ onAdd }) => { // Increase the count after successful form submission // to reset touched status of all the `Field`s const [count, setCount] = useState(0); - - const [title, setTitle] = useState(''); - const [description, setDescription] = useState(''); - const [imgUrl, setImgUrl] = useState(''); - const [imdbUrl, setimdbUrl] = useState(''); - const [imdbId, setImdbId] = useState(''); + const [newMovie, setNewMovie] = useState({ + title: '', + description: '', + imgUrl: '', + imdbUrl: '', + imdbId: '', + }); + + // const [title, setTitle] = useState(''); + // const [description, setDescription] = useState(''); + // const [imgUrl, setImgUrl] = useState(''); + // const [imdbUrl, setimdbUrl] = useState(''); + // const [imdbId, setImdbId] = useState(''); const urlValidator = (urlString: string): boolean => { // eslint-disable-next-line max-len @@ -39,22 +46,30 @@ export const NewMovie: React.FC = ({ onAdd }) => { event.preventDefault(); onAdd({ - title: title.trim(), - description: description.trim(), - imgUrl: imgUrl.trim(), - imdbUrl: imdbUrl.trim(), - imdbId: imdbId.trim(), + title: newMovie.title.trim(), + description: newMovie.description.trim(), + imgUrl: newMovie.imgUrl.trim(), + imdbUrl: newMovie.imdbUrl.trim(), + imdbId: newMovie.imdbId.trim(), }); - setTitle(''); - setDescription(''); - setImgUrl(''); - setimdbUrl(''); - setImdbId(''); + setNewMovie({ + title: '', + description: '', + imgUrl: '', + imdbUrl: '', + imdbId: '', + }); setCount(prev => prev + 1); } + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + + setNewMovie((prevMovie) => ({ ...prevMovie, [name]: value })); + }; + return ( = ({ onAdd }) => { handleChange(e)} required /> handleChange(e)} /> handleChange(e)} checkUrl={urlValidator} required /> handleChange(e)} checkUrl={urlValidator} required /> handleChange(e)} required /> @@ -106,10 +121,10 @@ export const NewMovie: React.FC = ({ onAdd }) => { type="submit" data-cy="submit-button" className="button is-link" - disabled={!title - || !urlValidator(imgUrl) - || !urlValidator(imdbUrl) - || !imdbId} + disabled={!newMovie.title + || !urlValidator(newMovie.imgUrl) + || !urlValidator(newMovie.imdbUrl) + || !newMovie.imdbId} > Add diff --git a/src/components/TextField/TextField.tsx b/src/components/TextField/TextField.tsx index 0a641b6b6..6f4a70d40 100644 --- a/src/components/TextField/TextField.tsx +++ b/src/components/TextField/TextField.tsx @@ -7,7 +7,7 @@ type Props = { label?: string, placeholder?: string, required?: boolean, - onChange?: (newValue: string) => void, + onChange?: (e: React.ChangeEvent) => void, checkUrl?: (urlString: string) => boolean, }; @@ -41,6 +41,7 @@ export const TextField: React.FC = ({
= ({ })} placeholder={placeholder} value={value} - onChange={event => onChange(event.target.value)} + onChange={event => onChange(event)} onBlur={() => setTouched(true)} />
From 042d00241d00e334bab948fc646d152d15597ae6 Mon Sep 17 00:00:00 2001 From: Nazar Pastuschak Date: Mon, 22 Jan 2024 22:26:16 +0200 Subject: [PATCH 3/5] fix task react_movies-list-add-form --- src/App.scss | 1 + src/App.tsx | 2 +- src/components/NewMovie/NewMovie.tsx | 25 ++++++++++--------------- src/components/TextField/TextField.tsx | 2 +- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/App.scss b/src/App.scss index 64693840b..2a0875434 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,6 +1,7 @@ iframe { display: none; } + .page { display: grid; grid-template: auto / 1fr 400px; diff --git a/src/App.tsx b/src/App.tsx index 63405ffe3..4eb41b39f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,7 @@ import moviesFromServer from './api/movies.json'; import { Movie } from './types/Movie'; export const App = () => { - const [movieList, setMovieList] = useState([...moviesFromServer]); + const [movieList, setMovieList] = useState(moviesFromServer); const handleAddMovie = (newMovie: Movie) => { setMovieList((prevMovies) => [...prevMovies, newMovie]); diff --git a/src/components/NewMovie/NewMovie.tsx b/src/components/NewMovie/NewMovie.tsx index 9481cf546..8402273aa 100644 --- a/src/components/NewMovie/NewMovie.tsx +++ b/src/components/NewMovie/NewMovie.tsx @@ -25,12 +25,6 @@ export const NewMovie: React.FC = ({ onAdd }) => { imdbId: '', }); - // const [title, setTitle] = useState(''); - // const [description, setDescription] = useState(''); - // const [imgUrl, setImgUrl] = useState(''); - // const [imdbUrl, setimdbUrl] = useState(''); - // const [imdbId, setImdbId] = useState(''); - const urlValidator = (urlString: 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]*))?)$/; @@ -42,6 +36,10 @@ export const NewMovie: React.FC = ({ onAdd }) => { return true; }; + const isDisabled = !newMovie.title.trim() + || !urlValidator(newMovie.imgUrl) || !urlValidator(newMovie.imdbUrl) + || !newMovie.imdbId.trim(); + function addMovie(event: React.FormEvent) { event.preventDefault(); @@ -82,20 +80,20 @@ export const NewMovie: React.FC = ({ onAdd }) => { name="title" label="Title" value={newMovie.title} - onChange={e => handleChange(e)} + onChange={handleChange} required /> handleChange(e)} + onChange={handleChange} /> handleChange(e)} + onChange={handleChange} checkUrl={urlValidator} required /> @@ -103,7 +101,7 @@ export const NewMovie: React.FC = ({ onAdd }) => { handleChange(e)} + onChange={handleChange} checkUrl={urlValidator} required /> @@ -111,7 +109,7 @@ export const NewMovie: React.FC = ({ onAdd }) => { handleChange(e)} + onChange={handleChange} required /> @@ -121,10 +119,7 @@ export const NewMovie: React.FC = ({ onAdd }) => { type="submit" data-cy="submit-button" className="button is-link" - disabled={!newMovie.title - || !urlValidator(newMovie.imgUrl) - || !urlValidator(newMovie.imdbUrl) - || !newMovie.imdbId} + disabled={isDisabled} > Add diff --git a/src/components/TextField/TextField.tsx b/src/components/TextField/TextField.tsx index 6f4a70d40..538a47da1 100644 --- a/src/components/TextField/TextField.tsx +++ b/src/components/TextField/TextField.tsx @@ -31,7 +31,7 @@ export const TextField: React.FC = ({ // To show errors only if the field was touched (onBlur) const [touched, setTouched] = useState(false); - const hasError = touched && required && !value; + const hasError = touched && required && !value.trim(); return (
From 726de92c2d70a0102b2feef4f02344fe53a42b9b Mon Sep 17 00:00:00 2001 From: Nazar Pastuschak Date: Mon, 22 Jan 2024 23:04:41 +0200 Subject: [PATCH 4/5] error correction task react_movies-list-add-form --- src/components/NewMovie/NewMovie.tsx | 47 +++++++++++++--------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/src/components/NewMovie/NewMovie.tsx b/src/components/NewMovie/NewMovie.tsx index 8402273aa..eb7c6f1a8 100644 --- a/src/components/NewMovie/NewMovie.tsx +++ b/src/components/NewMovie/NewMovie.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { TextField } from '../TextField'; +import { Movie } from '../../types/Movie'; interface Props { onAdd: ( @@ -13,28 +14,30 @@ interface Props { ) => void } +const urlValidator = (urlString: 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]*))?)$/; + + if (!urlString.match(pattern)) { + return false; + } + + return true; +}; + +const initialMovieState = { + title: '', + description: '', + imgUrl: '', + imdbUrl: '', + imdbId: '', +}; + export const NewMovie: React.FC = ({ onAdd }) => { // Increase the count after successful form submission // to reset touched status of all the `Field`s const [count, setCount] = useState(0); - const [newMovie, setNewMovie] = useState({ - title: '', - description: '', - imgUrl: '', - imdbUrl: '', - imdbId: '', - }); - - const urlValidator = (urlString: 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]*))?)$/; - - if (!urlString.match(pattern)) { - return false; - } - - return true; - }; + const [newMovie, setNewMovie] = useState(initialMovieState); const isDisabled = !newMovie.title.trim() || !urlValidator(newMovie.imgUrl) || !urlValidator(newMovie.imdbUrl) @@ -51,13 +54,7 @@ export const NewMovie: React.FC = ({ onAdd }) => { imdbId: newMovie.imdbId.trim(), }); - setNewMovie({ - title: '', - description: '', - imgUrl: '', - imdbUrl: '', - imdbId: '', - }); + setNewMovie(initialMovieState); setCount(prev => prev + 1); } From b960ddab2cfb42cc4dbd117f36a3109755830c07 Mon Sep 17 00:00:00 2001 From: Nazar Pastuschak Date: Tue, 23 Jan 2024 21:31:25 +0200 Subject: [PATCH 5/5] fixed all the errors --- src/components/NewMovie/NewMovie.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/components/NewMovie/NewMovie.tsx b/src/components/NewMovie/NewMovie.tsx index eb7c6f1a8..928efff5f 100644 --- a/src/components/NewMovie/NewMovie.tsx +++ b/src/components/NewMovie/NewMovie.tsx @@ -3,15 +3,8 @@ import { TextField } from '../TextField'; import { Movie } from '../../types/Movie'; interface Props { - onAdd: ( - movie: { - title: string, - description: string, - imgUrl: string, - imdbUrl: string, - imdbId: string, - } - ) => void + onAdd: (movie: Movie) => void + } const urlValidator = (urlString: string): boolean => {