4 Commits
lab5 ... lab6

Author SHA1 Message Date
915c3dba11 nerd 2025-05-27 21:09:27 +04:00
13ea8afc38 yfpdfybz 2025-05-26 12:46:01 +04:00
c6cb30ae37 отчет 2025-05-26 12:40:00 +04:00
LivelyPuer
a88039abc6 lab6 2025-05-07 10:53:11 +04:00
7 changed files with 220 additions and 47 deletions

View File

@@ -6,6 +6,7 @@ import AddMoviePage from './pages/AddMoviePage';
import EditMoviePage from './pages/EditMoviePage';
import AboutPage from './pages/AboutPage';
import MovieDetailsPage from './pages/MovieDetailsPage';
import FavoritesPage from './pages/FavoritesPage';
function App() {
return (
@@ -28,17 +29,23 @@ function App() {
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarNav">
<ul className="navbar-nav">
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
<li className="nav-item">
<Link className="nav-link" to="/">Главная</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/catalog">Каталог</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/favorites">Избранное</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/about">О нас</Link>
</li>
</ul>
<Link to="/add-movie" className="btn btn-orange">
<i className="bi bi-plus-circle me-1"></i>Добавить фильм
</Link>
</div>
</div>
</nav>
@@ -50,6 +57,7 @@ function App() {
<Route path="/add-movie" element={<AddMoviePage />} />
<Route path="/edit-movie/:id" element={<EditMoviePage />} />
<Route path="/movie/:id" element={<MovieDetailsPage />} />
<Route path="/favorites" element={<FavoritesPage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
</div>

View File

@@ -1,13 +1,25 @@
import React from 'react';
import { Link } from 'react-router-dom';
import useFavorites from '../../hooks/useFavorites';
function MovieCard({ movie, onDelete }) {
const { addFavorite, removeFavorite, isFavorite } = useFavorites();
const isFav = isFavorite(movie.id);
const handleDelete = () => {
if (window.confirm('Вы уверены, что хотите удалить этот фильм?')) {
onDelete(movie.id);
}
};
const handleToggleFavorite = () => {
if (isFav) {
removeFavorite(movie.id);
} else {
addFavorite(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`} />
@@ -36,11 +48,18 @@ function MovieCard({ movie, onDelete }) {
</div>
)}
</div>
<div className="card-footer d-flex justify-content-between">
<div className="card-footer d-flex justify-content-between align-items-center">
<Link to={`/movie/${movie.id}`} className="btn btn-orange">
<i className="bi bi-play-circle me-1"></i>Смотреть
</Link>
<div>
<div className='d-flex align-items-center'>
<button
onClick={handleToggleFavorite}
className={`btn ${isFav ? 'btn-danger' : 'btn-outline-danger'} me-1`}
title={isFav ? 'Удалить из избранного' : 'Добавить в избранное'}
>
<i className={`bi ${isFav ? 'bi-heart-fill' : 'bi-heart'}`}></i>
</button>
<Link to={`/edit-movie/${movie.id}`} className="btn btn-outline-warning edit-movie me-1">
<i className="bi bi-pencil"></i>
</Link>

45
src/hooks/useFavorites.js Normal file
View File

@@ -0,0 +1,45 @@
import { useState, useEffect, useCallback } from 'react';
const FAVORITES_KEY = 'favoriteMovies';
function useFavorites() {
const [favoriteIds, setFavoriteIds] = useState([]);
useEffect(() => {
const storedFavorites = localStorage.getItem(FAVORITES_KEY);
if (storedFavorites) {
setFavoriteIds(JSON.parse(storedFavorites));
}
}, []);
const updateLocalStorage = (ids) => {
localStorage.setItem(FAVORITES_KEY, JSON.stringify(ids));
};
const addFavorite = useCallback((movieId) => {
setFavoriteIds((prevIds) => {
if (!prevIds.includes(movieId)) {
const newIds = [...prevIds, movieId];
updateLocalStorage(newIds);
return newIds;
}
return prevIds;
});
}, []);
const removeFavorite = useCallback((movieId) => {
setFavoriteIds((prevIds) => {
const newIds = prevIds.filter(id => id !== movieId);
updateLocalStorage(newIds);
return newIds;
});
}, []);
const isFavorite = useCallback((movieId) => {
return favoriteIds.includes(movieId);
}, [favoriteIds]);
return { favoriteIds, addFavorite, removeFavorite, isFavorite };
}
export default useFavorites;

View File

@@ -1,13 +1,17 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import MovieService from '../services/MovieService';
import useFavorites from './useFavorites'; // Import useFavorites
function useMovies() {
const [movies, setMovies] = useState([]);
const [allMovies, setAllMovies] = useState([]); // Renamed from movies to allMovies
const [filteredMovies, setFilteredMovies] = useState([]); // This will be the final list to display
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [selectedGenre, setSelectedGenre] = useState('');
const [genres, setGenres] = useState([]);
const { favoriteIds } = useFavorites(); // Get favorite IDs
const [showOnlyFavorites, setShowOnlyFavorites] = useState(false); // New state for favorites filter
useEffect(() => {
const fetchMoviesAndGenres = async () => {
@@ -15,12 +19,13 @@ function useMovies() {
setError(null);
try {
const data = await MovieService.getMovies();
setMovies(data);
const allGenres = data.flatMap(movie => movie.genres || []);
const uniqueGenres = [...new Set(allGenres)];
setAllMovies(data);
// setFilteredMovies(data); // Initial filtering will happen in the next useEffect
const allGenres = data.flatMap(movie => movie.genres || []).filter(genre => genre.trim() !== '');
const uniqueGenres = [...new Set(allGenres)].sort();
setGenres(uniqueGenres);
} catch (err) {
console.error('Error fetching movies:', err);
setError('Не удалось загрузить фильмы. Пожалуйста, попробуйте позже.');
@@ -32,39 +37,54 @@ function useMovies() {
fetchMoviesAndGenres();
}, []);
useEffect(() => {
setLoading(true);
let tempMovies = [...allMovies];
if (showOnlyFavorites) {
tempMovies = tempMovies.filter(movie => favoriteIds.includes(movie.id));
}
if (selectedGenre) {
tempMovies = tempMovies.filter(movie =>
(Array.isArray(movie.genres) && movie.genres.includes(selectedGenre)) ||
(typeof movie.genres === 'string' && movie.genres === selectedGenre)
);
}
if (searchTerm) {
tempMovies = tempMovies.filter(movie =>
movie.title.toLowerCase().includes(searchTerm.toLowerCase())
);
}
setFilteredMovies(tempMovies);
setLoading(false);
}, [searchTerm, selectedGenre, allMovies, favoriteIds, showOnlyFavorites]); // Add dependencies
const handleDeleteMovie = async (id) => {
try {
await MovieService.deleteMovie(id);
setMovies(prevMovies => prevMovies.filter(movie => movie.id !== id));
setAllMovies(prevMovies => prevMovies.filter(movie => movie.id !== id));
// No need to setFilteredMovies here, the useEffect above will handle it
} catch (err) {
console.error('Error deleting movie:', err);
alert('Произошла ошибка при удалении фильма.');
// Optionally, re-throw or set an error state for the component to handle
setError('Не удалось удалить фильм.');
}
};
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
movies: filteredMovies, // Expose filteredMovies as movies
loading,
error,
searchTerm,
setSearchTerm,
selectedGenre,
setSelectedGenre,
genres, // Original list of unique genres for the dropdown
genres,
handleDeleteMovie,
allMovies: movies // Raw list of movies if needed elsewhere, though typically not exposed directly
showOnlyFavorites, // Expose new state
setShowOnlyFavorites // Expose setter for new state
};
}

View File

@@ -1,48 +1,52 @@
import React from 'react';
import MovieList from '../components/Movie/MovieList';
// import MovieService from '../services/MovieService'; // No longer needed here
import useMovies from '../hooks/useMovies'; // Import the custom hook
import useMovies from '../hooks/useMovies';
function CatalogPage() {
const {
movies, // This is now filteredMovies from the hook
movies,
loading,
error,
searchTerm,
setSearchTerm,
selectedGenre,
setSelectedGenre,
genres, // This is the unique genres list from the hook
handleDeleteMovie
genres,
handleDeleteMovie,
showOnlyFavorites,
setShowOnlyFavorites
} = useMovies();
// 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>;
if (loading && movies.length === 0) {
return <div className="text-center py-5"><div className="spinner-border text-orange" role="status"></div></div>;
}
if (error) {
return <div className="alert alert-danger">{error}</div>;
return <div className="container mt-3"><div className="alert alert-danger">{error}</div></div>;
}
return (
<div>
<div className="row mb-4">
<div className="col-md-6 mb-3">
<div className="container mt-3">
<h1 className="mb-4 text-orange">Каталог фильмов</h1>
{/* Filter Controls */}
<div className="row mb-4 g-3 align-items-center">
<div className="col-md-6">
<input
type="text"
className="form-control"
placeholder="Поиск по названию или режиссеру"
placeholder="Поиск по названию..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="col-md-6 mb-3">
<select
className="form-select"
value={selectedGenre}
<div className="col-md-4">
<select
className="form-select"
value={selectedGenre}
onChange={(e) => setSelectedGenre(e.target.value)}
>
<option value="">Все жанры</option>
@@ -51,10 +55,27 @@ function CatalogPage() {
))}
</select>
</div>
<div className="col-md-2">
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
role="switch"
id="favoritesSwitch"
checked={showOnlyFavorites}
onChange={(e) => setShowOnlyFavorites(e.target.checked)}
/>
<label className="form-check-label" htmlFor="favoritesSwitch">Избранное</label>
</div>
</div>
</div>
{/* Pass the movies (which are already filtered) and onDeleteMovie from the hook */}
<MovieList movies={movies} onDeleteMovie={handleDeleteMovie} />
{/* Movie List */}
{movies.length > 0 ? (
<MovieList movies={movies} onDelete={handleDeleteMovie} />
) : (
!loading && <p>Фильмы не найдены. Попробуйте изменить критерии поиска.</p>
)}
</div>
);
}

View File

@@ -0,0 +1,60 @@
import React, { useState, useEffect } from 'react';
import MovieList from '../components/Movie/MovieList';
import MovieService from '../services/MovieService';
import useFavorites from '../hooks/useFavorites';
function FavoritesPage() {
const { favoriteIds, removeFavorite } = useFavorites();
const [favoriteMovies, setFavoriteMovies] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchFavoriteMovies = async () => {
if (favoriteIds.length === 0) {
setFavoriteMovies([]);
setLoading(false);
return;
}
setLoading(true);
setError(null);
try {
const allMovies = await MovieService.getMovies();
const favs = allMovies.filter(movie => favoriteIds.includes(movie.id));
setFavoriteMovies(favs);
} catch (err) {
console.error('Error fetching favorite movies:', err);
setError('Не удалось загрузить избранные фильмы.');
} finally {
setLoading(false);
}
};
fetchFavoriteMovies();
}, [favoriteIds]);
const handleDeleteFromFavorites = (movieId) => {
removeFavorite(movieId);
};
if (loading) {
return <div className="text-center py-5"><div className="spinner-border" role="status"></div></div>;
}
if (error) {
return <div className="container mt-3"><div className="alert alert-danger">{error}</div></div>;
}
return (
<div className="container mt-3">
<h1 className="mb-4 text-orange">Избранные фильмы</h1>
{favoriteMovies.length > 0 ? (
<MovieList movies={favoriteMovies} onDelete={handleDeleteFromFavorites} />
) : (
<p>У вас пока нет избранных фильмов. Вы можете добавить их из каталога.</p>
)}
</div>
);
}
export default FavoritesPage;

Binary file not shown.