From ae6b7cfdd610a4bcb77cef539c8de47e212c201f Mon Sep 17 00:00:00 2001 From: Ernous Date: Mon, 20 Oct 2025 08:45:42 +0000 Subject: [PATCH] Release 2.5 --- .env.example | 12 +- README.md | 2 +- netlify.toml | 7 + package-lock.json | 4 +- package.json | 2 +- public/sitemap-0.xml | 24 +- src/app/categories/[id]/page.tsx | 135 ++++--- src/app/categories/page.tsx | 44 +-- src/app/favorites/page.tsx | 24 +- src/app/login/LoginClient.tsx | 18 +- src/app/movie/[...slug]/page.tsx | 44 +++ src/app/movie/[id]/MovieContent.tsx | 222 ------------ src/app/movie/[id]/page.tsx | 43 --- src/app/movie/_components/MovieContent.tsx | 291 +++++++++++++++ .../movie/{[id] => _components}/MoviePage.tsx | 0 src/app/movie/page.tsx | 1 + src/app/not-found.tsx | 7 +- src/app/page.tsx | 21 +- src/app/profile/page.tsx | 22 +- src/app/search/SearchClient.tsx | 170 ++++++++- src/app/terms/page.tsx | 193 +++++++--- src/app/tv/[...slug]/page.tsx | 44 +++ src/app/tv/[id]/TVContent.tsx | 222 ------------ src/app/tv/[id]/page.tsx | 44 --- src/app/tv/_components/TVContent.tsx | 325 +++++++++++++++++ src/app/tv/{[id] => _components}/TVPage.tsx | 2 +- .../{[id] => _components}/TVShowContent.tsx | 0 .../tv/{[id] => _components}/TVShowPage.tsx | 0 src/app/tv/page.tsx | 1 + src/app/verify/VerificationClient.tsx | 12 +- src/components/CategoryCard.tsx | 6 +- src/components/ClientLayout.tsx | 15 +- src/components/EpisodeSelector.tsx | 111 ++++++ src/components/FavoriteButton.tsx | 46 ++- src/components/HeaderBar.tsx | 12 +- src/components/MobileNav.tsx | 11 +- src/components/MovieCard.tsx | 14 +- src/components/MoviePlayer.tsx | 224 ++++++++++-- src/components/MovieTile.tsx | 35 +- src/components/PlayerSelector.tsx | 40 +++ src/components/SettingsContent.tsx | 143 +++++++- src/components/TorrentSelector.tsx | 52 +-- src/contexts/TranslationContext.tsx | 81 +++++ src/hooks/useAuth.ts | 7 + src/hooks/useSearch.ts | 61 +++- src/hooks/useSettings.ts | 47 ++- src/lib/dataUtils.ts | 147 ++++++++ src/lib/favoritesApi.ts | 15 +- src/lib/models/Favorite.ts | 31 -- src/lib/neoApi.ts | 333 ++++++++++++++---- src/lib/unifiedTypes.ts | 47 +++ src/locales/en.ts | 323 +++++++++++++++++ src/locales/index.ts | 2 + src/locales/ru.ts | 323 +++++++++++++++++ src/types/kinopoisk.ts | 240 +++++++++++++ tsconfig.tsbuildinfo | 1 + 56 files changed, 3295 insertions(+), 1008 deletions(-) create mode 100644 netlify.toml create mode 100644 src/app/movie/[...slug]/page.tsx delete mode 100644 src/app/movie/[id]/MovieContent.tsx delete mode 100644 src/app/movie/[id]/page.tsx create mode 100644 src/app/movie/_components/MovieContent.tsx rename src/app/movie/{[id] => _components}/MoviePage.tsx (100%) create mode 100644 src/app/movie/page.tsx create mode 100644 src/app/tv/[...slug]/page.tsx delete mode 100644 src/app/tv/[id]/TVContent.tsx delete mode 100644 src/app/tv/[id]/page.tsx create mode 100644 src/app/tv/_components/TVContent.tsx rename src/app/tv/{[id] => _components}/TVPage.tsx (92%) rename src/app/tv/{[id] => _components}/TVShowContent.tsx (100%) rename src/app/tv/{[id] => _components}/TVShowPage.tsx (100%) create mode 100644 src/app/tv/page.tsx create mode 100644 src/components/EpisodeSelector.tsx create mode 100644 src/components/PlayerSelector.tsx create mode 100644 src/contexts/TranslationContext.tsx create mode 100644 src/lib/dataUtils.ts delete mode 100644 src/lib/models/Favorite.ts create mode 100644 src/lib/unifiedTypes.ts create mode 100644 src/locales/en.ts create mode 100644 src/locales/index.ts create mode 100644 src/locales/ru.ts create mode 100644 src/types/kinopoisk.ts create mode 100644 tsconfig.tsbuildinfo diff --git a/.env.example b/.env.example index 2ecff17..41d8091 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,11 @@ -# API URL для нового Go API NEXT_PUBLIC_API_URL=https://api.neomovies.ru -# Для локальной разработки используйте: -# NEXT_PUBLIC_API_URL=http://localhost:3000 \ No newline at end of file +MONGODB_URI=mongodb://localhost:27017/neomovies + +NEXTAUTH_SECRET=your_nextauth_secret_here +NEXTAUTH_URL=http://localhost:3001 + +GOOGLE_CLIENT_ID=your_google_client_id +GOOGLE_CLIENT_SECRET=your_google_client_secret + +NEXT_PUBLIC_SITE_URL=https://neomovies.ru diff --git a/README.md b/README.md index 67e4e2e..69fd93d 100644 --- a/README.md +++ b/README.md @@ -187,5 +187,5 @@ Users are advised to verify whether use of the site complies with their local co ---
-

Made with <3 by Foxix/Erno

+

Made with ❤️ by Foxix

diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..c772559 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,7 @@ +[build] + command = "npm run build" + publish = ".next" + functions = "netlify/functions" + +[[plugins]] + package = "@netlify/plugin-nextjs" diff --git a/package-lock.json b/package-lock.json index d42c89d..b0a86b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "neo-movies-web", - "version": "0.1.0", + "version": "2.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "neo-movies-web", - "version": "0.1.0", + "version": "2.5.0", "dependencies": { "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-select": "^2.2.5", diff --git a/package.json b/package.json index d371175..ea06b6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neo-movies-web", - "version": "0.1.0", + "version": "2.5.0", "private": true, "scripts": { "dev": "next dev --turbopack", diff --git a/public/sitemap-0.xml b/public/sitemap-0.xml index 660f5a7..7d4367c 100644 --- a/public/sitemap-0.xml +++ b/public/sitemap-0.xml @@ -1,14 +1,16 @@ -https://neomovies.ru/auth/callback2025-08-08T16:29:17.862Zdaily0.7 -https://neomovies.ru/admin/login2025-08-08T16:29:17.862Zdaily0.7 -https://neomovies.ru/categories2025-08-08T16:29:17.862Zdaily0.7 -https://neomovies.ru/login2025-08-08T16:29:17.862Zdaily0.7 -https://neomovies.ru/favorites2025-08-08T16:29:17.862Zdaily0.7 -https://neomovies.ru2025-08-08T16:29:17.862Zdaily0.7 -https://neomovies.ru/profile2025-08-08T16:29:17.862Zdaily0.7 -https://neomovies.ru/search2025-08-08T16:29:17.862Zdaily0.7 -https://neomovies.ru/settings2025-08-08T16:29:17.862Zdaily0.7 -https://neomovies.ru/verify2025-08-08T16:29:17.862Zdaily0.7 -https://neomovies.ru/terms2025-08-08T16:29:17.862Zdaily0.7 +https://neomovies.ru/auth/callback2025-10-19T13:40:08.554Zdaily0.7 +https://neomovies.ru/categories2025-10-19T13:40:08.554Zdaily0.7 +https://neomovies.ru/admin/login2025-10-19T13:40:08.554Zdaily0.7 +https://neomovies.ru/favorites2025-10-19T13:40:08.554Zdaily0.7 +https://neomovies.ru/login2025-10-19T13:40:08.554Zdaily0.7 +https://neomovies.ru/movie2025-10-19T13:40:08.554Zdaily0.7 +https://neomovies.ru/profile2025-10-19T13:40:08.554Zdaily0.7 +https://neomovies.ru2025-10-19T13:40:08.554Zdaily0.7 +https://neomovies.ru/search2025-10-19T13:40:08.554Zdaily0.7 +https://neomovies.ru/settings2025-10-19T13:40:08.554Zdaily0.7 +https://neomovies.ru/tv2025-10-19T13:40:08.554Zdaily0.7 +https://neomovies.ru/verify2025-10-19T13:40:08.554Zdaily0.7 +https://neomovies.ru/terms2025-10-19T13:40:08.554Zdaily0.7 \ No newline at end of file diff --git a/src/app/categories/[id]/page.tsx b/src/app/categories/[id]/page.tsx index 265c0ab..d20ce1f 100644 --- a/src/app/categories/[id]/page.tsx +++ b/src/app/categories/[id]/page.tsx @@ -3,38 +3,60 @@ import { useState, useEffect, useMemo } from 'react'; import { useParams, useRouter } from 'next/navigation'; import { categoriesAPI, Movie } from '@/lib/neoApi'; +import { useTranslation } from '@/contexts/TranslationContext'; import MovieCard from '@/components/MovieCard'; import { ArrowLeft, Loader2 } from 'lucide-react'; -type MediaType = 'movies' | 'tv'; +// Маппинг ID категорий к их названиям (так как нет эндпоинта getCategory) +const categoryNames: Record = { + 12: 'приключения', + 10751: 'семейный', + 10752: 'военный', + 10762: 'Детский', + 10764: 'Реалити-шоу', + 10749: 'мелодрама', + 28: 'боевик', + 80: 'криминал', + 18: 'драма', + 14: 'фэнтези', + 27: 'ужасы', + 10402: 'музыка', + 10770: 'телевизионный фильм', + 16: 'мультфильм', + 99: 'документальный', + 878: 'фантастика', + 37: 'вестерн', + 10765: 'НФ и Фэнтези', + 10767: 'Ток-шоу', + 10768: 'Война и Политика', + 9648: 'детектив', + 35: 'комедия', + 36: 'история', + 53: 'триллер', + 10759: 'Боевик и Приключения', + 10763: 'Новости', + 10766: 'Мыльная опера' +}; function CategoryPage() { + const { t, locale } = useTranslation(); const params = useParams(); const router = useRouter(); const categoryId = parseInt(params.id as string); const [categoryName, setCategoryName] = useState(''); - const [mediaType, setMediaType] = useState('movies'); - const [items, setItems] = useState([]); + const [movies, setMovies] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); - const [moviesAvailable, setMoviesAvailable] = useState(true); - const [tvShowsAvailable, setTvShowsAvailable] = useState(true); useEffect(() => { - async function fetchCategoryName() { - try { - const response = await categoriesAPI.getCategory(categoryId); - setCategoryName(response.data.name); - } catch (error) { - console.error('Error fetching category:', error); - setError('Не удалось загрузить информацию о категории'); - } - } - if (categoryId) { - fetchCategoryName(); + // Устанавливаем название категории из переводов + if (categoryId && t.categories.names[categoryId as keyof typeof t.categories.names]) { + setCategoryName(t.categories.names[categoryId as keyof typeof t.categories.names]); + } else { + setCategoryName(t.categories.unknownCategory); } }, [categoryId]); @@ -46,47 +68,27 @@ function CategoryPage() { setError(null); try { - let response; - if (mediaType === 'movies') { - response = await categoriesAPI.getMoviesByCategory(categoryId, page); - const hasMovies = response.data.results.length > 0; - if (page === 1) setMoviesAvailable(hasMovies); - setItems(response.data.results); - setTotalPages(response.data.total_pages); - if (!hasMovies && tvShowsAvailable && page === 1) { - setMediaType('tv'); - } - } else { - 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: any) => ({ - ...show, - title: show.name || show.title, - release_date: show.first_air_date || show.release_date, - })); - setItems(transformedShows); - setTotalPages(response.data.total_pages); - if (!hasTvShows && moviesAvailable && page === 1) { - setMediaType('movies'); - } - } + // Выбираем источник по языку: ru -> kp, иначе tmdb + const source = (locale || 'ru') === 'ru' ? 'kp' : 'tmdb'; + // Получаем и фильмы, и сериалы, объединяем, чтобы не было пустых категорий + const [tvRes, movieRes]: any = await Promise.all([ + categoriesAPI.getMediaByCategory(categoryId, 'tv', page, undefined, source, t.categories?.names?.[categoryId as any] ?? ''), + categoriesAPI.getMediaByCategory(categoryId, 'movie', page, undefined, source, t.categories?.names?.[categoryId as any] ?? '') + ]); + const tvItems = tvRes?.results || []; + const movieItems = movieRes?.results || []; + const combined = [...tvItems, ...movieItems]; + setMovies(combined); + setTotalPages(Math.max(tvRes?.total_pages || 1, movieRes?.total_pages || 1)); } catch (err) { - setError('Ошибка при загрузке данных'); + setError(t.categories.errorLoadingMovies); console.error(err); } finally { setLoading(false); } } fetchData(); - }, [categoryId, mediaType, page, moviesAvailable, tvShowsAvailable]); - - const handleMediaTypeChange = (type: MediaType) => { - if (type === 'movies' && !moviesAvailable) return; - if (type === 'tv' && !tvShowsAvailable) return; - setMediaType(type); - setPage(1); - }; + }, [categoryId, page]); const handlePageChange = (newPage: number) => { if (newPage >= 1 && newPage <= totalPages) { @@ -166,7 +168,7 @@ function CategoryPage() {
{error} @@ -182,45 +184,28 @@ function CategoryPage() { Назад к категориям -

- {categoryName || 'Загрузка...'} +

+ {categoryName}

-
- - -
- {loading ? (
) : ( <> - {items.length > 0 ? ( + {movies.length > 0 ? (
- {items.map(item => ( + {movies.map(movie => ( ))}
) : (
-

Нет {mediaType === 'movies' ? 'фильмов' : 'сериалов'} в этой категории.

+

{t.categories.noMoviesInCategory}

)} diff --git a/src/app/categories/page.tsx b/src/app/categories/page.tsx index 6a3bf4b..6649577 100644 --- a/src/app/categories/page.tsx +++ b/src/app/categories/page.tsx @@ -3,12 +3,14 @@ import { useState, useEffect } from 'react'; import { categoriesAPI, Category } from '@/lib/neoApi'; import CategoryCard from '@/components/CategoryCard'; +import { useTranslation } from '@/contexts/TranslationContext'; interface CategoryWithBackground extends Category { backgroundUrl?: string | null; } function CategoriesPage() { + const { t } = useTranslation(); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -21,46 +23,24 @@ function CategoriesPage() { try { const categoriesResponse = await categoriesAPI.getCategories(); - if (!categoriesResponse.data.categories || categoriesResponse.data.categories.length === 0) { + if (!categoriesResponse.data || categoriesResponse.data.length === 0) { setError('Не удалось загрузить категории'); setLoading(false); return; } const categoriesWithBackgrounds: CategoryWithBackground[] = await Promise.all( - categoriesResponse.data.categories.map(async (category: Category) => { + categoriesResponse.data.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 - }; + // Пробуем получить сериал как фон, иначе фильм + const tvRes: any = await categoriesAPI.getMediaByCategory(category.id, 'tv', 1); + const movieRes: any = await categoriesAPI.getMediaByCategory(category.id, 'movie', 1); + const pick = tvRes?.results?.[0] || movieRes?.results?.[0]; + const backgroundUrl = pick?.backdrop_path || pick?.poster_path || null; + return { ...category, backgroundUrl }; } catch (error) { console.error(`Error fetching background for category ${category.id}:`, error); - return category; + return { ...category, backgroundUrl: null }; } }) ); @@ -68,7 +48,7 @@ function CategoriesPage() { setCategories(categoriesWithBackgrounds); } catch (error) { console.error('Error fetching categories:', error); - setError('Ошибка при загрузке категорий'); + setError(t.categories.errorLoadingCategories); } finally { setLoading(false); } diff --git a/src/app/favorites/page.tsx b/src/app/favorites/page.tsx index 713bbde..6b31b7b 100644 --- a/src/app/favorites/page.tsx +++ b/src/app/favorites/page.tsx @@ -6,6 +6,7 @@ import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { neoApi, getImageUrl } from '@/lib/neoApi'; import { Loader2, HeartCrack } from 'lucide-react'; +import { useTranslation } from '@/contexts/TranslationContext'; interface Favorite { id: number; @@ -16,6 +17,7 @@ interface Favorite { } export default function FavoritesPage() { + const { t } = useTranslation(); const [favorites, setFavorites] = useState([]); const [loading, setLoading] = useState(true); const router = useRouter(); @@ -30,16 +32,14 @@ export default function FavoritesPage() { try { const response = await neoApi.get('/api/v1/favorites'); - setFavorites(response.data); + // Обрабатываем как обёрнутый, так и прямой ответ + const data = response.data?.data || response.data; + setFavorites(Array.isArray(data) ? data : []); } catch (error: any) { console.error('Failed to fetch favorites:', error); - // Редиректим только при явном 401 - if (error?.response?.status === 401) { - localStorage.removeItem('token'); - localStorage.removeItem('userName'); - localStorage.removeItem('userEmail'); - router.replace('/login'); - } + // 401 ошибки теперь обрабатываются автоматически через interceptor + // Здесь просто устанавливаем пустой массив при ошибке + setFavorites([]); } finally { setLoading(false); } @@ -61,15 +61,15 @@ export default function FavoritesPage() {
-

Избранное пусто

+

{t.favorites.empty}

- У вас пока нет избранных фильмов и сериалов + {t.favorites.emptyDescription}

- Найти фильмы + {t.favorites.goToMovies}
@@ -80,7 +80,7 @@ export default function FavoritesPage() {
-

Избранное

+

{t.favorites.title}

Ваша коллекция любимых фильмов и сериалов

diff --git a/src/app/login/LoginClient.tsx b/src/app/login/LoginClient.tsx index 4454f9a..57f1aed 100644 --- a/src/app/login/LoginClient.tsx +++ b/src/app/login/LoginClient.tsx @@ -4,8 +4,10 @@ import { useState, useEffect } from 'react'; import { useAuth } from '../../hooks/useAuth'; import { useRouter } from 'next/navigation'; import Image from 'next/image'; +import { useTranslation } from '@/contexts/TranslationContext'; export default function LoginClient() { + const { t } = useTranslation(); const [isLogin, setIsLogin] = useState(true); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -34,7 +36,7 @@ export default function LoginClient() { router.push(`/verify?email=${encodeURIComponent(email)}`); } } catch (err) { - setError(err instanceof Error ? err.message : 'Произошла ошибка'); + setError(err instanceof Error ? err.message : t.common.error); } finally { setIsLoading(false); } @@ -53,7 +55,7 @@ export default function LoginClient() {
setName(e.target.value)} required={!isLogin} @@ -76,7 +78,7 @@ export default function LoginClient() {
setPassword(e.target.value)} required @@ -89,7 +91,7 @@ export default function LoginClient() { disabled={isLoading} className="w-full py-3 px-4 bg-accent text-white rounded-lg font-medium hover:bg-accent/90 focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" > - {isLoading ? 'Загрузка...' : isLogin ? 'Войти' : 'Зарегистрироваться'} + {isLoading ? t.common.loading : isLogin ? t.auth.loginButton : t.auth.registerButton} @@ -98,7 +100,7 @@ export default function LoginClient() {
- или + {t.common.or}
@@ -108,7 +110,7 @@ export default function LoginClient() { className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-warm-200 dark:border-warm-700 rounded-lg bg-white dark:bg-warm-800 hover:bg-warm-100 dark:hover:bg-warm-700 text-warm-900 dark:text-warm-50 transition-colors" > Google - Продолжить с Google + {t.auth.continueWithGoogle} {error && ( @@ -118,13 +120,13 @@ export default function LoginClient() { )}
- {isLogin ? 'Еще нет аккаунта?' : 'Уже есть аккаунт?'}{' '} + {isLogin ? t.auth.noAccount : t.auth.haveAccount}{' '}
diff --git a/src/app/movie/[...slug]/page.tsx b/src/app/movie/[...slug]/page.tsx new file mode 100644 index 0000000..666cfb8 --- /dev/null +++ b/src/app/movie/[...slug]/page.tsx @@ -0,0 +1,44 @@ +"use client"; +import { useParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { moviesAPI } from '@/lib/neoApi'; +import { Loader2 } from 'lucide-react'; +import { useTranslation } from '@/contexts/TranslationContext'; +import MoviePage from '../_components/MoviePage'; + +export default function Page() { + const { t } = useTranslation(); + const params = useParams(); + const slugParam = params.slug as string[] | undefined; + const [movie, setMovie] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const sourceId = (() => { + if (!slugParam || slugParam.length === 0) return ''; + if (slugParam.length === 1) return slugParam[0]; // new: kp_123 + if (slugParam.length >= 2) return `${slugParam[0]}_${slugParam[1]}`; // old: kp/123 + return ''; + })(); + + useEffect(() => { + async function fetchMovie() { + try { + setLoading(true); + setError(null); + const response = await moviesAPI.getMovieBySourceId(sourceId); + setMovie(response.data); + } catch (err: any) { + setError(`${t.common.failedToLoad}: ${err?.message || t.common.unknownError}`); + } finally { + setLoading(false); + } + } + if (sourceId) fetchMovie(); + }, [sourceId]); + + if (loading) return
; + if (error || !movie) return

{error || t.common.movieNotFound}

; + + return ; +} diff --git a/src/app/movie/[id]/MovieContent.tsx b/src/app/movie/[id]/MovieContent.tsx deleted file mode 100644 index d0b7cde..0000000 --- a/src/app/movie/[id]/MovieContent.tsx +++ /dev/null @@ -1,222 +0,0 @@ -'use client'; - -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/neoApi'; -import MoviePlayer from '@/components/MoviePlayer'; -import TorrentSelector from '@/components/TorrentSelector'; -import FavoriteButton from '@/components/FavoriteButton'; -import Reactions from '@/components/Reactions'; -import { formatDate } from '@/lib/utils'; -import { PlayCircle, ArrowLeft } from 'lucide-react'; -import { NextSeo } from 'next-seo'; - -interface MovieContentProps { - movieId: string; - initialMovie: MovieDetails; -} - -export default function MovieContent({ movieId, initialMovie }: MovieContentProps) { - const [movie] = useState(initialMovie); - const [externalIds, setExternalIds] = useState(null); - const [imdbId, setImdbId] = useState(null); - const [isPlayerFullscreen, setIsPlayerFullscreen] = useState(false); - const [isControlsVisible, setIsControlsVisible] = useState(false); - const controlsTimeoutRef = useRef(null); - - useEffect(() => { - const fetchExternalIds = async () => { - try { - const data = await moviesAPI.getExternalIds(movieId); - setExternalIds(data); - if (data?.imdb_id) { - setImdbId(data.imdb_id); - } - } catch (err) { - console.error('Error fetching external ids:', err); - } - }; - fetchExternalIds(); - }, [movieId]); - - const showControls = () => { - if (controlsTimeoutRef.current) { - clearTimeout(controlsTimeoutRef.current); - } - setIsControlsVisible(true); - controlsTimeoutRef.current = setTimeout(() => { - setIsControlsVisible(false); - }, 3000); - }; - - const handleOpenPlayer = () => { - setIsPlayerFullscreen(true); - showControls(); - }; - - const handleClosePlayer = () => { - setIsPlayerFullscreen(false); - if (controlsTimeoutRef.current) { - clearTimeout(controlsTimeoutRef.current); - } - }; - - return ( - <> - - {/* schema.org Movie */} -