Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add react movies list add form #2751

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,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 `<your_account>` with your Github username in the [DEMO LINK](https://<your_account>.github.io/react_movies-list-add-form/) and add it to the PR description.
- Replace `<your_account>` with your Github username in the [DEMO LINK](https://ValentynaD.github.io/react_movies-list-add-form/) and add it to the PR description.
67 changes: 61 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"license": "GPL-3.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.2",
"axios": "^1.7.9",
"bulma": "^0.9.4",
"classnames": "^2.5.1",
"react": "^18.3.1",
Expand All @@ -17,6 +18,7 @@
"@mate-academy/scripts": "^1.9.12",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/axios": "^0.9.36",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
Expand Down
20 changes: 17 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import React, { useState } from 'react';
import './App.scss';
import { MoviesList } from './components/MoviesList';
import { NewMovie } from './components/NewMovie';
import moviesFromServer from './api/movies.json';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that the moviesFromServer import is correctly pointing to a valid JSON file. Verify that the JSON structure matches the Movie interface defined in this file.


export const App = () => {
interface Movie {
title: string;
imgUrl: string;
imdbUrl: string;
imdbId: string;
description: string;
}

export const App: React.FC = () => {
const [movies, setMovies] = useState<Movie[]>(moviesFromServer);
const handleAddMovie = (newMovie: Movie) => {
setMovies(prevMovies => [...prevMovies, newMovie]);
};

return (
<div className="page">
<div className="page-content">
<MoviesList movies={moviesFromServer} />
<MoviesList movies={movies} />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that the MoviesList component is implemented to correctly handle the movies prop. It should expect an array of objects that match the Movie interface.

</div>
<div className="sidebar">
<NewMovie /* onAdd={(movie) => {}} */ />
<NewMovie onAdd={handleAddMovie} />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that the NewMovie component is implemented to correctly handle the onAdd prop. It should call this function with a new Movie object when a new movie is added.

</div>
</div>
);
Expand Down
13 changes: 13 additions & 0 deletions src/api/imdbApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import axios from 'axios';

const API_KEY = 'YOUR_OMDB_API_KEY'; // Замінити на ваш ключ API

export async function searchMovie(query: string) {
try {
const response = await axios.get(`http://www.omdbapi.com/?apikey=${API_KEY}&t=${query}`);
return response.data;
} catch (error) {
console.error('Error fetching movie data:', error);
return null;
}
}
1 change: 1 addition & 0 deletions src/components/MoviesList/MoviesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export const MoviesList: React.FC<Props> = ({ movies }) => (
))}
</div>
);
export default MoviesList;
162 changes: 139 additions & 23 deletions src/components/NewMovie/NewMovie.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,158 @@
import { 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);
interface Props {
onAdd: (movie: {
title: string;
imgUrl: string;
imdbUrl: string;
imdbId: string;
description: string;
}) => void;
}

export const NewMovie: React.FC<Props> = ({ onAdd }) => {
const [title, setTitle] = useState('');
const [imgUrl, setImgUrl] = useState('');
const [imdbUrl, setImdbUrl] = useState('');
const [imdbId, setImdbId] = useState('');
const [description, setDescription] = useState('');
const [errors, setErrors] = useState({
title: '',
imgUrl: '',
imdbUrl: '',
imdbId: '',
description: '',
});
const [successMessage, setSuccessMessage] = useState('');
const clearForm = () => {
setTitle('');
setImgUrl('');
setImdbUrl('');
setImdbId('');
setDescription('');
};

const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
if (title && imgUrl && imdbUrl && imdbId) {
if (typeof onAdd === 'function') {
onAdd({ title, imgUrl, imdbUrl, imdbId, description });
setSuccessMessage('Movie added successfully!');
setTimeout(() => setSuccessMessage(''), 3000);
clearForm();
}
}
};

const isValidUrl = (url: string) => {
try {
new URL(url);

return true;
} catch (_) {
return false;
}
};

const isValidImdbUrl = (url: string) =>
url.startsWith('https://www.imdb.com/title/');

const isValidImdbId = (id: string) => /^tt\d+$/.test(id);

const handleBlur = (field: string, value: string) => {
if (!value.trim()) {
setErrors(prev => ({ ...prev, [field]: `${field} is required` }));
} else {
if (field === 'imgUrl' && !isValidUrl(value)) {
setErrors(prev => ({ ...prev, [field]: 'Please provide a valid URL' }));
} else if (field === 'imdbUrl' && !isValidImdbUrl(value)) {
setErrors(prev => ({
...prev,
[field]: 'Please provide a valid IMDb URL',
}));
} else if (field === 'imdbId' && !isValidImdbId(value)) {
setErrors(prev => ({
...prev,
[field]: 'Please provide a valid IMDb ID',
}));
} else {
setErrors(prev => ({ ...prev, [field]: '' }));
}
}
};

return (
<form className="NewMovie" key={count}>
<h2 className="title">Add a movie</h2>
<form onSubmit={handleSubmit} data-cy="new-movie-form">
<h2>Add a movie</h2>

<TextField

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that the TextField component is implemented to correctly handle the props such as name, label, value, onChange, onBlur, and required. It should be able to display the input field with the correct label and handle user input and validation feedback.

name="title"
label="Title"
value=""
onChange={() => {}}
value={title}
onChange={e => setTitle(e.target.value)}
onBlur={e => handleBlur('title', e.target.value)}
required
data-cy="movie-title"
/>

<TextField
name="description"
label="Description"
value={description}
onChange={e => setDescription(e.target.value)}
data-cy="movie-description"
/>

<TextField
name="imgUrl"
label="Image URL"
value={imgUrl}
onChange={e => setImgUrl(e.target.value)}
onBlur={e => handleBlur('imgUrl', e.target.value)}
required
data-cy="movie-imgUrl"
/>

<TextField name="description" label="Description" value="" />
<TextField
name="imdbUrl"
label="Imdb URL"
value={imdbUrl}
onChange={e => setImdbUrl(e.target.value)}
onBlur={e => handleBlur('imdbUrl', e.target.value)}
required
data-cy="movie-imdbUrl"
/>

<TextField name="imgUrl" label="Image URL" value="" />
<TextField
name="imdbId"
label="Imdb ID"
value={imdbId}
onChange={e => setImdbId(e.target.value)}
onBlur={e => handleBlur('imdbId', e.target.value)}
required
data-cy="movie-imdbId"
/>

<TextField name="imdbUrl" label="Imdb URL" value="" />
<button
type="submit"
data-cy="submit-button"
className="button is-link"
disabled={!(title && imgUrl && imdbUrl && imdbId)}
>
Add
</button>

<TextField name="imdbId" label="Imdb ID" value="" />
{Object.entries(errors).map(
([key, value]) =>
value && (
<p key={key} className="error-message">
{value}
</p>
),
)}

<div className="field is-grouped">
<div className="control">
<button
type="submit"
data-cy="submit-button"
className="button is-link"
>
Add
</button>
</div>
</div>
{successMessage && <p className="success-message">{successMessage}</p>}
</form>
);
};
Loading