Измения в АПИ

This commit is contained in:
2025-08-07 18:33:28 +00:00
parent b2db578f5f
commit a1f1deea13
45 changed files with 1955 additions and 1119 deletions

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import { api } from '@/lib/api';
import { neoApi } from '@/lib/neoApi';
import { AxiosError } from 'axios';
import { useRouter } from 'next/navigation';
import styled from 'styled-components';
@@ -92,12 +92,12 @@ export default function AdminLoginClient() {
setIsLoading(true);
try {
const response = await api.post('/auth/login', {
const response = await neoApi.post('/api/v1/auth/login', {
email,
password,
});
const { token, user } = response.data;
const { token, user } = response.data.data || response.data;
if (user?.role !== 'admin') {
setError('У вас нет прав администратора.');

View File

@@ -1,45 +0,0 @@
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': '*'
}
}
);
}
}

View File

@@ -1,34 +0,0 @@
import { NextResponse } from 'next/server';
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.themoviedb.org/3',
headers: {
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_TMDB_ACCESS_TOKEN}`,
'Content-Type': 'application/json'
}
});
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const id = params.id;
try {
const response = await api.get(`/movie/${id}`, {
params: {
language: 'ru-RU',
append_to_response: 'credits,videos,similar'
}
});
return NextResponse.json(response.data);
} catch (error: any) {
console.error('Error fetching movie details:', error);
return NextResponse.json(
{ error: error.message || 'Failed to fetch movie details' },
{ status: error.response?.status || 500 }
);
}
}

View File

@@ -1,37 +0,0 @@
import { NextResponse } from 'next/server';
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.themoviedb.org/3',
headers: {
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_TMDB_ACCESS_TOKEN}`,
'Content-Type': 'application/json'
}
});
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') || '1';
try {
const response = await api.get('/discover/movie', {
params: {
page,
language: 'ru-RU',
'vote_count.gte': 100,
'vote_average.gte': 1,
sort_by: 'popularity.desc',
include_adult: false,
'primary_release_date.lte': new Date().toISOString().split('T')[0]
}
});
return NextResponse.json(response.data);
} catch (error: any) {
console.error('Error fetching popular movies:', error);
return NextResponse.json(
{ error: error.message || 'Failed to fetch movies' },
{ status: error.response?.status || 500 }
);
}
}

View File

@@ -1,31 +0,0 @@
import { NextResponse } from 'next/server';
import { searchAPI } from '@/lib/neoApi';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const query = searchParams.get('query');
const page = searchParams.get('page') || '1';
if (!query) {
return NextResponse.json(
{ error: 'Query parameter is required' },
{ status: 400 }
);
}
try {
// Используем обновленный multiSearch, который теперь запрашивает и фильмы, и сериалы параллельно
const response = await searchAPI.multiSearch(query, parseInt(page));
return NextResponse.json(response.data);
} catch (error: any) {
console.error('Error searching:', error);
return NextResponse.json(
{
error: 'Failed to search',
details: error.message || 'Unknown error'
},
{ status: error.response?.status || 500 }
);
}
}

View File

@@ -1,15 +0,0 @@
import { NextResponse } from 'next/server';
import { syncMovies } from '@/lib/movieSync';
export async function POST() {
try {
const movies = await syncMovies();
return NextResponse.json({ success: true, movies });
} catch (error) {
console.error('Error syncing movies:', error);
return NextResponse.json(
{ error: 'Failed to sync movies' },
{ status: 500 }
);
}
}

View File

@@ -1,19 +0,0 @@
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') || '1';
const response = await fetch(
`https://api.themoviedb.org/3/movie/top_rated?page=${page}`,
{
headers: {
'Authorization': `Bearer ${process.env.TMDB_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
}
);
const data = await response.json();
return NextResponse.json(data);
}

View File

@@ -1,19 +0,0 @@
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') || '1';
const response = await fetch(
`https://api.themoviedb.org/3/movie/upcoming?page=${page}`,
{
headers: {
'Authorization': `Bearer ${process.env.TMDB_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
}
);
const data = await response.json();
return NextResponse.json(data);
}

View File

@@ -2,7 +2,7 @@
import { useState, useEffect, useMemo } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { categoriesAPI, Movie, TVShow } from '@/lib/api';
import { categoriesAPI, Movie } from '@/lib/neoApi';
import MovieCard from '@/components/MovieCard';
import { ArrowLeft, Loader2 } from 'lucide-react';
@@ -60,10 +60,10 @@ function CategoryPage() {
response = await categoriesAPI.getTVShowsByCategory(categoryId, page);
const hasTvShows = response.data.results.length > 0;
if (page === 1) setTvShowsAvailable(hasTvShows);
const transformedShows = response.data.results.map((show: TVShow) => ({
const transformedShows = response.data.results.map((show: any) => ({
...show,
title: show.name,
release_date: show.first_air_date,
title: show.name || show.title,
release_date: show.first_air_date || show.release_date,
}));
setItems(transformedShows);
setTotalPages(response.data.total_pages);

View File

@@ -1,8 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import { categoriesAPI } from '@/lib/api';
import { Category } from '@/lib/api';
import { categoriesAPI, Category } from '@/lib/neoApi';
import CategoryCard from '@/components/CategoryCard';
interface CategoryWithBackground extends Category {
@@ -14,14 +13,12 @@ function CategoriesPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Загрузка категорий и фоновых изображений для них
useEffect(() => {
async function fetchCategoriesAndBackgrounds() {
setError(null);
setLoading(true);
try {
// Получаем список категорий
const categoriesResponse = await categoriesAPI.getCategories();
if (!categoriesResponse.data.categories || categoriesResponse.data.categories.length === 0) {
@@ -30,14 +27,11 @@ function CategoriesPage() {
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;
@@ -47,7 +41,6 @@ function CategoriesPage() {
backgroundUrl
};
} else {
// Если фильмов нет, пробуем получить сериалы
const tvResponse = await categoriesAPI.getTVShowsByCategory(category.id, 1);
if (tvResponse.data.results && tvResponse.data.results.length > 0) {
@@ -61,14 +54,13 @@ function CategoriesPage() {
}
}
// Если ни фильмов, ни сериалов не найдено
return {
...category,
backgroundUrl: undefined
};
} catch (error) {
console.error(`Error fetching background for category ${category.id}:`, error);
return category; // Возвращаем категорию без фона в случае ошибки
return category;
}
})
);

View File

@@ -49,3 +49,11 @@ body {
[data-nextjs-toast-wrapper] {
display: none !important;
}
/* Utility classes */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

View File

@@ -4,7 +4,7 @@ import { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
import { moviesAPI } from '@/lib/neoApi';
import { getImageUrl } from '@/lib/neoApi';
import type { MovieDetails } from '@/lib/api';
import type { MovieDetails } from '@/lib/neoApi';
import MoviePlayer from '@/components/MoviePlayer';
import TorrentSelector from '@/components/TorrentSelector';
import FavoriteButton from '@/components/FavoriteButton';
@@ -20,23 +20,25 @@ interface MovieContentProps {
export default function MovieContent({ movieId, initialMovie }: MovieContentProps) {
const [movie] = useState<MovieDetails>(initialMovie);
const [externalIds, setExternalIds] = useState<any>(null);
const [imdbId, setImdbId] = useState<string | null>(null);
const [isPlayerFullscreen, setIsPlayerFullscreen] = useState(false);
const [isControlsVisible, setIsControlsVisible] = useState(false);
const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
const fetchImdbId = async () => {
const fetchExternalIds = async () => {
try {
const { data } = await moviesAPI.getMovie(movieId);
const data = await moviesAPI.getExternalIds(movieId);
setExternalIds(data);
if (data?.imdb_id) {
setImdbId(data.imdb_id);
}
} catch (err) {
console.error('Error fetching IMDb ID:', err);
console.error('Error fetching external ids:', err);
}
};
fetchImdbId();
fetchExternalIds();
}, [movieId]);
const showControls = () => {
@@ -182,6 +184,9 @@ export default function MovieContent({ movieId, initialMovie }: MovieContentProp
<TorrentSelector
imdbId={imdbId}
type="movie"
title={movie.title}
originalTitle={movie.original_title}
year={movie.release_date?.split('-')[0]}
/>
</div>
)}

View File

@@ -2,7 +2,7 @@
import PageLayout from '@/components/PageLayout';
import MovieContent from './MovieContent';
import type { MovieDetails } from '@/lib/api';
import type { MovieDetails } from '@/lib/neoApi';
interface MoviePageProps {
movieId: string;

View File

@@ -1,5 +1,5 @@
import { Metadata } from 'next';
import { moviesAPI } from '@/lib/api';
import { moviesAPI } from '@/lib/neoApi';
import MoviePage from '@/app/movie/[id]/MoviePage';
interface PageProps {
@@ -8,19 +8,13 @@ interface PageProps {
};
}
// Генерация метаданных для страницы
export async function generateMetadata(props: Promise<PageProps>): Promise<Metadata> {
const { params } = await props;
// В Next.js 14, нужно сначала получить данные фильма,
// а затем использовать их для метаданных
try {
// Получаем id для использования в запросе
const movieId = params.id;
// Запрашиваем данные фильма
const { data: movie } = await moviesAPI.getMovie(movieId);
// Создаем метаданные на основе полученных данных
return {
title: `${movie.title} - NeoMovies`,
description: movie.overview,
@@ -33,7 +27,6 @@ export async function generateMetadata(props: Promise<PageProps>): Promise<Metad
}
}
// Получение данных для страницы
async function getData(id: string) {
try {
const { data: movie } = await moviesAPI.getMovie(id);

View File

@@ -49,9 +49,9 @@ export default function HomePage() {
Популярные
</button>
<button
onClick={() => setActiveTab('now_playing')}
onClick={() => setActiveTab('now-playing')}
className={`${
activeTab === 'now_playing'
activeTab === 'now-playing'
? 'border-red-500 text-red-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:border-gray-500'
} whitespace-nowrap border-b-2 px-1 py-4 text-sm font-medium`}
@@ -59,15 +59,25 @@ export default function HomePage() {
Новинки
</button>
<button
onClick={() => setActiveTab('top_rated')}
onClick={() => setActiveTab('top-rated')}
className={`${
activeTab === 'top_rated'
activeTab === 'top-rated'
? 'border-red-500 text-red-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:border-gray-500'
} whitespace-nowrap border-b-2 px-1 py-4 text-sm font-medium`}
>
Топ рейтинга
</button>
<button
onClick={() => setActiveTab('upcoming')}
className={`${
activeTab === 'upcoming'
? 'border-red-500 text-red-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:border-gray-500'
} whitespace-nowrap border-b-2 px-1 py-4 text-sm font-medium`}
>
Скоро
</button>
</nav>
</div>

View File

@@ -2,30 +2,24 @@
import { useState, useEffect, FormEvent } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import { Movie, TVShow, moviesAPI, tvAPI } from '@/lib/api';
import { searchAPI } from '@/lib/neoApi';
import type { Movie } from '@/lib/neoApi';
import MovieCard from '@/components/MovieCard';
export default function SearchClient() {
const router = useRouter();
const searchParams = useSearchParams();
const [query, setQuery] = useState(searchParams.get('q') || '');
const [results, setResults] = useState<(Movie | TVShow)[]>([]);
const [results, setResults] = useState<Movie[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const currentQuery = searchParams.get('q');
if (currentQuery) {
setLoading(true);
Promise.all([
moviesAPI.searchMovies(currentQuery),
tvAPI.searchShows(currentQuery),
])
.then(([movieResults, tvResults]) => {
const combined = [
...(movieResults.data.results || []),
...(tvResults.data.results || []),
];
setResults(combined.sort((a, b) => b.vote_count - a.vote_count));
searchAPI.multiSearch(currentQuery)
.then((response) => {
setResults(response.data.results || []);
})
.catch((error) => {
console.error('Search failed:', error);
@@ -56,7 +50,7 @@ export default function SearchClient() {
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
{results.map((item) => (
<MovieCard
key={`${item.id}-${'title' in item ? 'movie' : 'tv'}`}
key={`${item.id}-${item.media_type || 'movie'}`}
movie={item}
/>
))}

View File

@@ -2,9 +2,9 @@
import { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
import { tvAPI } from '@/lib/api';
import { tvShowsAPI } from '@/lib/neoApi';
import { getImageUrl } from '@/lib/neoApi';
import type { TVShowDetails } from '@/lib/api';
import type { TVShowDetails } from '@/lib/neoApi';
import MoviePlayer from '@/components/MoviePlayer';
import TorrentSelector from '@/components/TorrentSelector';
import FavoriteButton from '@/components/FavoriteButton';
@@ -20,29 +20,28 @@ interface TVContentProps {
export default function TVContent({ showId, initialShow }: TVContentProps) {
const [show] = useState<TVShowDetails>(initialShow);
const [externalIds, setExternalIds] = useState<any>(null);
const [imdbId, setImdbId] = useState<string | null>(null);
const [isPlayerFullscreen, setIsPlayerFullscreen] = useState(false);
const [isControlsVisible, setIsControlsVisible] = useState(false);
const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
const fetchImdbId = async () => {
const fetchExternalIds = async () => {
try {
// Используем dedicated эндпоинт для получения IMDb ID
const { data } = await tvAPI.getImdbId(showId);
const data = await tvShowsAPI.getExternalIds(showId);
setExternalIds(data);
if (data?.imdb_id) {
setImdbId(data.imdb_id);
}
} catch (err) {
console.error('Error fetching IMDb ID:', err);
console.error('Error fetching external ids:', err);
}
};
// Проверяем, есть ли ID в initialShow, чтобы избежать лишнего запроса
if (initialShow.external_ids?.imdb_id) {
setImdbId(initialShow.external_ids.imdb_id);
} else {
fetchImdbId();
fetchExternalIds();
}
}, [showId, initialShow.external_ids]);
@@ -185,7 +184,9 @@ export default function TVContent({ showId, initialShow }: TVContentProps) {
<TorrentSelector
imdbId={imdbId}
type="tv"
totalSeasons={show.number_of_seasons}
title={show.name}
originalTitle={show.original_name}
year={show.first_air_date?.split('-')[0]}
/>
</div>
)}

View File

@@ -2,7 +2,7 @@
import PageLayout from '@/components/PageLayout';
import TVContent from '@/app/tv/[id]/TVContent';
import type { TVShowDetails } from '@/lib/api';
import type { TVShowDetails } from '@/lib/neoApi';
interface TVPageProps {
showId: string;

View File

@@ -1,5 +1,5 @@
import { Metadata } from 'next';
import { tvAPI } from '@/lib/api';
import { tvShowsAPI } from '@/lib/neoApi';
import TVPage from '@/app/tv/[id]/TVPage';
export const dynamic = 'force-dynamic';
@@ -10,12 +10,11 @@ interface PageProps {
};
}
// Генерация метаданных для страницы
export async function generateMetadata(props: Promise<PageProps>): Promise<Metadata> {
const { params } = await props;
try {
const showId = params.id;
const { data: show } = await tvAPI.getShow(showId);
const { data: show } = await tvShowsAPI.getTVShow(showId);
return {
title: `${show.name} - NeoMovies`,
@@ -29,10 +28,9 @@ export async function generateMetadata(props: Promise<PageProps>): Promise<Metad
}
}
// Получение данных для страницы
async function getData(id: string) {
try {
const { data: show } = await tvAPI.getShow(id);
const { data: show } = await tvShowsAPI.getTVShow(id);
return { id, show };
} catch (error) {
throw new Error('Failed to fetch TV show');