mirror of
https://gitlab.com/foxixus/neomovies.git
synced 2025-10-28 01:48:50 +05:00
Add categories
This commit is contained in:
@@ -1,45 +1,45 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headersList = headers();
|
||||
const response = await fetch(
|
||||
`https://neomovies-api.vercel.app/movies/${params.id}/external-ids`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Создаем новый Response с нужными заголовками
|
||||
return new NextResponse(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching external IDs:', error);
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: 'Failed to fetch external IDs' }),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextResponse } from 'next/server';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headersList = headers();
|
||||
const response = await fetch(
|
||||
`https://neomovies-api.vercel.app/movies/${params.id}/external-ids`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Создаем новый Response с нужными заголовками
|
||||
return new NextResponse(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching external IDs:', error);
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: 'Failed to fetch external IDs' }),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 styled from 'styled-components';
|
||||
import { moviesAPI } from '@/lib/api';
|
||||
import MovieCard from '@/components/MovieCard';
|
||||
import { Movie } from '@/lib/api';
|
||||
|
||||
interface Genre {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
import { categoriesAPI } from '@/lib/api';
|
||||
import { Category } from '@/lib/api';
|
||||
import CategoryCard from '@/components/CategoryCard';
|
||||
|
||||
// Styled Components
|
||||
const Container = styled.div`
|
||||
@@ -18,50 +13,36 @@ const Container = styled.div`
|
||||
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`
|
||||
font-size: 1.5rem;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #fff;
|
||||
`;
|
||||
|
||||
const GenreButtons = styled.div`
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
const Subtitle = styled.p`
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 1rem;
|
||||
margin-top: -0.5rem;
|
||||
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`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
min-height: 300px;
|
||||
`;
|
||||
|
||||
const Spinner = styled.div`
|
||||
@@ -88,71 +69,90 @@ const ErrorMessage = styled.div`
|
||||
margin: 2rem 0;
|
||||
`;
|
||||
|
||||
export default function CategoriesPage() {
|
||||
const [genres, setGenres] = useState<Genre[]>([]);
|
||||
const [selectedGenre, setSelectedGenre] = useState<number | null>(null);
|
||||
const [movies, setMovies] = useState<Movie[]>([]);
|
||||
interface CategoryWithBackground extends Category {
|
||||
backgroundUrl?: string;
|
||||
}
|
||||
|
||||
function CategoriesPage() {
|
||||
const [categories, setCategories] = useState<CategoryWithBackground[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Загрузка жанров при монтировании
|
||||
// Загрузка категорий и фоновых изображений для них
|
||||
useEffect(() => {
|
||||
const fetchGenres = async () => {
|
||||
async function fetchCategoriesAndBackgrounds() {
|
||||
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);
|
||||
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]
|
||||
});
|
||||
// Получаем список категорий
|
||||
const categoriesResponse = await categoriesAPI.getCategories();
|
||||
|
||||
if (response.data.results) {
|
||||
setMovies(response.data.results);
|
||||
} else {
|
||||
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) {
|
||||
console.error('Error fetching movies:', error);
|
||||
setError('Ошибка при загрузке фильмов');
|
||||
console.error('Error fetching categories:', error);
|
||||
setError('Ошибка при загрузке категорий');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMoviesByGenre();
|
||||
}, [selectedGenre]);
|
||||
}
|
||||
|
||||
fetchCategoriesAndBackgrounds();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Container>
|
||||
<Title>Категории фильмов</Title>
|
||||
<Title>Категории</Title>
|
||||
<ErrorMessage>{error}</ErrorMessage>
|
||||
</Container>
|
||||
);
|
||||
@@ -160,33 +160,26 @@ export default function CategoriesPage() {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Title>Категории фильмов</Title>
|
||||
|
||||
{/* Кнопки жанров */}
|
||||
<GenreButtons>
|
||||
{genres.map((genre) => (
|
||||
<GenreButton
|
||||
key={genre.id}
|
||||
$active={selectedGenre === genre.id}
|
||||
onClick={() => setSelectedGenre(genre.id)}
|
||||
>
|
||||
{genre.name}
|
||||
</GenreButton>
|
||||
))}
|
||||
</GenreButtons>
|
||||
|
||||
{/* Сетка фильмов */}
|
||||
<Title>Категории</Title>
|
||||
<Subtitle>Различные жанры фильмов и сериалов</Subtitle>
|
||||
|
||||
{loading ? (
|
||||
<LoadingContainer>
|
||||
<Spinner />
|
||||
</LoadingContainer>
|
||||
) : (
|
||||
<MovieGrid>
|
||||
{movies.map((movie) => (
|
||||
<MovieCard key={movie.id} movie={movie} />
|
||||
<Grid>
|
||||
{categories.map((category) => (
|
||||
<CategoryCard
|
||||
key={category.id}
|
||||
category={category}
|
||||
backgroundUrl={category.backgroundUrl}
|
||||
/>
|
||||
))}
|
||||
</MovieGrid>
|
||||
</Grid>
|
||||
)}
|
||||
</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%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const MovieInfo = styled.div`
|
||||
@@ -35,11 +39,18 @@ const MovieInfo = styled.div`
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const PosterContainer = styled.div`
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const Poster = styled.img`
|
||||
@@ -48,14 +59,23 @@ const Poster = styled.img`
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
width: 200px;
|
||||
height: 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`
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
@@ -63,6 +83,16 @@ const Title = styled.h1`
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
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`
|
||||
@@ -70,11 +100,23 @@ const Info = styled.div`
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const InfoItem = styled.span`
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
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`
|
||||
@@ -82,6 +124,10 @@ const GenreList = styled.div`
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const Genre = styled.span`
|
||||
@@ -90,21 +136,80 @@ const Genre = styled.span`
|
||||
border-radius: 1rem;
|
||||
font-size: 0.9rem;
|
||||
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`
|
||||
font-style: italic;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Overview = styled.p`
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
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`
|
||||
margin-top: 2rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const LoadingContainer = styled.div`
|
||||
@@ -157,6 +262,7 @@ export default function MovieContent({ movieId, initialMovie }: MovieContentProp
|
||||
<Poster
|
||||
src={getImageUrl(movie.poster_path)}
|
||||
alt={movie.title}
|
||||
loading="eager"
|
||||
/>
|
||||
</PosterContainer>
|
||||
|
||||
@@ -174,19 +280,30 @@ export default function MovieContent({ movieId, initialMovie }: MovieContentProp
|
||||
</GenreList>
|
||||
{movie.tagline && <Tagline>{movie.tagline}</Tagline>}
|
||||
<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
|
||||
mediaId={movie.id.toString()}
|
||||
mediaType="movie"
|
||||
title={movie.title}
|
||||
posterPath={movie.poster_path}
|
||||
/>
|
||||
</div>
|
||||
</ActionButtons>
|
||||
</Details>
|
||||
</MovieInfo>
|
||||
|
||||
{imdbId && (
|
||||
<PlayerSection>
|
||||
<PlayerSection id="movie-player">
|
||||
<MoviePlayer
|
||||
imdbId={imdbId}
|
||||
/>
|
||||
|
||||
@@ -9,15 +9,25 @@ interface PageProps {
|
||||
}
|
||||
|
||||
// Генерация метаданных для страницы
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { id } = params;
|
||||
export async function generateMetadata(
|
||||
props: { params: { id: string }}
|
||||
): Promise<Metadata> {
|
||||
// В Next.js 14, нужно сначала получить данные фильма,
|
||||
// а затем использовать их для метаданных
|
||||
try {
|
||||
const { data: movie } = await moviesAPI.getMovie(id);
|
||||
// Получаем id для использования в запросе
|
||||
const movieId = props.params.id;
|
||||
|
||||
// Запрашиваем данные фильма
|
||||
const { data: movie } = await moviesAPI.getMovie(movieId);
|
||||
|
||||
// Создаем метаданные на основе полученных данных
|
||||
return {
|
||||
title: `${movie.title} - NeoMovies`,
|
||||
description: movie.overview,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error generating metadata:', error);
|
||||
return {
|
||||
title: 'Фильм - NeoMovies',
|
||||
};
|
||||
|
||||
@@ -19,7 +19,13 @@ async function getData(id: string) {
|
||||
}
|
||||
|
||||
export default async function Page(props: PageProps) {
|
||||
const { id } = props.params;
|
||||
const data = await getData(id);
|
||||
return <TVShowPage tvShowId={data.id} show={data.show} />;
|
||||
// В Next.js 14 нужно сначала использовать параметры в асинхронной функции
|
||||
try {
|
||||
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>;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user