mirror of
https://gitlab.com/foxixus/neomovies.git
synced 2025-10-27 17:38:50 +05:00
fix: исправлен плеер для фильмов
This commit is contained in:
10
.env
Normal file
10
.env
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
NEXT_PUBLIC_API_URL=https://neomovies-api.vercel.app
|
||||||
|
MONGODB_URI=mongodb+srv://neomoviesmail:Vfhreif1@neo-movies.nz1e2.mongodb.net/database
|
||||||
|
JWT_SECRET=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|
||||||
|
NEXTAUTH_SECRET=eyJhdWQiOiI4ZmU3ODhlYmI5ZDAwNjZiNjQ2MWZkwNDlkNzU4ZDQxOTQwYzA3NjlhNSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.x50tvcW
|
||||||
|
NEXTAUTH_URL=http://localhost:3000
|
||||||
|
GMAIL_USER=neo.movies.mail@gmail.com
|
||||||
|
GMAIL_APP_PASSWORD=togh lhlg zadn dywe
|
||||||
|
NEXT_PUBLIC_TMDB_API_KEY=8feg88ebi9d0066b6461fa7993c23771b
|
||||||
|
NEXT_PUBLIC_TMDB_ACCESS_TOKEN=eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI4ZmU3ODhlYmI5ZDAwNjZiNjQ2MWZhNzk5M2MyMzcxYiIsIm5iZiI6MTcyMzQwMTM3My4yMDgsInN1YiI6IjY2YjkwNDlkNzU4ZDQxOTQwYzA3NjlhNSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.x50tvcWDdBTEhtwRb3dE7aEe9qu4sXV_qOjLMn_Vmew
|
||||||
|
NEXT_PUBLIC_LUMEX_URL=https://p.lumex.site/k1GbtOF2cX6p
|
||||||
@@ -21,10 +21,22 @@ const nextConfig = {
|
|||||||
hostname: 'image.tmdb.org',
|
hostname: 'image.tmdb.org',
|
||||||
pathname: '/**',
|
pathname: '/**',
|
||||||
},
|
},
|
||||||
|
// Локальная разработка
|
||||||
{
|
{
|
||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
hostname: 'localhost',
|
hostname: 'localhost',
|
||||||
port: '3010',
|
port: '3000',
|
||||||
|
pathname: '/images/**',
|
||||||
|
},
|
||||||
|
// Продакшен на Vercel
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'neomovies-api.vercel.app',
|
||||||
|
pathname: '/images/**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'neo-movies.vercel.app',
|
||||||
pathname: '/images/**',
|
pathname: '/images/**',
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -39,6 +51,18 @@ const nextConfig = {
|
|||||||
experimental: {
|
experimental: {
|
||||||
scrollRestoration: true,
|
scrollRestoration: true,
|
||||||
},
|
},
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/api/:path*',
|
||||||
|
headers: [
|
||||||
|
{ key: 'Access-Control-Allow-Origin', value: '*' },
|
||||||
|
{ key: 'Access-Control-Allow-Methods', value: 'GET, POST, PUT, DELETE, OPTIONS' },
|
||||||
|
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
module.exports = nextConfig;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^2.5.0",
|
"@reduxjs/toolkit": "^2.5.0",
|
||||||
|
"@vercel/analytics": "^1.0.1",
|
||||||
"@tabler/icons-react": "^3.26.0",
|
"@tabler/icons-react": "^3.26.0",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
|||||||
66
src/app/api/mobile/auth/login/route.ts
Normal file
66
src/app/api/mobile/auth/login/route.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { compare } from 'bcryptjs';
|
||||||
|
import { connectToDatabase } from '@/lib/mongodb';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { email, password } = await req.json();
|
||||||
|
|
||||||
|
const { db } = await connectToDatabase();
|
||||||
|
const user = await db.collection('users').findOne({ email });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Пользователь не найден' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPasswordValid = await compare(password, user.password);
|
||||||
|
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Неверный пароль' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.verified) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'EMAIL_NOT_VERIFIED' },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем JWT токен
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
id: user._id.toString(),
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
verified: user.verified,
|
||||||
|
isAdmin: user.isAdmin
|
||||||
|
},
|
||||||
|
process.env.NEXTAUTH_SECRET!,
|
||||||
|
{ expiresIn: '30d' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: user._id.toString(),
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
verified: user.verified,
|
||||||
|
isAdmin: user.isAdmin
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Ошибка входа' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/app/api/mobile/auth/register/route.ts
Normal file
61
src/app/api/mobile/auth/register/route.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { hash } from 'bcryptjs';
|
||||||
|
import { connectToDatabase } from '@/lib/mongodb';
|
||||||
|
import { sendVerificationEmail } from '@/lib/mailer';
|
||||||
|
|
||||||
|
function generateVerificationCode(): string {
|
||||||
|
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { email, password, name } = await req.json();
|
||||||
|
|
||||||
|
const { db } = await connectToDatabase();
|
||||||
|
|
||||||
|
// Проверяем, существует ли пользователь
|
||||||
|
const existingUser = await db.collection('users').findOne({ email });
|
||||||
|
if (existingUser) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Email уже зарегистрирован' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хешируем пароль
|
||||||
|
const hashedPassword = await hash(password, 12);
|
||||||
|
|
||||||
|
// Генерируем код подтверждения
|
||||||
|
const verificationCode = generateVerificationCode();
|
||||||
|
const verificationExpires = new Date(Date.now() + 10 * 60 * 1000); // 10 минут
|
||||||
|
|
||||||
|
// Создаем пользователя
|
||||||
|
const result = await db.collection('users').insertOne({
|
||||||
|
email,
|
||||||
|
password: hashedPassword,
|
||||||
|
name,
|
||||||
|
verified: false,
|
||||||
|
verificationCode,
|
||||||
|
verificationExpires,
|
||||||
|
isAdmin: false,
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отправляем код подтверждения
|
||||||
|
await sendVerificationEmail(email, verificationCode);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
id: result.insertedId.toString(),
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
verified: false,
|
||||||
|
isAdmin: false
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Ошибка регистрации' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/app/api/mobile/auth/resend-code/route.ts
Normal file
56
src/app/api/mobile/auth/resend-code/route.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { connectToDatabase } from '@/lib/mongodb';
|
||||||
|
import { sendVerificationEmail } from '@/lib/mailer';
|
||||||
|
|
||||||
|
function generateVerificationCode(): string {
|
||||||
|
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { email } = await req.json();
|
||||||
|
|
||||||
|
const { db } = await connectToDatabase();
|
||||||
|
const user = await db.collection('users').findOne({ email });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Пользователь не найден' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.verified) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Email уже подтвержден' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем новый код
|
||||||
|
const verificationCode = generateVerificationCode();
|
||||||
|
const verificationExpires = new Date(Date.now() + 10 * 60 * 1000); // 10 минут
|
||||||
|
|
||||||
|
// Обновляем код в базе
|
||||||
|
await db.collection('users').updateOne(
|
||||||
|
{ _id: user._id },
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
verificationCode,
|
||||||
|
verificationExpires
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Отправляем новый код
|
||||||
|
await sendVerificationEmail(email, verificationCode);
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Resend code error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Ошибка отправки кода' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/app/api/mobile/auth/verify/route.ts
Normal file
62
src/app/api/mobile/auth/verify/route.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { connectToDatabase } from '@/lib/mongodb';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { email, code } = await req.json();
|
||||||
|
|
||||||
|
const { db } = await connectToDatabase();
|
||||||
|
const user = await db.collection('users').findOne({
|
||||||
|
email,
|
||||||
|
verificationCode: code,
|
||||||
|
verificationExpires: { $gt: new Date() }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Неверный код или код истек' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подтверждаем email
|
||||||
|
await db.collection('users').updateOne(
|
||||||
|
{ _id: user._id },
|
||||||
|
{
|
||||||
|
$set: { verified: true },
|
||||||
|
$unset: { verificationCode: "", verificationExpires: "" }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Создаем JWT токен
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
id: user._id.toString(),
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
verified: true,
|
||||||
|
isAdmin: user.isAdmin
|
||||||
|
},
|
||||||
|
process.env.NEXTAUTH_SECRET!,
|
||||||
|
{ expiresIn: '30d' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: user._id.toString(),
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
verified: true,
|
||||||
|
isAdmin: user.isAdmin
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Verification error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Ошибка верификации' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/app/api/movies/[id]/external-ids/route.ts
Normal file
45
src/app/api/movies/[id]/external-ids/route.ts
Normal file
@@ -0,0 +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': '*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
192
src/app/categories/page.tsx
Normal file
192
src/app/categories/page.tsx
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 GenreButtons = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 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;
|
||||||
|
`;
|
||||||
|
|
||||||
|
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;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function CategoriesPage() {
|
||||||
|
const [genres, setGenres] = useState<Genre[]>([]);
|
||||||
|
const [selectedGenre, setSelectedGenre] = useState<number | null>(null);
|
||||||
|
const [movies, setMovies] = useState<Movie[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Загрузка жанров при монтировании
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchGenres = async () => {
|
||||||
|
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]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.results) {
|
||||||
|
setMovies(response.data.results);
|
||||||
|
} else {
|
||||||
|
setError('Не удалось загрузить фильмы');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching movies:', error);
|
||||||
|
setError('Ошибка при загрузке фильмов');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchMoviesByGenre();
|
||||||
|
}, [selectedGenre]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Title>Категории фильмов</Title>
|
||||||
|
<ErrorMessage>{error}</ErrorMessage>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Title>Категории фильмов</Title>
|
||||||
|
|
||||||
|
{/* Кнопки жанров */}
|
||||||
|
<GenreButtons>
|
||||||
|
{genres.map((genre) => (
|
||||||
|
<GenreButton
|
||||||
|
key={genre.id}
|
||||||
|
$active={selectedGenre === genre.id}
|
||||||
|
onClick={() => setSelectedGenre(genre.id)}
|
||||||
|
>
|
||||||
|
{genre.name}
|
||||||
|
</GenreButton>
|
||||||
|
))}
|
||||||
|
</GenreButtons>
|
||||||
|
|
||||||
|
{/* Сетка фильмов */}
|
||||||
|
{loading ? (
|
||||||
|
<LoadingContainer>
|
||||||
|
<Spinner />
|
||||||
|
</LoadingContainer>
|
||||||
|
) : (
|
||||||
|
<MovieGrid>
|
||||||
|
{movies.map((movie) => (
|
||||||
|
<MovieCard key={movie.id} movie={movie} />
|
||||||
|
))}
|
||||||
|
</MovieGrid>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Inter } from 'next/font/google';
|
|||||||
import './globals.css';
|
import './globals.css';
|
||||||
import { ClientLayout } from '@/components/ClientLayout';
|
import { ClientLayout } from '@/components/ClientLayout';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin', 'cyrillic'] });
|
const inter = Inter({ subsets: ['latin', 'cyrillic'] });
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export default function RootLayout({
|
|||||||
</head>
|
</head>
|
||||||
<body className={inter.className} suppressHydrationWarning>
|
<body className={inter.className} suppressHydrationWarning>
|
||||||
<ClientLayout>{children}</ClientLayout>
|
<ClientLayout>{children}</ClientLayout>
|
||||||
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
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 { moviesAPI } from '@/lib/neoApi';
|
||||||
import { getImageUrl } from '@/lib/neoApi';
|
import { getImageUrl } from '@/lib/neoApi';
|
||||||
import type { MovieDetails } from '@/lib/api';
|
import type { MovieDetails } from '@/lib/api';
|
||||||
import { useSettings } from '@/hooks/useSettings';
|
import { useSettings } from '@/hooks/useSettings';
|
||||||
import MoviePlayer from '@/components/MoviePlayer';
|
import MoviePlayer from '@/components/MoviePlayer';
|
||||||
import FavoriteButton from '@/components/FavoriteButton';
|
import FavoriteButton from '@/components/FavoriteButton';
|
||||||
|
import { formatDate } from '@/lib/utils';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -137,9 +138,9 @@ export default function MovieContent({ movieId, initialMovie }: MovieContentProp
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchImdbId = async () => {
|
const fetchImdbId = async () => {
|
||||||
try {
|
try {
|
||||||
const newImdbId = await moviesAPI.getImdbId(movieId);
|
const { data } = await moviesAPI.getMovie(movieId);
|
||||||
if (newImdbId) {
|
if (data?.imdb_id) {
|
||||||
setImdbId(newImdbId);
|
setImdbId(data.imdb_id);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching IMDb ID:', err);
|
console.error('Error fetching IMDb ID:', err);
|
||||||
@@ -164,7 +165,7 @@ export default function MovieContent({ movieId, initialMovie }: MovieContentProp
|
|||||||
<Info>
|
<Info>
|
||||||
<InfoItem>Рейтинг: {movie.vote_average.toFixed(1)}</InfoItem>
|
<InfoItem>Рейтинг: {movie.vote_average.toFixed(1)}</InfoItem>
|
||||||
<InfoItem>Длительность: {movie.runtime} мин.</InfoItem>
|
<InfoItem>Длительность: {movie.runtime} мин.</InfoItem>
|
||||||
<InfoItem>Дата выхода: {new Date(movie.release_date).toLocaleDateString('ru-RU')}</InfoItem>
|
<InfoItem>Дата выхода: {formatDate(movie.release_date)}</InfoItem>
|
||||||
</Info>
|
</Info>
|
||||||
<GenreList>
|
<GenreList>
|
||||||
{movie.genres.map(genre => (
|
{movie.genres.map(genre => (
|
||||||
|
|||||||
@@ -106,11 +106,11 @@ export default function MoviePlayer({ id, title, poster, imdbId }: MoviePlayerPr
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
if (!imdbId) {
|
if (!imdbId) {
|
||||||
const newImdbId = await moviesAPI.getImdbId(id);
|
const { data } = await moviesAPI.getMovie(id);
|
||||||
if (!newImdbId) {
|
if (!data?.imdb_id) {
|
||||||
throw new Error('IMDb ID не найден');
|
throw new Error('IMDb ID не найден');
|
||||||
}
|
}
|
||||||
imdbId = newImdbId;
|
imdbId = data.imdb_id;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching IMDb ID:', err);
|
console.error('Error fetching IMDb ID:', err);
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export const moviesAPI = {
|
|||||||
|
|
||||||
// Получение IMDB ID
|
// Получение IMDB ID
|
||||||
getImdbId(id: string | number) {
|
getImdbId(id: string | number) {
|
||||||
return neoApi.get(`/movies/${id}/external-ids`, { timeout: 30000 }).then(res => res.data.imdb_id);
|
return neoApi.get(`/movies/${id}/external_ids`, { timeout: 30000 }).then(res => res.data.imdb_id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,30 @@ export const formatDate = (dateString: string | Date | undefined | null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const date = typeof dateString === 'string' ? new Date(dateString) : dateString;
|
let date: Date;
|
||||||
|
|
||||||
|
if (typeof dateString === 'string') {
|
||||||
|
// Пробуем разные форматы даты
|
||||||
|
if (dateString.includes('T')) {
|
||||||
|
// ISO формат
|
||||||
|
date = new Date(dateString);
|
||||||
|
} else if (dateString.includes('-')) {
|
||||||
|
// YYYY-MM-DD формат
|
||||||
|
const [year, month, day] = dateString.split('-').map(Number);
|
||||||
|
date = new Date(year, month - 1, day);
|
||||||
|
} else if (dateString.includes('.')) {
|
||||||
|
// DD.MM.YYYY формат
|
||||||
|
const [day, month, year] = dateString.split('.').map(Number);
|
||||||
|
date = new Date(year, month - 1, day);
|
||||||
|
} else {
|
||||||
|
date = new Date(dateString);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
date = dateString;
|
||||||
|
}
|
||||||
|
|
||||||
if (isNaN(date.getTime())) {
|
if (isNaN(date.getTime())) {
|
||||||
|
console.error('Invalid date:', dateString);
|
||||||
return 'Нет даты';
|
return 'Нет даты';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +49,7 @@ export const formatDate = (dateString: string | Date | undefined | null) => {
|
|||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
}).format(date) + ' г.';
|
}).format(date) + ' г.';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error formatting date:', error);
|
console.error('Error formatting date:', error, dateString);
|
||||||
return 'Нет даты';
|
return 'Нет даты';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user