Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca8329cbb2 | |||
| 0acf8f02c4 |
10
src/App.jsx
10
src/App.jsx
@@ -6,7 +6,6 @@ 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 (
|
||||
@@ -29,23 +28,17 @@ function App() {
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div className="collapse navbar-collapse" id="navbarNav">
|
||||
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<ul className="navbar-nav">
|
||||
<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>
|
||||
@@ -57,7 +50,6 @@ 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>
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
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`} />
|
||||
@@ -48,18 +36,11 @@ function MovieCard({ movie, onDelete }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-footer d-flex justify-content-between align-items-center">
|
||||
<div className="card-footer d-flex justify-content-between">
|
||||
<Link to={`/movie/${movie.id}`} className="btn btn-orange">
|
||||
<i className="bi bi-play-circle me-1"></i>Смотреть
|
||||
</Link>
|
||||
<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>
|
||||
<div>
|
||||
<Link to={`/edit-movie/${movie.id}`} className="btn btn-outline-warning edit-movie me-1">
|
||||
<i className="bi bi-pencil"></i>
|
||||
</Link>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
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;
|
||||
@@ -1,17 +1,13 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import MovieService from '../services/MovieService';
|
||||
import useFavorites from './useFavorites'; // Import useFavorites
|
||||
|
||||
function useMovies() {
|
||||
const [allMovies, setAllMovies] = useState([]); // Renamed from movies to allMovies
|
||||
const [filteredMovies, setFilteredMovies] = useState([]); // This will be the final list to display
|
||||
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([]);
|
||||
const { favoriteIds } = useFavorites(); // Get favorite IDs
|
||||
const [showOnlyFavorites, setShowOnlyFavorites] = useState(false); // New state for favorites filter
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMoviesAndGenres = async () => {
|
||||
@@ -19,13 +15,12 @@ function useMovies() {
|
||||
setError(null);
|
||||
try {
|
||||
const data = await MovieService.getMovies();
|
||||
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();
|
||||
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('Не удалось загрузить фильмы. Пожалуйста, попробуйте позже.');
|
||||
@@ -37,54 +32,39 @@ 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);
|
||||
setAllMovies(prevMovies => prevMovies.filter(movie => movie.id !== id));
|
||||
// No need to setFilteredMovies here, the useEffect above will handle it
|
||||
setMovies(prevMovies => prevMovies.filter(movie => movie.id !== id));
|
||||
} catch (err) {
|
||||
console.error('Error deleting movie:', err);
|
||||
setError('Не удалось удалить фильм.');
|
||||
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, // Expose filteredMovies as movies
|
||||
movies: filteredMovies, // Return filtered movies
|
||||
loading,
|
||||
error,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
selectedGenre,
|
||||
setSelectedGenre,
|
||||
genres,
|
||||
genres, // Original list of unique genres for the dropdown
|
||||
handleDeleteMovie,
|
||||
showOnlyFavorites, // Expose new state
|
||||
setShowOnlyFavorites // Expose setter for new state
|
||||
allMovies: movies // Raw list of movies if needed elsewhere, though typically not exposed directly
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,52 +1,48 @@
|
||||
import React from 'react';
|
||||
import MovieList from '../components/Movie/MovieList';
|
||||
|
||||
import useMovies from '../hooks/useMovies';
|
||||
// import MovieService from '../services/MovieService'; // No longer needed here
|
||||
import useMovies from '../hooks/useMovies'; // Import the custom hook
|
||||
|
||||
function CatalogPage() {
|
||||
const {
|
||||
movies,
|
||||
movies, // This is now filteredMovies from the hook
|
||||
loading,
|
||||
error,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
selectedGenre,
|
||||
setSelectedGenre,
|
||||
genres,
|
||||
handleDeleteMovie,
|
||||
showOnlyFavorites,
|
||||
setShowOnlyFavorites
|
||||
genres, // This is the unique genres list from the hook
|
||||
handleDeleteMovie
|
||||
} = useMovies();
|
||||
|
||||
|
||||
// useEffect and handleDeleteMovie logic is now in useMovies hook
|
||||
// Filtered movies logic is also in useMovies hook
|
||||
|
||||
if (loading && movies.length === 0) {
|
||||
return <div className="text-center py-5"><div className="spinner-border text-orange" role="status"></div></div>;
|
||||
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="alert alert-danger">{error}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<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">
|
||||
<div>
|
||||
<div className="row mb-4">
|
||||
<div className="col-md-6 mb-3">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Поиск по названию..."
|
||||
placeholder="Поиск по названию или режиссеру"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<select
|
||||
className="form-select"
|
||||
value={selectedGenre}
|
||||
<div className="col-md-6 mb-3">
|
||||
<select
|
||||
className="form-select"
|
||||
value={selectedGenre}
|
||||
onChange={(e) => setSelectedGenre(e.target.value)}
|
||||
>
|
||||
<option value="">Все жанры</option>
|
||||
@@ -55,27 +51,10 @@ 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>
|
||||
|
||||
{/* Movie List */}
|
||||
{movies.length > 0 ? (
|
||||
<MovieList movies={movies} onDelete={handleDeleteMovie} />
|
||||
) : (
|
||||
!loading && <p>Фильмы не найдены. Попробуйте изменить критерии поиска.</p>
|
||||
)}
|
||||
{/* Pass the movies (which are already filtered) and onDeleteMovie from the hook */}
|
||||
<MovieList movies={movies} onDeleteMovie={handleDeleteMovie} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
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;
|
||||
BIN
~$Отчет.docx
BIN
~$Отчет.docx
Binary file not shown.
BIN
Отчет.docx
BIN
Отчет.docx
Binary file not shown.
Reference in New Issue
Block a user