This commit is contained in:
LivelyPuer
2025-05-06 20:44:13 +04:00
parent bd75372ea2
commit e3a1b6f03c
32 changed files with 3643 additions and 2907 deletions

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { Link } from 'react-router-dom';
function MovieCard({ movie, onDelete }) {
const handleDelete = () => {
if (window.confirm('Вы уверены, что хотите удалить этот фильм?')) {
onDelete(movie.id);
}
};
return (
<div className="card movie-card h-100 bg-dark p-0">
<img src={movie.poster} className="card-img-top" alt={`${movie.title} Poster`} />
<div className="card-body">
<h5 className="card-title text-white">
<i className="bi bi-film text-orange me-2"></i>{movie.title}
</h5>
<p className="card-text text-white">
<i className="bi bi-person-video3 text-secondary me-2"></i>{movie.director}
</p>
<p className="card-text text-light">
<i className="bi bi-tags text-secondary me-2"></i>
{Array.isArray(movie.genres) ? movie.genres.join(', ') : movie.genres}
</p>
<p className="card-text text-light">
<i className="bi bi-calendar3 text-secondary me-2"></i>{movie.year}
</p>
{movie.description && (
<div className="mt-3">
<p className="card-text text-light small">
<i className="bi bi-text-paragraph text-secondary me-2"></i>
{movie.description.length > 100
? movie.description.substring(0, 100) + '...'
: movie.description}
</p>
</div>
)}
</div>
<div className="card-footer d-flex justify-content-between">
<Link to="/about" className="btn btn-orange">
<i className="bi bi-play-circle me-1"></i>Смотреть
</Link>
<div>
<Link to={`/edit-movie/${movie.id}`} className="btn btn-outline-warning edit-movie me-1">
<i className="bi bi-pencil"></i>
</Link>
<button className="btn btn-outline-danger delete-movie" onClick={handleDelete}>
<i className="bi bi-trash"></i>
</button>
</div>
</div>
</div>
);
}
export default MovieCard;

View File

@@ -1,292 +0,0 @@
export class MovieController {
constructor(model, view) {
this.model = model;
this.view = view;
// Initialize the controller based on the current page
this.initializeController();
}
initializeController() {
// Check which page we're on and initialize accordingly
const path = window.location.pathname;
if (path.includes('catalog.html')) {
// Catalog page initialization
this.initializeCatalogPage();
} else if (path.includes('add-movie.html')) {
// Add movie page initialization
this.initializeAddMoviePage();
} else if (path.includes('edit-movie.html')) {
// Edit movie page initialization
this.initializeEditMoviePage();
} else if (path.endsWith('index.html') || path.endsWith('/')) {
// Homepage initialization
this.initializeHomepage();
}
}
initializeHomepage() {
// Load and display featured movies on the homepage
this.model.getMovies().then(movies => {
// Sort movies by some criteria to get "featured" ones
// For example, sort by year (newest first)
const sortedMovies = [...movies].sort((a, b) => b.year - a.year);
this.view.renderMovies(sortedMovies);
// Bind delete functionality on homepage too
this.view.bindDeleteMovie(this.handleDeleteMovie.bind(this));
});
}
initializeCatalogPage() {
// Load and display all movies
this.model.getMovies().then(movies => {
this.view.renderMovies(movies);
});
// Bind event handlers for the catalog page
this.view.bindDeleteMovie(this.handleDeleteMovie.bind(this));
this.view.bindFilterMovies(this.handleFilterMovies.bind(this));
}
initializeAddMoviePage() {
// Bind the save new movie handler
const addMovieForm = document.getElementById('addMovieForm');
if (addMovieForm) {
addMovieForm.addEventListener('submit', (e) => {
e.preventDefault();
const newMovie = {
title: document.getElementById('movieTitle').value,
director: document.getElementById('movieDirector').value,
genres: Array.from(document.getElementById('movieGenre').selectedOptions).map(option => option.text),
year: document.getElementById('movieYear').value,
description: document.getElementById('movieDescription').value,
poster: document.getElementById('posterPreview')?.src || 'resources/movies/placeholder.jpg'
};
this.handleSaveNewMovie(newMovie);
});
}
// Handle poster preview
const moviePoster = document.getElementById('moviePoster');
const posterPreview = document.getElementById('posterPreview');
const previewContainer = document.querySelector('.preview-container');
if (moviePoster && posterPreview && previewContainer) {
moviePoster.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
posterPreview.src = e.target.result;
previewContainer.classList.remove('d-none');
};
reader.readAsDataURL(file);
}
});
}
}
initializeEditMoviePage() {
// Get movie ID from URL
const urlParams = new URLSearchParams(window.location.search);
const movieId = urlParams.get('id');
if (!movieId) {
alert('Фильм не найден');
window.location.href = 'catalog.html';
return;
}
// Load movie data and set up form
this.model.getMovieById(movieId)
.then(movie => {
if (!movie) {
alert('Фильм не найден');
window.location.href = 'catalog.html';
return;
}
// Fill form with movie data
document.getElementById('movieId').value = movie.id;
document.getElementById('movieTitle').value = movie.title;
document.getElementById('movieDirector').value = movie.director;
document.getElementById('movieYear').value = movie.year;
document.getElementById('movieDescription').value = movie.description || '';
// Handle genres (multi-select)
const genreSelect = document.getElementById('movieGenre');
if (Array.isArray(movie.genres)) {
Array.from(genreSelect.options).forEach(option => {
option.selected = movie.genres.includes(option.text);
});
}
// Handle poster preview
const posterPreview = document.getElementById('posterPreview');
if (movie.poster) {
posterPreview.src = movie.poster;
document.querySelector('.preview-container').classList.remove('d-none');
}
})
.catch(error => {
console.error('Error loading movie:', error);
alert('Произошла ошибка при загрузке фильма');
window.location.href = 'catalog.html';
});
// Handle poster file input change
const moviePoster = document.getElementById('moviePoster');
const posterPreview = document.getElementById('posterPreview');
const previewContainer = document.querySelector('.preview-container');
if (moviePoster && posterPreview && previewContainer) {
moviePoster.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
posterPreview.src = e.target.result;
previewContainer.classList.remove('d-none');
};
reader.readAsDataURL(file);
}
});
}
// Handle form submission
const editMovieForm = document.getElementById('editMovieForm');
if (editMovieForm) {
editMovieForm.addEventListener('submit', (e) => {
e.preventDefault();
const updatedMovie = {
id: movieId,
title: document.getElementById('movieTitle').value,
director: document.getElementById('movieDirector').value,
genres: Array.from(document.getElementById('movieGenre').selectedOptions).map(option => option.text),
year: document.getElementById('movieYear').value,
description: document.getElementById('movieDescription').value,
poster: document.getElementById('posterPreview').src
};
this.model.updateMovie(movieId, updatedMovie)
.then(() => {
alert('Фильм успешно обновлен!');
window.location.href = 'catalog.html';
})
.catch(error => {
console.error('Error updating movie:', error);
alert('Произошла ошибка при обновлении фильма');
});
});
}
}
handleSaveNewMovie(movieData) {
this.model.addMovie(movieData)
.then(() => {
alert('Фильм успешно добавлен!');
window.location.href = 'catalog.html';
})
.catch(error => {
console.error('Error adding movie:', error);
alert('Произошла ошибка при добавлении фильма.');
});
}
handleDeleteMovie(movieId) {
// Find the movie card element
const movieCard = document.querySelector(`.movie-card[data-movie-id="${movieId}"]`);
if (movieCard) {
// Add visual indication that deletion is in progress
movieCard.classList.add('deleting');
movieCard.style.opacity = '0.5';
movieCard.style.transition = 'all 0.3s ease';
}
this.model.deleteMovie(movieId)
.then(() => {
// Remove the movie card from DOM without page refresh
if (movieCard) {
movieCard.style.opacity = '0';
movieCard.style.transform = 'scale(0.8)';
// Remove the element after animation completes
setTimeout(() => {
movieCard.remove();
}, 300);
}
// Show success notification
this.showNotification('Фильм успешно удален!', 'success');
})
.catch(error => {
console.error('Error deleting movie:', error);
// Reset the card if deletion failed
if (movieCard) {
movieCard.classList.remove('deleting');
movieCard.style.opacity = '1';
movieCard.style.transform = 'none';
}
this.showNotification('Произошла ошибка при удалении фильма.', 'danger');
});
}
// Add a notification method to show feedback without alerts
showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.className = `alert alert-${type} notification-toast`;
notification.style.position = 'fixed';
notification.style.top = '20px';
notification.style.right = '20px';
notification.style.zIndex = '9999';
notification.style.minWidth = '250px';
notification.style.opacity = '0';
notification.style.transform = 'translateY(-20px)';
notification.style.transition = 'all 0.3s ease';
notification.innerHTML = message;
// Add close button
const closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'btn-close';
closeBtn.setAttribute('aria-label', 'Close');
closeBtn.style.float = 'right';
closeBtn.onclick = () => notification.remove();
notification.prepend(closeBtn);
// Add to document
document.body.appendChild(notification);
// Animate in
setTimeout(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateY(0)';
}, 10);
// Auto remove after 5 seconds
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateY(-20px)';
setTimeout(() => notification.remove(), 300);
}, 5000);
}
handleFilterMovies(filters) {
this.model.getFilteredMovies(filters)
.then(movies => {
this.view.renderMovies(movies);
})
.catch(error => {
console.error('Error filtering movies:', error);
});
}
}

View File

@@ -0,0 +1,167 @@
import React, { useState, useEffect } from 'react';
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 handleSubmit = (e) => {
e.preventDefault();
const movieData = {
title,
director,
genres,
year,
description,
poster
};
onSubmit(movieData);
};
return (
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label htmlFor="movieTitle" className="form-label">Название фильма</label>
<input
type="text"
className="form-control"
id="movieTitle"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div className="mb-3">
<label htmlFor="movieDirector" className="form-label">Режиссер</label>
<input
type="text"
className="form-control"
id="movieDirector"
value={director}
onChange={(e) => setDirector(e.target.value)}
required
/>
</div>
<div className="mb-3">
<label htmlFor="movieGenre" className="form-label">Жанры</label>
<select
className="form-select"
id="movieGenre"
multiple
value={genres}
onChange={handleGenreChange}
required
>
<option value="Боевик">Боевик</option>
<option value="Комедия">Комедия</option>
<option value="Драма">Драма</option>
<option value="Фантастика">Фантастика</option>
<option value="Ужасы">Ужасы</option>
<option value="Триллер">Триллер</option>
<option value="Детектив">Детектив</option>
<option value="Приключения">Приключения</option>
</select>
</div>
<div className="mb-3">
<label htmlFor="movieYear" className="form-label">Год выпуска</label>
<input
type="number"
className="form-control"
id="movieYear"
min="1900"
max={new Date().getFullYear()}
value={year}
onChange={(e) => setYear(e.target.value)}
required
/>
</div>
<div className="mb-3">
<label htmlFor="movieDescription" className="form-label">Описание</label>
<textarea
className="form-control"
id="movieDescription"
rows="3"
value={description}
onChange={(e) => setDescription(e.target.value)}
></textarea>
</div>
<div className="mb-3">
<label htmlFor="moviePoster" className="form-label">Постер</label>
<input
type="file"
className="form-control"
id="moviePoster"
accept="image/*"
onChange={handlePosterChange}
/>
</div>
{previewVisible && (
<div className="preview-container mb-3">
<label className="form-label">Предпросмотр постера</label>
<img
src={poster}
alt="Предпросмотр постера"
id="posterPreview"
className="img-thumbnail"
style={{ maxHeight: '300px' }}
/>
</div>
)}
<div className="d-flex justify-content-between">
<button type="submit" className="btn btn-primary">
{isEditing ? 'Сохранить изменения' : 'Добавить фильм'}
</button>
<button type="button" className="btn btn-secondary" onClick={() => window.history.back()}>
Отмена
</button>
</div>
</form>
);
}
export default MovieForm;

View File

@@ -0,0 +1,45 @@
import React from 'react';
import MovieCard from './MovieCard';
import { Link } from 'react-router-dom';
function MovieList({ movies, onDeleteMovie, isHomepage = false }) {
// If we're on the homepage, only show up to 6 featured movies
const moviesToShow = isHomepage ? movies.slice(0, 6) : movies;
return (
<div>
<div className="mb-4 d-flex justify-content-between align-items-center">
<h2 className={isHomepage ? "text-orange" : ""}>
{isHomepage ? 'Популярные фильмы' : 'Каталог фильмов'}
</h2>
<Link to="/add-movie" className="btn btn-success">
<i className="bi bi-plus-circle me-2"></i>Добавить фильм
</Link>
</div>
<div className="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4" id="movieContainer">
{moviesToShow.length > 0 ? (
moviesToShow.map(movie => (
<div className="col" key={movie.id}>
<MovieCard movie={movie} onDelete={onDeleteMovie} />
</div>
))
) : (
<div className="col-12 text-center py-5">
<p className="text-muted">Фильмы не найдены</p>
</div>
)}
</div>
{isHomepage && movies.length > 6 && (
<div className="col-12 text-center mt-4">
<Link to="/catalog" className="btn btn-primary">
Смотреть все фильмы
</Link>
</div>
)}
</div>
);
}
export default MovieList;

View File

@@ -1,356 +0,0 @@
export class MovieModel {
constructor() {
this.apiUrl = 'http://localhost:3000';
this.movies = [];
this.genres = [];
this.directors = [];
// Initialize data
this.initializeData();
}
async initializeData() {
try {
await this.fetchAllData();
} catch (error) {
console.error('Failed to initialize from API, using localStorage:', error);
// Fallback to localStorage
let movies = JSON.parse(localStorage.getItem('movies')) || [];
if (movies.length === 0) {
movies = this.getSampleMovies();
localStorage.setItem('movies', JSON.stringify(movies));
}
this.movies = movies;
}
}
async fetchAllData() {
try {
// Fetch all necessary data in parallel using Fetch API
const [moviesResponse, genresResponse, directorsResponse] = await Promise.all([
fetch(`${this.apiUrl}/movies`),
fetch(`${this.apiUrl}/genres`),
fetch(`${this.apiUrl}/directors`)
]);
if (!moviesResponse.ok || !genresResponse.ok || !directorsResponse.ok) {
throw new Error('Failed to fetch data from server');
}
this.movies = await moviesResponse.json();
this.genres = await genresResponse.json();
this.directors = await directorsResponse.json();
// Process movies to include full genre and director objects
this.movies = this.movies.map(movie => this.processMovie(movie));
return this.movies;
} catch (error) {
console.error('Error fetching data:', error);
throw error;
}
}
processMovie(movie) {
// Convert genreIds to full genre names
const genres = movie.genreIds?.map(id =>
this.genres.find(genre => genre.id === id)?.name
).filter(Boolean) || [];
// Get director name
const director = this.directors.find(dir => dir.id === movie.directorId)?.name || 'Unknown Director';
return {
...movie,
genres,
director
};
}
// Add this method to match what's being called in the controller
async getMovies() {
return this.getAllMovies();
}
async getAllMovies() {
if (this.movies.length === 0) {
try {
await this.fetchAllData();
} catch (error) {
console.error('Error fetching movies:', error);
// Fallback to localStorage
this.movies = JSON.parse(localStorage.getItem('movies')) || this.getSampleMovies();
this.saveToStorage();
}
}
return this.movies;
}
async getMovieById(id) {
try {
const response = await fetch(`${this.apiUrl}/movies/${id}`);
if (!response.ok) {
throw new Error('Failed to fetch movie');
}
const movie = await response.json();
return this.processMovie(movie);
} catch (error) {
console.error('Error fetching movie:', error);
// Fallback to local cache
return this.movies.find(movie => movie.id === id);
}
}
async addMovie(movie) {
try {
// Convert genre names to ids
const genreIds = movie.genres.map(genreName => {
const genre = this.genres.find(g => g.name === genreName);
return genre ? genre.id : null;
}).filter(Boolean);
// Find director id by name or create a new one
let directorId;
const directorObj = this.directors.find(d => d.name === movie.director);
if (directorObj) {
directorId = directorObj.id;
} else {
// If director doesn't exist, create a new one
const newDirector = {
name: movie.director,
birthYear: "Unknown",
country: "Unknown"
};
const dirResponse = await fetch(`${this.apiUrl}/directors`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newDirector)
});
if (!dirResponse.ok) {
throw new Error('Failed to add new director');
}
const createdDirector = await dirResponse.json();
this.directors.push(createdDirector);
directorId = createdDirector.id;
}
const movieData = {
title: movie.title,
directorId,
year: movie.year,
description: movie.description || '',
poster: movie.poster,
genreIds
};
const response = await fetch(`${this.apiUrl}/movies`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(movieData)
});
if (!response.ok) {
throw new Error('Failed to add movie');
}
const newMovie = await response.json();
const processedMovie = this.processMovie(newMovie);
this.movies.push(processedMovie);
return processedMovie;
} catch (error) {
console.error('Error adding movie:', error);
// Fallback to local storage
return this.addMovieLocally(movie);
}
}
async deleteMovie(id) {
try {
const response = await fetch(`${this.apiUrl}/movies/${id}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete movie');
}
this.movies = this.movies.filter(movie => movie.id !== id);
return true;
} catch (error) {
console.error('Error deleting movie:', error);
// Fallback to local storage
return this.deleteMovieLocally(id);
}
}
async updateMovie(id, updatedMovie) {
try {
// Convert genre names to ids
const genreIds = updatedMovie.genres.map(genreName => {
const genre = this.genres.find(g => g.name === genreName);
return genre ? genre.id : null;
}).filter(Boolean);
// Find director id by name
let directorId;
const directorObj = this.directors.find(d => d.name === updatedMovie.director);
if (directorObj) {
directorId = directorObj.id;
} else {
// If director doesn't exist, create a new one
const newDirector = {
name: updatedMovie.director,
birthYear: "Unknown",
country: "Unknown"
};
const dirResponse = await fetch(`${this.apiUrl}/directors`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newDirector)
});
if (!dirResponse.ok) {
throw new Error('Failed to add new director');
}
const createdDirector = await dirResponse.json();
this.directors.push(createdDirector);
directorId = createdDirector.id;
}
const movieData = {
title: updatedMovie.title,
directorId,
year: updatedMovie.year,
description: updatedMovie.description || '',
poster: updatedMovie.poster,
genreIds
};
const response = await fetch(`${this.apiUrl}/movies/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(movieData)
});
if (!response.ok) {
throw new Error('Failed to update movie');
}
const updatedMovieData = await response.json();
const processedMovie = this.processMovie(updatedMovieData);
const index = this.movies.findIndex(movie => movie.id === id);
if (index !== -1) {
this.movies[index] = processedMovie;
return processedMovie;
}
return null;
} catch (error) {
console.error('Error updating movie:', error);
// Fallback to local storage
const index = this.movies.findIndex(movie => movie.id === id);
if (index !== -1) {
this.movies[index] = { ...this.movies[index], ...updatedMovie };
this.saveToStorage();
return this.movies[index];
}
return null;
}
}
// Helper methods for local storage fallback
addMovieLocally(movie) {
const newMovie = {
id: Date.now().toString(),
...movie
};
this.movies.push(newMovie);
this.saveToStorage();
return newMovie;
}
deleteMovieLocally(id) {
this.movies = this.movies.filter(movie => movie.id !== id);
this.saveToStorage();
return true;
}
saveToStorage() {
localStorage.setItem('movies', JSON.stringify(this.movies));
}
filterMovies(filters = {}) {
return this.movies.filter(movie => {
let match = true;
if (filters.genre && filters.genre !== 'all') {
match = match && movie.genres.includes(filters.genre);
}
if (filters.year && filters.year !== 'all') {
match = match && movie.year === filters.year;
}
if (filters.search) {
const searchLower = filters.search.toLowerCase();
match = match && (
movie.title.toLowerCase().includes(searchLower) ||
movie.director.toLowerCase().includes(searchLower)
);
}
return match;
});
}
getSampleMovies() {
return [
{
id: '1',
title: 'Груз 200',
director: 'Алексей Балабанов',
genres: ['Триллер', 'Драма', 'Криминал'],
year: '2007',
description: 'Действие фильма происходит в 1984 году в провинциальном городе. Молодая девушка оказывается в руках маньяка, который представляется сотрудником милиции.',
poster: 'resources/movies/gruz.jpeg'
},
{
id: '2',
title: 'Брат',
director: 'Алексей Балабанов',
genres: ['Драма', 'Криминал', 'Боевик'],
year: '1997',
description: 'Демобилизовавшись, Данила Багров возвращается в родной городок. Но скучная жизнь провинциального городка не устраивает его, и он решает поехать в Петербург, где, по слухам, уже давно процветает его старший брат.',
poster: 'resources/movies/brat.webp'
},
{
id: '3',
title: 'Зеленый слоник',
director: 'Светлана Баскова',
genres: ['Драма', 'Арт-хаус'],
year: '1999',
description: 'Два офицера, "Младший лейтенант" и "Капитан", сидят в одной камере на гауптвахте. Капитан — дослуживающий до пенсии армейский алкоголик, а Младший лейтенант — молодой офицер, мечтающий о карьере.',
poster: 'resources/movies/slonik.jpg'
}
];
}
}

View File

@@ -1,277 +0,0 @@
// Import Bootstrap modal functionality
import { Modal } from 'bootstrap';
export class MovieView {
constructor() {
this.movieContainer = document.getElementById('movieContainer');
// Create add movie button if we're on the catalog page
if (window.location.pathname.includes('catalog.html')) {
this.createAddMovieButton();
}
// Initialize featured movies section if we're on the homepage
if (window.location.pathname.endsWith('index.html') || window.location.pathname.endsWith('/')) {
this.initializeHomepage();
}
}
// Initialize homepage elements
initializeHomepage() {
// We'll use the same movieContainer for homepage
if (!this.movieContainer) {
console.error('Movie container element not found on homepage');
return;
}
// Add a heading for the featured movies section if not already present
const parentSection = this.movieContainer.closest('section');
if (parentSection) {
const heading = parentSection.querySelector('h2');
if (heading) {
heading.textContent = 'Популярные фильмы';
heading.className = 'text-orange mb-4';
}
}
// Add a button to add movies on the homepage too
this.createAddMovieButton();
}
// Create "Add Movie" button and append it to the page
createAddMovieButton() {
// Find the appropriate container - either .mb-5 or the section containing movieContainer
let container = document.querySelector('.mb-5');
if (!container && this.movieContainer) {
container = this.movieContainer.closest('section');
}
if (!container) return;
// Check if button already exists
if (container.querySelector('.add-movie-btn')) return;
const buttonContainer = document.createElement('div');
buttonContainer.className = 'mb-4 d-flex justify-content-end';
const addButton = document.createElement('a');
addButton.className = 'btn btn-success add-movie-btn';
addButton.href = 'add-movie.html';
addButton.innerHTML = '<i class="bi bi-plus-circle me-2"></i>Добавить фильм';
buttonContainer.appendChild(addButton);
container.insertBefore(buttonContainer, this.movieContainer);
}
// Bind the save new movie form submit event
bindSaveNewMovie(handler) {
const addMovieForm = document.getElementById('addMovieForm');
if (!addMovieForm) return;
addMovieForm.addEventListener('submit', (e) => {
e.preventDefault();
const newMovie = {
title: document.getElementById('movieTitle').value,
director: document.getElementById('movieDirector').value,
genres: Array.from(document.getElementById('movieGenre').selectedOptions).map(option => option.text),
year: document.getElementById('movieYear').value,
description: document.getElementById('movieDescription').value,
poster: document.getElementById('posterPreview')?.src || 'resources/movies/placeholder.jpg'
};
handler(newMovie);
// Redirect back to catalog after adding
window.location.href = 'catalog.html';
});
}
createMovieElement(movie) {
const movieCard = document.createElement('div');
movieCard.className = 'card movie-card h-100 bg-dark';
movieCard.dataset.movieId = movie.id;
movieCard.innerHTML = `
<img src="${movie.poster}" class="card-img-top" alt="${movie.title} Poster">
<div class="card-body">
<h5 class="card-title text-white"><i class="bi bi-film text-orange me-2"></i>${movie.title}</h5>
<p class="card-text text-white"><i class="bi bi-person-video3 me-2"></i>Режиссер: ${movie.director}</p>
<p class="card-text text-light"><i class="bi bi-tags me-2"></i>Жанр: ${Array.isArray(movie.genres) ? movie.genres.join(', ') : movie.genres}</p>
<p class="card-text text-light"><i class="bi bi-calendar3 me-2"></i>Год выпуска: ${movie.year}</p>
${movie.description ? `<p class="card-text text-light"><i class="bi bi-text-paragraph me-2"></i>Описание: ${movie.description}</p>` : ''}
</div>
<div class="card-footer bg-transparent border-0 d-flex justify-content-between">
<a href="about.html" class="btn btn-orange"><i class="bi bi-play-circle me-2"></i>Смотреть</a>
<button class="btn btn-danger delete-movie"><i class="bi bi-trash"></i> Удалить</button>
</div>
`;
return movieCard;
}
bindDeleteMovie(handler) {
if (!this.movieContainer) return;
// Use event delegation to handle delete button clicks
this.movieContainer.addEventListener('click', (e) => {
if (e.target.closest('.delete-movie')) {
const movieCard = e.target.closest('.movie-card');
const movieId = movieCard.dataset.movieId;
if (confirm('Вы уверены, что хотите удалить этот фильм?')) {
handler(movieId);
}
}
});
}
renderMovies(movies) {
// Make sure movies is an array before using forEach
if (!Array.isArray(movies)) {
console.error('Expected movies to be an array but got:', movies);
movies = [];
}
// Use movieContainer property instead of looking for 'movies-container'
if (!this.movieContainer) {
console.error('Movie container element not found. Make sure an element with id "movieContainer" exists in your HTML.');
return;
}
// Save scroll position before clearing the container
const scrollPosition = window.scrollY;
this.movieContainer.innerHTML = '';
// If we're on the homepage, only show up to 6 featured movies
const isHomepage = window.location.pathname.endsWith('index.html') || window.location.pathname.endsWith('/');
const moviesToShow = isHomepage ? movies.slice(0, 6) : movies;
moviesToShow.forEach(movie => {
const movieCard = this.createMovieElement(movie);
this.movieContainer.appendChild(movieCard);
});
// If we're on the homepage and there are more movies, add a "See All" button
if (isHomepage && movies.length > 6) {
const seeAllContainer = document.createElement('div');
seeAllContainer.className = 'col-12 text-center mt-4';
const seeAllButton = document.createElement('a');
seeAllButton.href = 'catalog.html';
seeAllButton.className = 'btn btn-primary';
seeAllButton.textContent = 'Смотреть все фильмы';
seeAllContainer.appendChild(seeAllButton);
this.movieContainer.parentNode.appendChild(seeAllContainer);
}
// Restore scroll position after rendering
window.scrollTo(0, scrollPosition);
}
fillEditModal(movie) {
if (!this.editModal) return;
const titleInput = this.editModal.querySelector('#editMovieTitle');
const directorInput = this.editModal.querySelector('#editMovieDirector');
const genreSelect = this.editModal.querySelector('#editMovieGenre');
const yearInput = this.editModal.querySelector('#editMovieYear');
const descriptionInput = this.editModal.querySelector('#editMovieDescription');
const posterPreview = this.editModal.querySelector('#editPosterPreview');
titleInput.value = movie.title;
directorInput.value = movie.director;
// Handle genres (multi-select)
if (Array.isArray(movie.genres)) {
Array.from(genreSelect.options).forEach(option => {
option.selected = movie.genres.includes(option.text);
});
}
yearInput.value = movie.year;
descriptionInput.value = movie.description || '';
if (movie.poster) {
posterPreview.src = movie.poster;
this.editModal.querySelector('.preview-container').classList.remove('d-none');
}
this.editModal.dataset.movieId = movie.id;
}
bindEditMovie(handler) {
if (!this.movieContainer) return;
this.movieContainer.addEventListener('click', (e) => {
if (e.target.closest('.edit-movie')) {
const movieCard = e.target.closest('.movie-card');
const movieId = movieCard.dataset.movieId;
handler(movieId);
}
});
}
bindSaveEditedMovie(handler) {
if (!this.editModal) return;
const form = this.editModal.querySelector('form');
form.addEventListener('submit', (e) => {
e.preventDefault();
const movieId = this.editModal.dataset.movieId;
const updatedMovie = {
title: form.querySelector('#editMovieTitle').value,
director: form.querySelector('#editMovieDirector').value,
genres: Array.from(form.querySelector('#editMovieGenre').selectedOptions).map(option => option.text),
year: form.querySelector('#editMovieYear').value,
description: form.querySelector('#editMovieDescription').value,
poster: form.querySelector('#editPosterPreview').src
};
handler(movieId, updatedMovie);
// Close modal using Bootstrap
bootstrap.Modal.getInstance(this.editModal).hide();
});
}
bindFilterMovies(handler) {
const genreSelect = document.getElementById('genre-select');
const yearSelect = document.getElementById('year-select');
const searchInput = document.getElementById('search-input');
if (genreSelect) {
genreSelect.addEventListener('change', () => {
const filters = {
genre: genreSelect.value,
year: yearSelect ? yearSelect.value : 'all',
search: searchInput ? searchInput.value : ''
};
handler(filters);
});
}
if (yearSelect) {
yearSelect.addEventListener('change', () => {
const filters = {
genre: genreSelect ? genreSelect.value : 'all',
year: yearSelect.value,
search: searchInput ? searchInput.value : ''
};
handler(filters);
});
}
if (searchInput) {
searchInput.addEventListener('input', () => {
const filters = {
genre: genreSelect ? genreSelect.value : 'all',
year: yearSelect ? yearSelect.value : 'all',
search: searchInput.value
};
handler(filters);
});
}
}
}