This commit is contained in:
LivelyPuer
2025-05-06 21:10:03 +04:00
parent e3a1b6f03c
commit 9a980379a2
10 changed files with 303 additions and 133 deletions

View File

@@ -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() {
<Route path="/catalog" element={<CatalogPage />} />
<Route path="/add-movie" element={<AddMoviePage />} />
<Route path="/edit-movie/:id" element={<EditMoviePage />} />
<Route path="/movie/:id" element={<MovieDetailsPage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
</div>

View File

@@ -37,7 +37,7 @@ function MovieCard({ movie, onDelete }) {
)}
</div>
<div className="card-footer d-flex justify-content-between">
<Link to="/about" className="btn btn-orange">
<Link to={`/movie/${movie.id}`} className="btn btn-orange">
<i className="bi bi-play-circle me-1"></i>Смотреть
</Link>
<div>

View File

@@ -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 (

47
src/hooks/useMovie.js Normal file
View File

@@ -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;

80
src/hooks/useMovieForm.js Normal file
View File

@@ -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;

71
src/hooks/useMovies.js Normal file
View File

@@ -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;

View File

@@ -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 <div className="text-center py-5"><div className="spinner-border" role="status"></div></div>;
@@ -90,7 +53,8 @@ function CatalogPage() {
</div>
</div>
<MovieList movies={filteredMovies} onDeleteMovie={handleDeleteMovie} />
{/* Pass the movies (which are already filtered) and onDeleteMovie from the hook */}
<MovieList movies={movies} onDeleteMovie={handleDeleteMovie} />
</div>
);
}

View File

@@ -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 <div className="alert alert-danger">{error}</div>;
}
if (!movie) {
return <div className="alert alert-warning">Фильм для редактирования не найден.</div>;
}
return (
<div>
<h2 className="mb-4">Редактировать фильм</h2>

View File

@@ -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 <div className="text-center py-5"><div className="spinner-border" role="status"></div></div>;
}
if (error) {
return <div className="alert alert-danger">{error}</div>;
}
if (!movie) {
return <div className="alert alert-warning">Фильм не найден.</div>;
}
return (
<div className="container mt-5">
<div className="row">
<div className="col-md-4">
<img src={movie.poster} className="img-fluid rounded shadow-sm" alt={`${movie.title} Poster`} />
</div>
<div className="col-md-8">
<h1 className="mb-3 text-orange">{movie.title}</h1>
<p className="lead">
<i className="bi bi-person-video3 text-secondary me-2"></i>
<strong>Режиссер:</strong> {movie.director}
</p>
<p>
<i className="bi bi-calendar3 text-secondary me-2"></i>
<strong>Год:</strong> {movie.year}
</p>
<p>
<i className="bi bi-tags text-secondary me-2"></i>
<strong>Жанры:</strong> {Array.isArray(movie.genres) ? movie.genres.join(', ') : movie.genres}
</p>
<hr />
<h5 className="mt-4 mb-3">Описание:</h5>
<p style={{ textAlign: 'justify' }}>{movie.description}</p>
<hr />
<div className="mt-4">
<Link to={`/edit-movie/${movie.id}`} className="btn btn-warning me-2">
<i className="bi bi-pencil me-1"></i> Редактировать
</Link>
<Link to="/catalog" className="btn btn-outline-secondary">
<i className="bi bi-arrow-left-circle me-1"></i> Назад к каталогу
</Link>
{/* Placeholder for a play button or video embed area */}
{/* <button className=\"btn btn-lg btn-success mt-3 w-100\">Смотреть фильм</button> */}
</div>
</div>
</div>
</div>
);
}
export default MovieDetailsPage;

Binary file not shown.