diff --git a/src/App.jsx b/src/App.jsx index a05ee8e..b718038 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,6 +5,7 @@ import CatalogPage from './pages/CatalogPage'; import AddMoviePage from './pages/AddMoviePage'; import EditMoviePage from './pages/EditMoviePage'; import AboutPage from './pages/AboutPage'; +import MovieDetailsPage from './pages/MovieDetailsPage'; function App() { return ( @@ -48,6 +49,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/src/components/movie/MovieCard.jsx b/src/components/movie/MovieCard.jsx index 5b00dd1..0e883d0 100644 --- a/src/components/movie/MovieCard.jsx +++ b/src/components/movie/MovieCard.jsx @@ -37,7 +37,7 @@ function MovieCard({ movie, onDelete }) { )}
- + Смотреть
diff --git a/src/components/movie/MovieForm.jsx b/src/components/movie/MovieForm.jsx index 0f0182a..da880b2 100644 --- a/src/components/movie/MovieForm.jsx +++ b/src/components/movie/MovieForm.jsx @@ -1,59 +1,21 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; +import useMovieForm from '../../hooks/useMovieForm'; function MovieForm({ movie, onSubmit, isEditing = false }) { - const [title, setTitle] = useState(''); - const [director, setDirector] = useState(''); - const [genres, setGenres] = useState([]); - const [year, setYear] = useState(''); - const [description, setDescription] = useState(''); - const [poster, setPoster] = useState(''); - const [previewVisible, setPreviewVisible] = useState(false); - - // Initialize form with movie data if editing - useEffect(() => { - if (movie) { - setTitle(movie.title || ''); - setDirector(movie.director || ''); - setGenres(Array.isArray(movie.genres) ? movie.genres : []); - setYear(movie.year || ''); - setDescription(movie.description || ''); - setPoster(movie.poster || ''); - if (movie.poster) { - setPreviewVisible(true); - } - } - }, [movie]); - - const handleGenreChange = (e) => { - const selectedGenres = Array.from(e.target.selectedOptions).map(option => option.value); - setGenres(selectedGenres); - }; - - const handlePosterChange = (e) => { - const file = e.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (e) => { - setPoster(e.target.result); - setPreviewVisible(true); - }; - reader.readAsDataURL(file); - } - }; + const { + title, setTitle, + director, setDirector, + genres, handleGenreChange, + year, setYear, + description, setDescription, + poster, handlePosterChange, + previewVisible, + getFormData + } = useMovieForm(movie); const handleSubmit = (e) => { e.preventDefault(); - - const movieData = { - title, - director, - genres, - year, - description, - poster - }; - - onSubmit(movieData); + onSubmit(getFormData()); }; return ( diff --git a/src/hooks/useMovie.js b/src/hooks/useMovie.js new file mode 100644 index 0000000..44bbe64 --- /dev/null +++ b/src/hooks/useMovie.js @@ -0,0 +1,47 @@ +import { useState, useEffect } from 'react'; +import MovieService from '../services/MovieService'; + +function useMovie(id) { + const [movie, setMovie] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!id) { + setLoading(false); + setError('Movie ID is not provided.'); // Or handle as you see fit + return; + } + + const fetchMovie = async () => { + setLoading(true); + setError(null); + try { + const data = await MovieService.getMovieById(id); + if (!data) { + setError('Фильм не найден'); + setMovie(null); // Ensure movie state is reset if not found + } else { + setMovie(data); + } + } catch (err) { + console.error('Error fetching movie:', err); + setError('Не удалось загрузить данные фильма'); + setMovie(null); // Ensure movie state is reset on error + } finally { + setLoading(false); + } + }; + + fetchMovie(); + }, [id]); // Effect runs when the id changes + + return { + movie, + loading, + error, + setMovie // It might be useful to allow manually setting the movie, e.g., after an update + }; +} + +export default useMovie; \ No newline at end of file diff --git a/src/hooks/useMovieForm.js b/src/hooks/useMovieForm.js new file mode 100644 index 0000000..1c441d8 --- /dev/null +++ b/src/hooks/useMovieForm.js @@ -0,0 +1,80 @@ +import { useState, useEffect } from 'react'; + +function useMovieForm(initialMovieData = null) { + const [title, setTitle] = useState(''); + const [director, setDirector] = useState(''); + const [genres, setGenres] = useState([]); + const [year, setYear] = useState(''); + const [description, setDescription] = useState(''); + const [poster, setPoster] = useState(''); + const [previewVisible, setPreviewVisible] = useState(false); + + useEffect(() => { + if (initialMovieData) { + setTitle(initialMovieData.title || ''); + setDirector(initialMovieData.director || ''); + setGenres(Array.isArray(initialMovieData.genres) ? initialMovieData.genres : []); + setYear(initialMovieData.year || ''); + setDescription(initialMovieData.description || ''); + setPoster(initialMovieData.poster || ''); + if (initialMovieData.poster) { + setPreviewVisible(true); + } else { + setPreviewVisible(false); // Ensure preview is hidden if no poster + } + } else { + // Reset form if no initial data (e.g., for add form after an edit) + setTitle(''); + setDirector(''); + setGenres([]); + setYear(''); + setDescription(''); + setPoster(''); + setPreviewVisible(false); + } + }, [initialMovieData]); + + const handleGenreChange = (e) => { + const selectedGenres = Array.from(e.target.selectedOptions).map(option => option.value); + setGenres(selectedGenres); + }; + + const handlePosterChange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (event) => { + setPoster(event.target.result); + setPreviewVisible(true); + }; + reader.readAsDataURL(file); + } else { + // If no file is selected, or selection is cancelled + // setPoster(''); // Optionally clear poster if no file is chosen + // setPreviewVisible(false); // Optionally hide preview + } + }; + + const getFormData = () => ({ + title, + director, + genres, + year, + description, + poster + }); + + // Exposed state and handlers + return { + title, setTitle, + director, setDirector, + genres, setGenres, handleGenreChange, // Expose specific handler for genres + year, setYear, + description, setDescription, + poster, setPoster, handlePosterChange, // Expose specific handler for poster + previewVisible, + getFormData // Function to get all form data for submission + }; +} + +export default useMovieForm; \ No newline at end of file diff --git a/src/hooks/useMovies.js b/src/hooks/useMovies.js new file mode 100644 index 0000000..9734c0c --- /dev/null +++ b/src/hooks/useMovies.js @@ -0,0 +1,71 @@ +import { useState, useEffect } from 'react'; +import MovieService from '../services/MovieService'; + +function useMovies() { + const [movies, setMovies] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [selectedGenre, setSelectedGenre] = useState(''); + const [genres, setGenres] = useState([]); + + useEffect(() => { + const fetchMoviesAndGenres = async () => { + setLoading(true); + setError(null); + try { + const data = await MovieService.getMovies(); + setMovies(data); + + const allGenres = data.flatMap(movie => movie.genres || []); + const uniqueGenres = [...new Set(allGenres)]; + setGenres(uniqueGenres); + + } catch (err) { + console.error('Error fetching movies:', err); + setError('Не удалось загрузить фильмы. Пожалуйста, попробуйте позже.'); + } finally { + setLoading(false); + } + }; + + fetchMoviesAndGenres(); + }, []); + + const handleDeleteMovie = async (id) => { + try { + await MovieService.deleteMovie(id); + setMovies(prevMovies => prevMovies.filter(movie => movie.id !== id)); + } catch (err) { + console.error('Error deleting movie:', err); + alert('Произошла ошибка при удалении фильма.'); + // Optionally, re-throw or set an error state for the component to handle + } + }; + + const filteredMovies = movies.filter(movie => { + const matchesSearch = searchTerm ? + (movie.title?.toLowerCase().includes(searchTerm.toLowerCase()) || + movie.director?.toLowerCase().includes(searchTerm.toLowerCase())) : true; + + const matchesGenre = selectedGenre === '' || + (Array.isArray(movie.genres) && movie.genres.includes(selectedGenre)); + + return matchesSearch && matchesGenre; + }); + + return { + movies: filteredMovies, // Return filtered movies + loading, + error, + searchTerm, + setSearchTerm, + selectedGenre, + setSelectedGenre, + genres, // Original list of unique genres for the dropdown + handleDeleteMovie, + allMovies: movies // Raw list of movies if needed elsewhere, though typically not exposed directly + }; +} + +export default useMovies; \ No newline at end of file diff --git a/src/pages/CatalogPage.jsx b/src/pages/CatalogPage.jsx index 773f635..2e29049 100644 --- a/src/pages/CatalogPage.jsx +++ b/src/pages/CatalogPage.jsx @@ -1,60 +1,23 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import MovieList from '../components/Movie/MovieList'; -import MovieService from '../services/MovieService'; +// import MovieService from '../services/MovieService'; // No longer needed here +import useMovies from '../hooks/useMovies'; // Import the custom hook function CatalogPage() { - const [movies, setMovies] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // For filtering - const [searchTerm, setSearchTerm] = useState(''); - const [selectedGenre, setSelectedGenre] = useState(''); - const [genres, setGenres] = useState([]); + const { + movies, // This is now filteredMovies from the hook + loading, + error, + searchTerm, + setSearchTerm, + selectedGenre, + setSelectedGenre, + genres, // This is the unique genres list from the hook + handleDeleteMovie + } = useMovies(); - useEffect(() => { - const fetchMovies = async () => { - try { - const data = await MovieService.getMovies(); - setMovies(data); - - // Extract unique genres for filter - const allGenres = data.flatMap(movie => movie.genres || []); - const uniqueGenres = [...new Set(allGenres)]; - setGenres(uniqueGenres); - - setLoading(false); - } catch (error) { - console.error('Error fetching movies:', error); - setError('Не удалось загрузить фильмы. Пожалуйста, попробуйте позже.'); - setLoading(false); - } - }; - - fetchMovies(); - }, []); - - const handleDeleteMovie = async (id) => { - try { - await MovieService.deleteMovie(id); - setMovies(movies.filter(movie => movie.id !== id)); - } catch (error) { - console.error('Error deleting movie:', error); - alert('Произошла ошибка при удалении фильма.'); - } - }; - - // Filter movies based on search term and selected genre - const filteredMovies = movies.filter(movie => { - const matchesSearch = searchTerm ? - (movie.title?.toLowerCase().includes(searchTerm.toLowerCase()) || - movie.director?.toLowerCase().includes(searchTerm.toLowerCase())) : true; - - const matchesGenre = selectedGenre === '' || - (Array.isArray(movie.genres) && movie.genres.includes(selectedGenre)); - - return matchesSearch && matchesGenre; - }); + // useEffect and handleDeleteMovie logic is now in useMovies hook + // Filtered movies logic is also in useMovies hook if (loading) { return
; @@ -90,7 +53,8 @@ function CatalogPage() {
- + {/* Pass the movies (which are already filtered) and onDeleteMovie from the hook */} + ); } diff --git a/src/pages/EditMoviePage.jsx b/src/pages/EditMoviePage.jsx index 2aae2e9..1892ebc 100644 --- a/src/pages/EditMoviePage.jsx +++ b/src/pages/EditMoviePage.jsx @@ -1,42 +1,21 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import MovieForm from '../components/Movie/MovieForm'; import MovieService from '../services/MovieService'; +import useMovie from '../hooks/useMovie'; function EditMoviePage() { const { id } = useParams(); const navigate = useNavigate(); - const [movie, setMovie] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchMovie = async () => { - try { - const data = await MovieService.getMovieById(id); - if (!data) { - setError('Фильм не найден'); - } else { - setMovie(data); - } - } catch (error) { - console.error('Error fetching movie:', error); - setError('Не удалось загрузить данные фильма'); - } finally { - setLoading(false); - } - }; - - fetchMovie(); - }, [id]); + const { movie, loading, error, setMovie } = useMovie(id); const handleUpdateMovie = async (movieData) => { try { - await MovieService.updateMovie(id, movieData); + const updatedMovie = await MovieService.updateMovie(id, movieData); alert('Фильм успешно обновлен!'); navigate('/catalog'); - } catch (error) { - console.error('Error updating movie:', error); + } catch (err) { + console.error('Error updating movie:', err); alert('Произошла ошибка при обновлении фильма.'); } }; @@ -49,6 +28,10 @@ function EditMoviePage() { return
{error}
; } + if (!movie) { + return
Фильм для редактирования не найден.
; + } + return (

Редактировать фильм

diff --git a/src/pages/MovieDetailsPage.jsx b/src/pages/MovieDetailsPage.jsx new file mode 100644 index 0000000..818d7cb --- /dev/null +++ b/src/pages/MovieDetailsPage.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { useParams, Link } from 'react-router-dom'; +import useMovie from '../hooks/useMovie'; + +function MovieDetailsPage() { + const { id } = useParams(); + const { movie, loading, error } = useMovie(id); + + if (loading) { + return
; + } + + if (error) { + return
{error}
; + } + + if (!movie) { + return
Фильм не найден.
; + } + + return ( +
+
+
+ {`${movie.title} +
+
+

{movie.title}

+

+ + Режиссер: {movie.director} +

+

+ + Год: {movie.year} +

+

+ + Жанры: {Array.isArray(movie.genres) ? movie.genres.join(', ') : movie.genres} +

+
+
Описание:
+

{movie.description}

+
+
+ + Редактировать + + + Назад к каталогу + + {/* Placeholder for a play button or video embed area */} + {/* */} +
+
+
+
+ ); +} + +export default MovieDetailsPage; \ No newline at end of file diff --git a/Отчет.docx b/Отчет.docx index 6c76740..1d72af1 100644 Binary files a/Отчет.docx and b/Отчет.docx differ