mirror of
https://gitlab.com/foxixus/neomovies.git
synced 2025-10-27 17:38:50 +05:00
Add categories
This commit is contained in:
485
src/app/categories/[id]/page.tsx
Normal file
485
src/app/categories/[id]/page.tsx
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { categoriesAPI } from '@/lib/api';
|
||||||
|
import MovieCard from '@/components/MovieCard';
|
||||||
|
import { Movie, Category } from '@/lib/api';
|
||||||
|
|
||||||
|
// Styled Components
|
||||||
|
const Container = styled.div`
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title = styled.h1`
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #fff;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ButtonsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabButton = styled.button<{ $active?: boolean }>`
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
background: ${props => props.$active ? '#3182ce' : 'rgba(255, 255, 255, 0.1)'};
|
||||||
|
color: ${props => props.$active ? '#fff' : 'rgba(255, 255, 255, 0.8)'};
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${props => props.$active ? '#2b6cb0' : 'rgba(255, 255, 255, 0.2)'};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BackButton = styled.button`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MediaGrid = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const PaginationContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const PaginationButton = styled.button<{ $active?: boolean }>`
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
background: ${props => props.$active ? '#3182ce' : 'rgba(255, 255, 255, 0.1)'};
|
||||||
|
color: ${props => props.$active ? '#fff' : 'rgba(255, 255, 255, 0.8)'};
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
min-width: 2.5rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${props => props.$active ? '#2b6cb0' : 'rgba(255, 255, 255, 0.2)'};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LoadingContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 200px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Spinner = styled.div`
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-left-color: #3182ce;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ErrorMessage = styled.div`
|
||||||
|
color: #fc8181;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
background: rgba(252, 129, 129, 0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type MediaType = 'movies' | 'tv';
|
||||||
|
|
||||||
|
function CategoryPage() {
|
||||||
|
// Используем хук useParams вместо props
|
||||||
|
const params = useParams();
|
||||||
|
const categoryId = parseInt(params.id as string);
|
||||||
|
|
||||||
|
const [category, setCategory] = useState<Category | null>(null);
|
||||||
|
const [mediaType, setMediaType] = useState<MediaType>('movies');
|
||||||
|
const [movies, setMovies] = useState<Movie[]>([]);
|
||||||
|
const [tvShows, setTvShows] = useState<Movie[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
const [moviesAvailable, setMoviesAvailable] = useState(true);
|
||||||
|
const [tvShowsAvailable, setTvShowsAvailable] = useState(true);
|
||||||
|
|
||||||
|
// Загрузка информации о категории
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchCategory() {
|
||||||
|
try {
|
||||||
|
const response = await categoriesAPI.getCategory(categoryId);
|
||||||
|
setCategory(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching category:', error);
|
||||||
|
setError('Не удалось загрузить информацию о категории');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryId) {
|
||||||
|
fetchCategory();
|
||||||
|
}
|
||||||
|
}, [categoryId]);
|
||||||
|
|
||||||
|
// Загрузка фильмов по категории
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchMovies() {
|
||||||
|
if (!categoryId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await categoriesAPI.getMoviesByCategory(categoryId, page);
|
||||||
|
|
||||||
|
if (response.data.results) {
|
||||||
|
// Добавляем дебаг-логи
|
||||||
|
console.log(`Получены фильмы для категории ${categoryId}, страница ${page}:`, {
|
||||||
|
count: response.data.results.length,
|
||||||
|
ids: response.data.results.slice(0, 5).map(m => m.id),
|
||||||
|
titles: response.data.results.slice(0, 5).map(m => m.title)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем, есть ли фильмы в этой категории
|
||||||
|
const hasMovies = response.data.results.length > 0;
|
||||||
|
setMoviesAvailable(hasMovies);
|
||||||
|
|
||||||
|
// Если фильмов нет, а выбран тип "movies", пробуем переключиться на сериалы
|
||||||
|
if (!hasMovies && mediaType === 'movies' && tvShowsAvailable) {
|
||||||
|
setMediaType('tv');
|
||||||
|
} else {
|
||||||
|
setMovies(response.data.results);
|
||||||
|
|
||||||
|
// Устанавливаем общее количество страниц
|
||||||
|
if (response.data.total_pages) {
|
||||||
|
setTotalPages(response.data.total_pages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setMoviesAvailable(false);
|
||||||
|
if (mediaType === 'movies' && tvShowsAvailable) {
|
||||||
|
setMediaType('tv');
|
||||||
|
} else {
|
||||||
|
setError('Не удалось загрузить фильмы');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching movies:', error);
|
||||||
|
setMoviesAvailable(false);
|
||||||
|
if (mediaType === 'movies' && tvShowsAvailable) {
|
||||||
|
setMediaType('tv');
|
||||||
|
} else {
|
||||||
|
setError('Ошибка при загрузке фильмов');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaType === 'movies') {
|
||||||
|
fetchMovies();
|
||||||
|
}
|
||||||
|
}, [categoryId, mediaType, page, tvShowsAvailable]);
|
||||||
|
|
||||||
|
// Загрузка сериалов по категории
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchTVShows() {
|
||||||
|
if (!categoryId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await categoriesAPI.getTVShowsByCategory(categoryId, page);
|
||||||
|
|
||||||
|
if (response.data.results) {
|
||||||
|
// Добавляем дебаг-логи
|
||||||
|
console.log(`Получены сериалы для категории ${categoryId}, страница ${page}:`, {
|
||||||
|
count: response.data.results.length,
|
||||||
|
ids: response.data.results.slice(0, 5).map(tv => tv.id),
|
||||||
|
names: response.data.results.slice(0, 5).map(tv => tv.name)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем, есть ли сериалы в этой категории
|
||||||
|
const hasTVShows = response.data.results.length > 0;
|
||||||
|
setTvShowsAvailable(hasTVShows);
|
||||||
|
|
||||||
|
// Если сериалов нет, а выбран тип "tv", пробуем переключиться на фильмы
|
||||||
|
if (!hasTVShows && mediaType === 'tv' && moviesAvailable) {
|
||||||
|
setMediaType('movies');
|
||||||
|
} else {
|
||||||
|
setTvShows(response.data.results);
|
||||||
|
|
||||||
|
// Устанавливаем общее количество страниц
|
||||||
|
if (response.data.total_pages) {
|
||||||
|
setTotalPages(response.data.total_pages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setTvShowsAvailable(false);
|
||||||
|
if (mediaType === 'tv' && moviesAvailable) {
|
||||||
|
setMediaType('movies');
|
||||||
|
} else {
|
||||||
|
setError('Не удалось загрузить сериалы');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching TV shows:', error);
|
||||||
|
setTvShowsAvailable(false);
|
||||||
|
if (mediaType === 'tv' && moviesAvailable) {
|
||||||
|
setMediaType('movies');
|
||||||
|
} else {
|
||||||
|
setError('Ошибка при загрузке сериалов');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaType === 'tv') {
|
||||||
|
fetchTVShows();
|
||||||
|
}
|
||||||
|
}, [categoryId, mediaType, page, moviesAvailable]);
|
||||||
|
|
||||||
|
function handleGoBack() {
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функции для пагинации
|
||||||
|
function handlePageChange(newPage: number) {
|
||||||
|
if (newPage >= 1 && newPage <= totalPages) {
|
||||||
|
setPage(newPage);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPagination() {
|
||||||
|
if (totalPages <= 1) return null;
|
||||||
|
|
||||||
|
const pageButtons = [];
|
||||||
|
// Отображаем максимум 5 страниц вокруг текущей
|
||||||
|
const startPage = Math.max(1, page - 2);
|
||||||
|
const endPage = Math.min(totalPages, startPage + 4);
|
||||||
|
|
||||||
|
// Кнопка "Предыдущая"
|
||||||
|
pageButtons.push(
|
||||||
|
<PaginationButton
|
||||||
|
key="prev"
|
||||||
|
onClick={() => handlePageChange(page - 1)}
|
||||||
|
disabled={page === 1}
|
||||||
|
>
|
||||||
|
<
|
||||||
|
</PaginationButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Отображаем первую страницу и многоточие, если startPage > 1
|
||||||
|
if (startPage > 1) {
|
||||||
|
pageButtons.push(
|
||||||
|
<PaginationButton
|
||||||
|
key="1"
|
||||||
|
onClick={() => handlePageChange(1)}
|
||||||
|
$active={page === 1}
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</PaginationButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (startPage > 2) {
|
||||||
|
pageButtons.push(
|
||||||
|
<span key="dots1" style={{ color: 'white' }}>...</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отображаем страницы вокруг текущей
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
pageButtons.push(
|
||||||
|
<PaginationButton
|
||||||
|
key={i}
|
||||||
|
onClick={() => handlePageChange(i)}
|
||||||
|
$active={page === i}
|
||||||
|
>
|
||||||
|
{i}
|
||||||
|
</PaginationButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отображаем многоточие и последнюю страницу, если endPage < totalPages
|
||||||
|
if (endPage < totalPages) {
|
||||||
|
if (endPage < totalPages - 1) {
|
||||||
|
pageButtons.push(
|
||||||
|
<span key="dots2" style={{ color: 'white' }}>...</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pageButtons.push(
|
||||||
|
<PaginationButton
|
||||||
|
key={totalPages}
|
||||||
|
onClick={() => handlePageChange(totalPages)}
|
||||||
|
$active={page === totalPages}
|
||||||
|
>
|
||||||
|
{totalPages}
|
||||||
|
</PaginationButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка "Следующая"
|
||||||
|
pageButtons.push(
|
||||||
|
<PaginationButton
|
||||||
|
key="next"
|
||||||
|
onClick={() => handlePageChange(page + 1)}
|
||||||
|
disabled={page === totalPages}
|
||||||
|
>
|
||||||
|
>
|
||||||
|
</PaginationButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <PaginationContainer>{pageButtons}</PaginationContainer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<BackButton onClick={handleGoBack}>
|
||||||
|
<span>←</span> Назад к категориям
|
||||||
|
</BackButton>
|
||||||
|
<ErrorMessage>{error}</ErrorMessage>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<BackButton onClick={handleGoBack}>
|
||||||
|
<span>←</span> Назад к категориям
|
||||||
|
</BackButton>
|
||||||
|
|
||||||
|
<Title>{category?.name || 'Загрузка...'}</Title>
|
||||||
|
|
||||||
|
<ButtonsContainer>
|
||||||
|
<TabButton
|
||||||
|
$active={mediaType === 'movies'}
|
||||||
|
onClick={() => {
|
||||||
|
if (moviesAvailable) {
|
||||||
|
setMediaType('movies');
|
||||||
|
setPage(1); // Сбрасываем страницу при переключении типа контента
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!moviesAvailable}
|
||||||
|
style={{ opacity: moviesAvailable ? 1 : 0.5, cursor: moviesAvailable ? 'pointer' : 'not-allowed' }}
|
||||||
|
>
|
||||||
|
Фильмы
|
||||||
|
</TabButton>
|
||||||
|
<TabButton
|
||||||
|
$active={mediaType === 'tv'}
|
||||||
|
onClick={() => {
|
||||||
|
if (tvShowsAvailable) {
|
||||||
|
setMediaType('tv');
|
||||||
|
setPage(1); // Сбрасываем страницу при переключении типа контента
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!tvShowsAvailable}
|
||||||
|
style={{ opacity: tvShowsAvailable ? 1 : 0.5, cursor: tvShowsAvailable ? 'pointer' : 'not-allowed' }}
|
||||||
|
>
|
||||||
|
Сериалы
|
||||||
|
</TabButton>
|
||||||
|
</ButtonsContainer>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<LoadingContainer>
|
||||||
|
<Spinner />
|
||||||
|
</LoadingContainer>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MediaGrid>
|
||||||
|
{mediaType === 'movies' ? (
|
||||||
|
movies.length > 0 ? (
|
||||||
|
movies.map(movie => (
|
||||||
|
<MovieCard
|
||||||
|
key={`movie-${categoryId}-${movie.id}-${page}`}
|
||||||
|
movie={movie}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div style={{ gridColumn: '1/-1', textAlign: 'center', padding: '2rem' }}>
|
||||||
|
Нет фильмов в этой категории
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
tvShows.length > 0 ? (
|
||||||
|
tvShows.map(tvShow => (
|
||||||
|
<MovieCard
|
||||||
|
key={`tv-${categoryId}-${tvShow.id}-${page}`}
|
||||||
|
movie={tvShow}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div style={{ gridColumn: '1/-1', textAlign: 'center', padding: '2rem' }}>
|
||||||
|
Нет сериалов в этой категории
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</MediaGrid>
|
||||||
|
|
||||||
|
{/* Отображаем пагинацию */}
|
||||||
|
{renderPagination()}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoryPage;
|
||||||
@@ -2,14 +2,9 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { moviesAPI } from '@/lib/api';
|
import { categoriesAPI } from '@/lib/api';
|
||||||
import MovieCard from '@/components/MovieCard';
|
import { Category } from '@/lib/api';
|
||||||
import { Movie } from '@/lib/api';
|
import CategoryCard from '@/components/CategoryCard';
|
||||||
|
|
||||||
interface Genre {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Styled Components
|
// Styled Components
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
@@ -18,50 +13,36 @@ const Container = styled.div`
|
|||||||
padding: 2rem 1rem;
|
padding: 2rem 1rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Grid = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const Title = styled.h1`
|
const Title = styled.h1`
|
||||||
font-size: 1.5rem;
|
font-size: 2rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const GenreButtons = styled.div`
|
const Subtitle = styled.p`
|
||||||
display: flex;
|
color: rgba(255, 255, 255, 0.7);
|
||||||
gap: 0.5rem;
|
font-size: 1rem;
|
||||||
|
margin-top: -0.5rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
flex-wrap: wrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GenreButton = styled.button<{ $active?: boolean }>`
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
background: ${props => props.$active ? '#3182ce' : 'rgba(255, 255, 255, 0.1)'};
|
|
||||||
color: ${props => props.$active ? '#fff' : 'rgba(255, 255, 255, 0.8)'};
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: ${props => props.$active ? '#2b6cb0' : 'rgba(255, 255, 255, 0.2)'};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MovieGrid = styled.div`
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const LoadingContainer = styled.div`
|
const LoadingContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 200px;
|
min-height: 300px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Spinner = styled.div`
|
const Spinner = styled.div`
|
||||||
@@ -88,71 +69,90 @@ const ErrorMessage = styled.div`
|
|||||||
margin: 2rem 0;
|
margin: 2rem 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function CategoriesPage() {
|
interface CategoryWithBackground extends Category {
|
||||||
const [genres, setGenres] = useState<Genre[]>([]);
|
backgroundUrl?: string;
|
||||||
const [selectedGenre, setSelectedGenre] = useState<number | null>(null);
|
}
|
||||||
const [movies, setMovies] = useState<Movie[]>([]);
|
|
||||||
|
function CategoriesPage() {
|
||||||
|
const [categories, setCategories] = useState<CategoryWithBackground[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Загрузка жанров при монтировании
|
// Загрузка категорий и фоновых изображений для них
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchGenres = async () => {
|
async function fetchCategoriesAndBackgrounds() {
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
|
||||||
console.log('Fetching genres...');
|
|
||||||
const response = await moviesAPI.getGenres();
|
|
||||||
console.log('Genres response:', response.data);
|
|
||||||
|
|
||||||
if (response.data.genres && response.data.genres.length > 0) {
|
|
||||||
setGenres(response.data.genres);
|
|
||||||
setSelectedGenre(response.data.genres[0].id);
|
|
||||||
} else {
|
|
||||||
setError('Не удалось загрузить жанры');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching genres:', error);
|
|
||||||
setError('Ошибка при загрузке жанров');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchGenres();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Загрузка фильмов при изменении выбранного жанра
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchMoviesByGenre = async () => {
|
|
||||||
if (!selectedGenre) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
console.log('Fetching movies for genre:', selectedGenre);
|
|
||||||
const response = await moviesAPI.getMoviesByGenre(selectedGenre);
|
|
||||||
console.log('Movies response:', {
|
|
||||||
total: response.data.results?.length,
|
|
||||||
first: response.data.results?.[0]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.results) {
|
try {
|
||||||
setMovies(response.data.results);
|
// Получаем список категорий
|
||||||
} else {
|
const categoriesResponse = await categoriesAPI.getCategories();
|
||||||
setError('Не удалось загрузить фильмы');
|
|
||||||
|
if (!categoriesResponse.data.categories || categoriesResponse.data.categories.length === 0) {
|
||||||
|
setError('Не удалось загрузить категории');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Добавляем фоновые изображения для каждой категории
|
||||||
|
const categoriesWithBackgrounds: CategoryWithBackground[] = await Promise.all(
|
||||||
|
categoriesResponse.data.categories.map(async (category: Category) => {
|
||||||
|
try {
|
||||||
|
// Сначала пробуем получить фильм для фона
|
||||||
|
const moviesResponse = await categoriesAPI.getMoviesByCategory(category.id, 1);
|
||||||
|
|
||||||
|
// Проверяем, есть ли фильмы в данной категории
|
||||||
|
if (moviesResponse.data.results && moviesResponse.data.results.length > 0) {
|
||||||
|
const backgroundUrl = moviesResponse.data.results[0].backdrop_path ||
|
||||||
|
moviesResponse.data.results[0].poster_path;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...category,
|
||||||
|
backgroundUrl
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Если фильмов нет, пробуем получить сериалы
|
||||||
|
const tvResponse = await categoriesAPI.getTVShowsByCategory(category.id, 1);
|
||||||
|
|
||||||
|
if (tvResponse.data.results && tvResponse.data.results.length > 0) {
|
||||||
|
const backgroundUrl = tvResponse.data.results[0].backdrop_path ||
|
||||||
|
tvResponse.data.results[0].poster_path;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...category,
|
||||||
|
backgroundUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если ни фильмов, ни сериалов не найдено
|
||||||
|
return {
|
||||||
|
...category,
|
||||||
|
backgroundUrl: undefined
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching background for category ${category.id}:`, error);
|
||||||
|
return category; // Возвращаем категорию без фона в случае ошибки
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setCategories(categoriesWithBackgrounds);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching movies:', error);
|
console.error('Error fetching categories:', error);
|
||||||
setError('Ошибка при загрузке фильмов');
|
setError('Ошибка при загрузке категорий');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
fetchMoviesByGenre();
|
fetchCategoriesAndBackgrounds();
|
||||||
}, [selectedGenre]);
|
}, []);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Title>Категории фильмов</Title>
|
<Title>Категории</Title>
|
||||||
<ErrorMessage>{error}</ErrorMessage>
|
<ErrorMessage>{error}</ErrorMessage>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
@@ -160,33 +160,26 @@ export default function CategoriesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Title>Категории фильмов</Title>
|
<Title>Категории</Title>
|
||||||
|
<Subtitle>Различные жанры фильмов и сериалов</Subtitle>
|
||||||
|
|
||||||
{/* Кнопки жанров */}
|
|
||||||
<GenreButtons>
|
|
||||||
{genres.map((genre) => (
|
|
||||||
<GenreButton
|
|
||||||
key={genre.id}
|
|
||||||
$active={selectedGenre === genre.id}
|
|
||||||
onClick={() => setSelectedGenre(genre.id)}
|
|
||||||
>
|
|
||||||
{genre.name}
|
|
||||||
</GenreButton>
|
|
||||||
))}
|
|
||||||
</GenreButtons>
|
|
||||||
|
|
||||||
{/* Сетка фильмов */}
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingContainer>
|
<LoadingContainer>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</LoadingContainer>
|
</LoadingContainer>
|
||||||
) : (
|
) : (
|
||||||
<MovieGrid>
|
<Grid>
|
||||||
{movies.map((movie) => (
|
{categories.map((category) => (
|
||||||
<MovieCard key={movie.id} movie={movie} />
|
<CategoryCard
|
||||||
|
key={category.id}
|
||||||
|
category={category}
|
||||||
|
backgroundUrl={category.backgroundUrl}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</MovieGrid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default CategoriesPage;
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const LoginClient = dynamic(() => import('./LoginClient'), {
|
|
||||||
ssr: false
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<GlowingBackground>
|
|
||||||
<Glow1 />
|
|
||||||
<Glow2 />
|
|
||||||
<Glow3 />
|
|
||||||
</GlowingBackground>
|
|
||||||
|
|
||||||
<Content>
|
|
||||||
<Logo>
|
|
||||||
<span>Neo</span> Movies
|
|
||||||
</Logo>
|
|
||||||
|
|
||||||
<GlassCard>
|
|
||||||
<LoginClient />
|
|
||||||
</GlassCard>
|
|
||||||
</Content>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
min-height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
background-color: #0a0a0a;
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Content = styled.main`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1200px;
|
|
||||||
padding: 2rem;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Logo = styled.h1`
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: #2196f3;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GlassCard = styled.div`
|
|
||||||
background: rgba(0, 0, 0, 0.45);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
padding: 3rem;
|
|
||||||
border-radius: 24px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
box-shadow:
|
|
||||||
0 8px 32px 0 rgba(0, 0, 0, 0.3),
|
|
||||||
inset 0 0 0 1px rgba(255, 255, 255, 0.05);
|
|
||||||
margin: 0 auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GlowingBackground = styled.div`
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Glow = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
filter: blur(100px);
|
|
||||||
opacity: 0.3;
|
|
||||||
animation: float 20s infinite ease-in-out;
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
0%, 100% { transform: translate(0, 0); }
|
|
||||||
50% { transform: translate(-30px, 30px); }
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Glow1 = styled(Glow)`
|
|
||||||
background: #2196f3;
|
|
||||||
width: 600px;
|
|
||||||
height: 600px;
|
|
||||||
top: -200px;
|
|
||||||
left: -200px;
|
|
||||||
animation-delay: 0s;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Glow2 = styled(Glow)`
|
|
||||||
background: #9c27b0;
|
|
||||||
width: 500px;
|
|
||||||
height: 500px;
|
|
||||||
bottom: -150px;
|
|
||||||
right: -150px;
|
|
||||||
animation-delay: -5s;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Glow3 = styled(Glow)`
|
|
||||||
background: #00bcd4;
|
|
||||||
width: 400px;
|
|
||||||
height: 400px;
|
|
||||||
bottom: 100px;
|
|
||||||
left: 30%;
|
|
||||||
animation-delay: -10s;
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ const Content = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const MovieInfo = styled.div`
|
const MovieInfo = styled.div`
|
||||||
@@ -35,11 +39,18 @@ const MovieInfo = styled.div`
|
|||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const PosterContainer = styled.div`
|
const PosterContainer = styled.div`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Poster = styled.img`
|
const Poster = styled.img`
|
||||||
@@ -48,14 +59,23 @@ const Poster = styled.img`
|
|||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
width: 100%;
|
width: 200px;
|
||||||
max-width: 300px;
|
height: auto;
|
||||||
margin: 0 auto;
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
width: 160px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Details = styled.div`
|
const Details = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Title = styled.h1`
|
const Title = styled.h1`
|
||||||
@@ -63,6 +83,16 @@ const Title = styled.h1`
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
color: white;
|
color: white;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Info = styled.div`
|
const Info = styled.div`
|
||||||
@@ -70,11 +100,23 @@ const Info = styled.div`
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const InfoItem = styled.span`
|
const InfoItem = styled.span`
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const GenreList = styled.div`
|
const GenreList = styled.div`
|
||||||
@@ -82,6 +124,10 @@ const GenreList = styled.div`
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Genre = styled.span`
|
const Genre = styled.span`
|
||||||
@@ -90,21 +136,80 @@ const Genre = styled.span`
|
|||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Tagline = styled.div`
|
const Tagline = styled.div`
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
color: rgba(255, 255, 255, 0.6);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Overview = styled.p`
|
const Overview = styled.p`
|
||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
text-align: justify;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ActionButtons = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WatchButton = styled.button`
|
||||||
|
background: #e50914;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f40612;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const PlayerSection = styled.div`
|
const PlayerSection = styled.div`
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const LoadingContainer = styled.div`
|
const LoadingContainer = styled.div`
|
||||||
@@ -157,6 +262,7 @@ export default function MovieContent({ movieId, initialMovie }: MovieContentProp
|
|||||||
<Poster
|
<Poster
|
||||||
src={getImageUrl(movie.poster_path)}
|
src={getImageUrl(movie.poster_path)}
|
||||||
alt={movie.title}
|
alt={movie.title}
|
||||||
|
loading="eager"
|
||||||
/>
|
/>
|
||||||
</PosterContainer>
|
</PosterContainer>
|
||||||
|
|
||||||
@@ -174,19 +280,30 @@ export default function MovieContent({ movieId, initialMovie }: MovieContentProp
|
|||||||
</GenreList>
|
</GenreList>
|
||||||
{movie.tagline && <Tagline>{movie.tagline}</Tagline>}
|
{movie.tagline && <Tagline>{movie.tagline}</Tagline>}
|
||||||
<Overview>{movie.overview}</Overview>
|
<Overview>{movie.overview}</Overview>
|
||||||
<div style={{ marginTop: '1rem' }}>
|
|
||||||
|
<ActionButtons>
|
||||||
|
{imdbId && (
|
||||||
|
<WatchButton
|
||||||
|
onClick={() => document.getElementById('movie-player')?.scrollIntoView({ behavior: 'smooth' })}
|
||||||
|
>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8 5.14V19.14L19 12.14L8 5.14Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
Смотреть
|
||||||
|
</WatchButton>
|
||||||
|
)}
|
||||||
<FavoriteButton
|
<FavoriteButton
|
||||||
mediaId={movie.id.toString()}
|
mediaId={movie.id.toString()}
|
||||||
mediaType="movie"
|
mediaType="movie"
|
||||||
title={movie.title}
|
title={movie.title}
|
||||||
posterPath={movie.poster_path}
|
posterPath={movie.poster_path}
|
||||||
/>
|
/>
|
||||||
</div>
|
</ActionButtons>
|
||||||
</Details>
|
</Details>
|
||||||
</MovieInfo>
|
</MovieInfo>
|
||||||
|
|
||||||
{imdbId && (
|
{imdbId && (
|
||||||
<PlayerSection>
|
<PlayerSection id="movie-player">
|
||||||
<MoviePlayer
|
<MoviePlayer
|
||||||
imdbId={imdbId}
|
imdbId={imdbId}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,15 +9,25 @@ interface PageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Генерация метаданных для страницы
|
// Генерация метаданных для страницы
|
||||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
export async function generateMetadata(
|
||||||
const { id } = params;
|
props: { params: { id: string }}
|
||||||
|
): Promise<Metadata> {
|
||||||
|
// В Next.js 14, нужно сначала получить данные фильма,
|
||||||
|
// а затем использовать их для метаданных
|
||||||
try {
|
try {
|
||||||
const { data: movie } = await moviesAPI.getMovie(id);
|
// Получаем id для использования в запросе
|
||||||
|
const movieId = props.params.id;
|
||||||
|
|
||||||
|
// Запрашиваем данные фильма
|
||||||
|
const { data: movie } = await moviesAPI.getMovie(movieId);
|
||||||
|
|
||||||
|
// Создаем метаданные на основе полученных данных
|
||||||
return {
|
return {
|
||||||
title: `${movie.title} - NeoMovies`,
|
title: `${movie.title} - NeoMovies`,
|
||||||
description: movie.overview,
|
description: movie.overview,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error generating metadata:', error);
|
||||||
return {
|
return {
|
||||||
title: 'Фильм - NeoMovies',
|
title: 'Фильм - NeoMovies',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,13 @@ async function getData(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page(props: PageProps) {
|
export default async function Page(props: PageProps) {
|
||||||
const { id } = props.params;
|
// В Next.js 14 нужно сначала использовать параметры в асинхронной функции
|
||||||
const data = await getData(id);
|
try {
|
||||||
return <TVShowPage tvShowId={data.id} show={data.show} />;
|
const tvShowId = props.params.id;
|
||||||
|
const data = await getData(tvShowId);
|
||||||
|
return <TVShowPage tvShowId={data.id} show={data.show} />;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading TV show page:', error);
|
||||||
|
return <div>Ошибка загрузки страницы сериала</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
136
src/components/CategoryCard.tsx
Normal file
136
src/components/CategoryCard.tsx
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Category } from '@/lib/api';
|
||||||
|
|
||||||
|
interface CategoryCardProps {
|
||||||
|
category: Category;
|
||||||
|
backgroundUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Словарь цветов для разных жанров
|
||||||
|
const genreColors: Record<number, string> = {
|
||||||
|
28: '#E53935', // Боевик - красный
|
||||||
|
12: '#43A047', // Приключения - зеленый
|
||||||
|
16: '#FB8C00', // Мультфильм - оранжевый
|
||||||
|
35: '#FFEE58', // Комедия - желтый
|
||||||
|
80: '#424242', // Криминал - темно-серый
|
||||||
|
99: '#8D6E63', // Документальный - коричневый
|
||||||
|
18: '#5E35B1', // Драма - пурпурный
|
||||||
|
10751: '#EC407A', // Семейный - розовый
|
||||||
|
14: '#7E57C2', // Фэнтези - фиолетовый
|
||||||
|
36: '#795548', // История - коричневый
|
||||||
|
27: '#212121', // Ужасы - черный
|
||||||
|
10402: '#26A69A', // Музыка - бирюзовый
|
||||||
|
9648: '#5C6BC0', // Детектив - индиго
|
||||||
|
10749: '#EC407A', // Мелодрама - розовый
|
||||||
|
878: '#00BCD4', // Фантастика - голубой
|
||||||
|
10770: '#9E9E9E', // ТВ фильм - серый
|
||||||
|
53: '#FFA000', // Триллер - янтарный
|
||||||
|
10752: '#455A64', // Военный - сине-серый
|
||||||
|
37: '#8D6E63', // Вестерн - коричневый
|
||||||
|
// Добавим цвета для популярных жанров сериалов
|
||||||
|
10759: '#1E88E5', // Боевик и приключения - синий
|
||||||
|
10762: '#00ACC1', // Детский - циан
|
||||||
|
10763: '#546E7A', // Новости - сине-серый
|
||||||
|
10764: '#F06292', // Реалити-шоу - розовый
|
||||||
|
10765: '#00BCD4', // Фантастика и фэнтези - голубой
|
||||||
|
10766: '#5E35B1', // Мыльная опера - пурпурный
|
||||||
|
10767: '#4CAF50', // Ток-шоу - зеленый
|
||||||
|
10768: '#FFD54F' // Война и политика - желтый
|
||||||
|
};
|
||||||
|
|
||||||
|
// Получаем цвет для категории или используем запасной вариант
|
||||||
|
function getCategoryColor(categoryId: number): string {
|
||||||
|
return genreColors[categoryId] || '#3949AB'; // Индиго как запасной вариант
|
||||||
|
}
|
||||||
|
|
||||||
|
const CardContainer = styled.div<{ $bgUrl: string; $bgColor: string }>`
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
background-image: url(${props => props.$bgUrl || '/images/placeholder.jpg'});
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: ${props => props.$bgColor};
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CardContent = styled.div`
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CategoryName = styled.h3`
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CategoryCount = styled.p`
|
||||||
|
font-size: 0.875rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function CategoryCard({ category, backgroundUrl }: CategoryCardProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [imageUrl, setImageUrl] = useState<string>(backgroundUrl || '/images/placeholder.jpg');
|
||||||
|
|
||||||
|
const categoryColor = getCategoryColor(category.id);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
router.push(`/categories/${category.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardContainer
|
||||||
|
$bgUrl={imageUrl}
|
||||||
|
$bgColor={categoryColor}
|
||||||
|
onClick={handleClick}
|
||||||
|
role="button"
|
||||||
|
aria-label={`Категория ${category.name}`}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<CategoryName>{category.name}</CategoryName>
|
||||||
|
<CategoryCount>Фильмы и сериалы</CategoryCount>
|
||||||
|
</CardContent>
|
||||||
|
</CardContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoryCard;
|
||||||
@@ -1,17 +1,33 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const GlassCard = styled.div`
|
export const GlassCard = styled.div`
|
||||||
background: rgba(0, 0, 0, 0.45);
|
background: rgba(0, 0, 0, 0.65); /* Увеличили непрозрачность фона для лучшей читаемости */
|
||||||
backdrop-filter: blur(20px);
|
/* Убираем тяжелый blur на мобильных устройствах */
|
||||||
|
@supports (backdrop-filter: blur(20px)) {
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
padding: 3rem;
|
padding: 2.5rem;
|
||||||
border-radius: 24px;
|
border-radius: 16px; /* Уменьшили радиус для компактности */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 32px 0 rgba(0, 0, 0, 0.3),
|
0 8px 32px 0 rgba(0, 0, 0, 0.3),
|
||||||
inset 0 0 0 1px rgba(255, 255, 255, 0.05);
|
inset 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
overflow: hidden; /* Предотвращаем выход контента за пределы карточки */
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
padding: 1.75rem 1.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
padding: 1.5rem 1.25rem;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
width: calc(100% - 1rem);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default GlassCard;
|
export default GlassCard;
|
||||||
|
|||||||
@@ -4,65 +4,97 @@ import React from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Movie } from '@/types/movie';
|
import { Movie, TVShow } from '@/types/movie';
|
||||||
import { formatDate } from '@/lib/utils';
|
import { formatDate } from '@/lib/utils';
|
||||||
import { useImageLoader } from '@/hooks/useImageLoader';
|
import { useImageLoader } from '@/hooks/useImageLoader';
|
||||||
|
|
||||||
|
// Тип-гард для проверки, является ли объект сериалом
|
||||||
|
function isTVShow(media: Movie | TVShow): media is TVShow {
|
||||||
|
return 'name' in media && 'first_air_date' in media;
|
||||||
|
}
|
||||||
|
|
||||||
interface MovieCardProps {
|
interface MovieCardProps {
|
||||||
movie: Movie;
|
movie: Movie | TVShow;
|
||||||
priority?: boolean;
|
priority?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MovieCard({ movie, priority = false }: MovieCardProps) {
|
export default function MovieCard({ movie, priority = false }: MovieCardProps) {
|
||||||
const { imageUrl, isLoading } = useImageLoader(movie.poster_path, 'w342');
|
// Определяем, это фильм или сериал с помощью тип-гарда
|
||||||
|
const isTV = isTVShow(movie);
|
||||||
|
|
||||||
|
// Используем правильный заголовок и дату в зависимости от типа
|
||||||
|
const title = isTV ? movie.name || 'Без названия' : movie.title || 'Без названия';
|
||||||
|
const date = isTV ? movie.first_air_date : movie.release_date;
|
||||||
|
|
||||||
|
// Выбираем правильный URL
|
||||||
|
const url = isTV ? `/tv/${movie.id}` : `/movie/${movie.id}`;
|
||||||
|
|
||||||
|
// Загружаем изображение с оптимизированным размером для конкретного устройства
|
||||||
|
// Используем меньший размер изображения для мобильных устройств
|
||||||
|
const { imageUrl, isLoading } = useImageLoader(movie.poster_path, 'w342'); // Используем поддерживаемый размер
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card href={`/movie/${movie.id}`}>
|
<Card href={url}>
|
||||||
<PosterWrapper>
|
<PosterWrapper>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex h-full w-full items-center justify-center bg-gray-700">
|
<LoadingPlaceholder aria-label="Загрузка постера">
|
||||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-white" />
|
<div className="h-6 w-6 animate-spin rounded-full border-2 border-gray-300 border-t-blue-500" />
|
||||||
</div>
|
</LoadingPlaceholder>
|
||||||
) : imageUrl ? (
|
) : imageUrl ? (
|
||||||
<Poster
|
<Poster
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={movie.title}
|
alt={`Постер ${title}`}
|
||||||
fill
|
fill
|
||||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
sizes="(max-width: 640px) 150px, (max-width: 768px) 180px, (max-width: 1024px) 200px, 220px"
|
||||||
priority={priority}
|
priority={priority}
|
||||||
className="object-cover transition-opacity duration-300 group-hover:opacity-75"
|
loading={priority ? 'eager' : 'lazy'}
|
||||||
|
className="object-cover"
|
||||||
unoptimized // Отключаем оптимизацию Next.js, так как используем CDN
|
unoptimized // Отключаем оптимизацию Next.js, так как используем CDN
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center bg-gray-700 text-gray-400">
|
<NoImagePlaceholder aria-label="Нет изображения">
|
||||||
No Image
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
</div>
|
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
</NoImagePlaceholder>
|
||||||
)}
|
)}
|
||||||
<Rating style={{ backgroundColor: getRatingColor(movie.vote_average) }}>
|
<Rating style={{ backgroundColor: getRatingColor(movie.vote_average) }}>
|
||||||
{movie.vote_average.toFixed(1)}
|
{movie.vote_average.toFixed(1)}
|
||||||
</Rating>
|
</Rating>
|
||||||
</PosterWrapper>
|
</PosterWrapper>
|
||||||
<Content>
|
<Content>
|
||||||
<Title>{movie.title}</Title>
|
<Title>{title}</Title>
|
||||||
<Year>{formatDate(movie.release_date)}</Year>
|
<Year>{date ? formatDate(date) : 'Без даты'}</Year>
|
||||||
</Content>
|
</Content>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция для определения цвета рейтинга
|
||||||
const getRatingColor = (rating: number) => {
|
const getRatingColor = (rating: number) => {
|
||||||
if (rating >= 7) return '#4CAF50';
|
if (rating >= 7) return '#4CAF50';
|
||||||
if (rating >= 5) return '#FFC107';
|
if (rating >= 5) return '#FFC107';
|
||||||
return '#F44336';
|
return '#F44336';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Оптимизированные стилевые компоненты для мобильных устройств
|
||||||
const Card = styled(Link)`
|
const Card = styled(Link)`
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 16px;
|
border-radius: 12px; /* Уменьшили радиус для компактности */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #242424;
|
background: #1c1c1c; /* Темнее фон для лучшего контраста */
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
will-change: transform; /* Подсказка браузеру для оптимизации */
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex; /* Используем flexbox для лучшего контроля над высотой */
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%; /* Занимаем всю доступную высоту */
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
border-radius: 8px; /* Еще меньше радиус на малых экранах */
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const PosterWrapper = styled.div`
|
const PosterWrapper = styled.div`
|
||||||
@@ -75,33 +107,85 @@ const Poster = styled(Image)`
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Плейсхолдер для загрузки
|
||||||
|
const LoadingPlaceholder = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Плейсхолдер для отсутствующих изображений
|
||||||
|
const NoImagePlaceholder = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #1c1c1c;
|
||||||
|
color: #6b7280;
|
||||||
|
`;
|
||||||
|
|
||||||
const Content = styled.div`
|
const Content = styled.div`
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
flex-grow: 1; /* Занимаем все оставшееся пространство */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
padding: 8px 10px; /* Уменьшенные отступы для мобильных устройств */
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Title = styled.h3`
|
const Title = styled.h3`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin: 0;
|
margin: 0 0 4px 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
white-space: normal; /* Важно: разрешаем перенос текста */
|
||||||
|
max-height: 2.8em; /* Фиксированная высота для заголовка */
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
font-size: 13px; /* Уменьшенный размер шрифта для мобильных устройств */
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Year = styled.div`
|
const Year = styled.p`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #808191;
|
color: #aaa;
|
||||||
margin-top: 4px;
|
margin: 0;
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
font-size: 11px; /* Уменьшенный размер шрифта для мобильных устройств */
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Rating = styled.div`
|
const Rating = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
padding: 4px 8px;
|
background-color: #2196F3;
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: white;
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
padding: 2px 5px;
|
||||||
|
font-size: 11px;
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -75,14 +75,15 @@ const MobileNav = styled.nav`
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: rgba(18, 18, 23, 0.8);
|
background: #121217; /* Заменили полупрозрачный фон на сплошной для производительности */
|
||||||
backdrop-filter: blur(10px);
|
/* Удалили тяжелый эффект blur для мобильных устройств */
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); /* Добавили тень для визуального разделения */
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 60px;
|
height: 56px; /* Уменьшили высоту для компактности */
|
||||||
|
|
||||||
@media (min-width: 769px) {
|
@media (min-width: 769px) {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -115,17 +116,19 @@ const MobileMenuButton = styled.button`
|
|||||||
|
|
||||||
const MobileMenu = styled.div<{ $isOpen: boolean }>`
|
const MobileMenu = styled.div<{ $isOpen: boolean }>`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 60px;
|
top: 56px; /* Соответствует новой высоте навбара */
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(18, 18, 23, 0.95);
|
background: #121217; /* Сплошной фон без прозрачности */
|
||||||
backdrop-filter: blur(10px);
|
/* Удалили тяжелый эффект blur */
|
||||||
transform: translateX(${props => props.$isOpen ? '0' : '100%'});
|
transform: translateX(${props => props.$isOpen ? '0' : '100%'});
|
||||||
transition: transform 0.3s ease-in-out;
|
transition: transform 0.25s ease-out; /* Ускорили анимацию */
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
z-index: 49;
|
z-index: 49;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
will-change: transform; /* Подсказка браузеру для оптимизации */
|
||||||
|
-webkit-overflow-scrolling: touch; /* Плавный скролл на iOS */
|
||||||
|
|
||||||
@media (min-width: 769px) {
|
@media (min-width: 769px) {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -136,20 +139,27 @@ const MobileMenuItem = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 1rem;
|
padding: 1rem 0.75rem; /* Уменьшили горизонтальные отступы */
|
||||||
|
margin-bottom: 0.25rem; /* Добавили отступ между элементами */
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-radius: 12px;
|
border-radius: 8px; /* Уменьшили радиус для компактности */
|
||||||
transition: background-color 0.2s;
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
font-weight: 500; /* Добавили небольшое утолщение шрифта */
|
||||||
|
position: relative; /* Для анимации ripple-эффекта */
|
||||||
|
overflow: hidden; /* Для анимации ripple-эффекта */
|
||||||
|
|
||||||
&:hover {
|
/* Заменили плавную анимацию на мгновенную для мобильных устройств */
|
||||||
background: rgba(255, 255, 255, 0.1);
|
&:active {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
transform: scale(0.98); /* Небольшой эффект нажатия */
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 20px;
|
width: 22px; /* Увеличили иконки для лучшей видимости на мобильных устройствах */
|
||||||
height: 20px;
|
height: 22px;
|
||||||
|
min-width: 22px; /* Чтобы иконки были выровнены */
|
||||||
|
color: #3b82f6; /* Цвет для лучшего визуального разделения */
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ export const api = axios.create({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface Category {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Genre {
|
export interface Genre {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -117,6 +123,32 @@ export interface TVShowResponse {
|
|||||||
total_results: number;
|
total_results: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const categoriesAPI = {
|
||||||
|
// Получение всех категорий
|
||||||
|
getCategories() {
|
||||||
|
return api.get<{ categories: Category[] }>('/categories');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Получение категории по ID
|
||||||
|
getCategory(id: number) {
|
||||||
|
return api.get<Category>(`/categories/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Получение фильмов по категории
|
||||||
|
getMoviesByCategory(categoryId: number, page = 1) {
|
||||||
|
return api.get<MovieResponse>(`/categories/${categoryId}/movies`, {
|
||||||
|
params: { page }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Получение сериалов по категории
|
||||||
|
getTVShowsByCategory(categoryId: number, page = 1) {
|
||||||
|
return api.get<TVShowResponse>(`/categories/${categoryId}/tv`, {
|
||||||
|
params: { page }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const moviesAPI = {
|
export const moviesAPI = {
|
||||||
// Получение популярных фильмов
|
// Получение популярных фильмов
|
||||||
getPopular(page = 1) {
|
getPopular(page = 1) {
|
||||||
@@ -163,9 +195,14 @@ export const moviesAPI = {
|
|||||||
|
|
||||||
// Получение фильмов по жанру
|
// Получение фильмов по жанру
|
||||||
getMoviesByGenre(genreId: number, page = 1) {
|
getMoviesByGenre(genreId: number, page = 1) {
|
||||||
return api.get<MovieResponse>('/movies/genre/' + genreId, {
|
return api.get<MovieResponse>('/movies/discover', {
|
||||||
params: { page }
|
params: { with_genres: genreId, page }
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Получение жанров
|
||||||
|
getGenres() {
|
||||||
|
return api.get<{ genres: Genre[] }>('/movies/genres');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user