mirror of
https://gitlab.com/foxixus/neomovies.git
synced 2025-10-28 09:58:49 +05:00
Update 103 files
- /public/file.svg - /public/globe.svg - /public/next.svg - /public/vercel.svg - /public/window.svg - /public/google.svg - /public/logo.png - /src/eslint.config.mjs - /src/api.ts - /src/middleware.ts - /src/app/favicon.ico - /src/app/globals.css - /src/app/layout.tsx - /src/app/page.tsx - /src/app/providers.tsx - /src/app/not-found.tsx - /src/app/error.tsx - /src/app/metadata.ts - /src/app/styles.tsx - /src/app/api/auth/[...nextauth]/route.ts - /src/app/api/auth/register/route.ts - /src/app/api/auth/verify/route.ts - /src/app/api/auth/check-verification/route.ts - /src/app/api/auth/resend-code/route.ts - /src/app/api/movies/search/route.ts - /src/app/api/movies/sync/route.ts - /src/app/api/admin/send-verification/route.ts - /src/app/api/admin/verify-code/route.ts - /src/app/api/admin/movies/route.ts - /src/app/api/admin/movies/toggle-visibility/route.ts - /src/app/api/admin/create/route.ts - /src/app/api/admin/users/toggle-admin/route.ts - /src/app/api/admin/toggle-admin/route.ts - /src/app/login/page.tsx - /src/app/login/LoginClient.tsx - /src/app/verify/page.tsx - /src/app/verify/VerificationClient.tsx - /src/app/profile/page.tsx - /src/app/movie/[id]/page.tsx - /src/app/movie/[id]/MoviePage.tsx - /src/app/movie/[id]/MovieContent.tsx - /src/app/settings/page.tsx - /src/app/tv/[id]/page.tsx - /src/app/tv/[id]/TVShowPage.tsx - /src/app/tv/[id]/TVShowContent.tsx - /src/app/admin/login/page.tsx - /src/app/admin/login/AdminLoginClient.tsx - /src/lib/db.ts - /src/lib/jwt.ts - /src/lib/registry.tsx - /src/lib/api.ts - /src/lib/mongodb.ts - /src/lib/mailer.ts - /src/lib/auth.ts - /src/lib/utils.ts - /src/lib/email.ts - /src/lib/movieSync.ts - /src/models/User.ts - /src/models/index.ts - /src/models/Movie.ts - /src/types/auth.ts - /src/types/movie.ts - /src/components/MovieCard.tsx - /src/components/Notification.tsx - /src/components/Pagination.tsx - /src/components/GoogleIcon.tsx - /src/components/StyleProvider.tsx - /src/components/Providers.tsx - /src/components/VerificationCodeInput.tsx - /src/components/GlassCard.tsx - /src/components/AppLayout.tsx - /src/components/SearchModal.tsx - /src/components/DarkReaderFix.tsx - /src/components/ClientLayout.tsx - /src/components/MenuItem.tsx - /src/components/MoviePlayer.tsx - /src/components/PageLayout.tsx - /src/components/SettingsContent.tsx - /src/components/Navbar.tsx - /src/components/LayoutContent.tsx - /src/components/SearchResults.tsx - /src/components/Icons/Icons.tsx - /src/components/Icons/HeartIcon.tsx - /src/components/Icons/PlayIcon.tsx - /src/components/admin/MovieSearch.tsx - /src/hooks/useUser.ts - /src/hooks/useMovies.ts - /src/hooks/useSettings.ts - /src/hooks/useSearch.ts - /src/styles/GlobalStyles.ts - /src/styles/GlobalStyles.tsx - /src/providers/AuthProvider.tsx - /src/data/movies.ts - /types/next-auth.d.ts - /middleware.ts - /next.config.js - /next-env.d.ts - /package.json - /postcss.config.mjs - /README.md - /tailwind.config.ts - /tsconfig.json - /package-lock.json
This commit is contained in:
255
src/lib/api.ts
Normal file
255
src/lib/api.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const BASE_URL = 'https://api.themoviedb.org/3';
|
||||
|
||||
if (typeof window === 'undefined' && !process.env.NEXT_PUBLIC_TMDB_ACCESS_TOKEN) {
|
||||
throw new Error('TMDB_ACCESS_TOKEN is not defined in environment variables');
|
||||
}
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_TMDB_ACCESS_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
export interface Genre {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Movie {
|
||||
id: number;
|
||||
title: string;
|
||||
overview: string;
|
||||
poster_path: string | null;
|
||||
backdrop_path: string | null;
|
||||
release_date: string;
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
genre_ids: number[];
|
||||
runtime?: number;
|
||||
genres?: Array<{ id: number; name: string }>;
|
||||
}
|
||||
|
||||
export interface MovieDetails extends Movie {
|
||||
genres: Genre[];
|
||||
runtime: number;
|
||||
tagline: string;
|
||||
budget: number;
|
||||
revenue: number;
|
||||
videos: {
|
||||
results: Video[];
|
||||
};
|
||||
credits: {
|
||||
cast: Cast[];
|
||||
crew: Crew[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface TVShow {
|
||||
id: number;
|
||||
name: string;
|
||||
overview: string;
|
||||
poster_path: string | null;
|
||||
backdrop_path: string | null;
|
||||
first_air_date: string;
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
genre_ids: number[];
|
||||
}
|
||||
|
||||
export interface TVShowDetails extends TVShow {
|
||||
genres: Genre[];
|
||||
number_of_episodes: number;
|
||||
number_of_seasons: number;
|
||||
tagline: string;
|
||||
credits: {
|
||||
cast: Cast[];
|
||||
crew: Crew[];
|
||||
};
|
||||
seasons: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
episode_count: number;
|
||||
poster_path: string | null;
|
||||
}>;
|
||||
external_ids?: {
|
||||
imdb_id: string | null;
|
||||
tvdb_id: number | null;
|
||||
tvrage_id: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
site: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Cast {
|
||||
id: number;
|
||||
name: string;
|
||||
character: string;
|
||||
profile_path: string | null;
|
||||
}
|
||||
|
||||
export interface Crew {
|
||||
id: number;
|
||||
name: string;
|
||||
job: string;
|
||||
profile_path: string | null;
|
||||
}
|
||||
|
||||
interface MovieResponse {
|
||||
page: number;
|
||||
results: Movie[];
|
||||
total_pages: number;
|
||||
total_results: number;
|
||||
}
|
||||
|
||||
interface TVShowResponse {
|
||||
page: number;
|
||||
results: TVShow[];
|
||||
total_pages: number;
|
||||
total_results: number;
|
||||
}
|
||||
|
||||
export const moviesAPI = {
|
||||
// Получение популярных фильмов
|
||||
getPopular: (page = 1) =>
|
||||
api.get<MovieResponse>('/movie/popular', {
|
||||
params: {
|
||||
page,
|
||||
language: 'ru-RU',
|
||||
}
|
||||
}),
|
||||
|
||||
// Получение данных о фильме по его TMDB ID
|
||||
getMovie: (id: string | number) =>
|
||||
api.get<MovieDetails>(`/movie/${id}`, {
|
||||
params: {
|
||||
language: 'ru-RU',
|
||||
append_to_response: 'credits,videos,similar'
|
||||
}
|
||||
}),
|
||||
|
||||
// Получение IMDb ID по TMDB ID для плеера
|
||||
getImdbId: async (tmdbId: string | number) => {
|
||||
try {
|
||||
const response = await api.get(`/movie/${tmdbId}/external_ids`);
|
||||
return response.data.imdb_id;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении IMDb ID:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// Получение видео по TMDB ID для плеера
|
||||
getVideo: async (tmdbId: string | number) => {
|
||||
try {
|
||||
const response = await api.get(`/movie/${tmdbId}/videos`, {
|
||||
params: {
|
||||
language: 'ru-RU',
|
||||
},
|
||||
});
|
||||
return response.data.results;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении видео:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// Поиск фильмов
|
||||
searchMovies: (query: string, page = 1) =>
|
||||
api.get<MovieResponse>('/search/movie', {
|
||||
params: {
|
||||
query,
|
||||
page,
|
||||
language: 'ru-RU',
|
||||
}
|
||||
}),
|
||||
|
||||
// Получение предстоящих фильмов
|
||||
getUpcoming: (page = 1) =>
|
||||
api.get('/movie/upcoming', {
|
||||
params: {
|
||||
page,
|
||||
language: 'ru-RU',
|
||||
}
|
||||
}),
|
||||
|
||||
// Получение лучших фильмов
|
||||
getTopRated: (page = 1) =>
|
||||
api.get('/movie/top_rated', {
|
||||
params: {
|
||||
page,
|
||||
language: 'ru-RU',
|
||||
'vote_count.gte': 100
|
||||
}
|
||||
}),
|
||||
|
||||
// Получение фильмов по жанру
|
||||
getMoviesByGenre: (genreId: number, page = 1) =>
|
||||
api.get('/discover/movie', {
|
||||
params: {
|
||||
with_genres: genreId,
|
||||
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]
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
export const tvAPI = {
|
||||
// Получение популярных сериалов
|
||||
getPopular: (page = 1) =>
|
||||
api.get<TVShowResponse>('/tv/popular', {
|
||||
params: {
|
||||
page,
|
||||
language: 'ru-RU',
|
||||
}
|
||||
}),
|
||||
|
||||
// Получение данных о сериале по его TMDB ID
|
||||
getShow: (id: string | number) =>
|
||||
api.get<TVShowDetails>(`/tv/${id}`, {
|
||||
params: {
|
||||
language: 'ru-RU',
|
||||
append_to_response: 'credits,external_ids',
|
||||
}
|
||||
}),
|
||||
|
||||
// Получение IMDb ID по TMDB ID для плеера
|
||||
getImdbId: (tmdbId: string | number) =>
|
||||
api.get<{ imdb_id: string | null }>(`/tv/${tmdbId}/external_ids`),
|
||||
|
||||
// Поиск сериалов
|
||||
searchShows: (query: string, page = 1) =>
|
||||
api.get<TVShowResponse>('/search/tv', {
|
||||
params: {
|
||||
query,
|
||||
page,
|
||||
language: 'ru-RU',
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
// Мультипоиск (фильмы и сериалы)
|
||||
export const searchAPI = {
|
||||
multiSearch: (query: string, page = 1) =>
|
||||
api.get('/search/multi', {
|
||||
params: {
|
||||
query,
|
||||
page,
|
||||
language: 'ru-RU',
|
||||
}
|
||||
}),
|
||||
};
|
||||
89
src/lib/auth.ts
Normal file
89
src/lib/auth.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { AuthOptions } from 'next-auth';
|
||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
||||
import { User } from '@/models';
|
||||
import { connectDB } from './db';
|
||||
|
||||
export const authOptions: AuthOptions = {
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
id: 'credentials',
|
||||
name: 'Credentials',
|
||||
credentials: {
|
||||
email: { label: 'Email', type: 'text' },
|
||||
password: { label: 'Password', type: 'password' },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
throw new Error('Необходимо указать email и пароль');
|
||||
}
|
||||
|
||||
await connectDB();
|
||||
|
||||
const user = await User.findOne({ email: credentials.email });
|
||||
if (!user) {
|
||||
throw new Error('Пользователь не найден');
|
||||
}
|
||||
|
||||
const isValid = await user.comparePassword(credentials.password);
|
||||
if (!isValid) {
|
||||
throw new Error('Неверный пароль');
|
||||
}
|
||||
|
||||
if (!user.isVerified) {
|
||||
throw new Error('Email не подтвержден');
|
||||
}
|
||||
|
||||
return {
|
||||
id: user._id.toString(),
|
||||
email: user.email,
|
||||
isAdmin: user.isAdmin,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
token.email = user.email;
|
||||
token.isAdmin = user.isAdmin;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (token) {
|
||||
session.user.id = token.id as string;
|
||||
session.user.email = token.email as string;
|
||||
session.user.isAdmin = token.isAdmin as boolean;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: '/login',
|
||||
},
|
||||
session: {
|
||||
strategy: 'jwt',
|
||||
},
|
||||
};
|
||||
|
||||
// Расширяем типы для NextAuth
|
||||
declare module 'next-auth' {
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
interface Session {
|
||||
user: User;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'next-auth/jwt' {
|
||||
interface JWT {
|
||||
id: string;
|
||||
email: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
}
|
||||
38
src/lib/db.ts
Normal file
38
src/lib/db.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI!;
|
||||
|
||||
if (!MONGODB_URI) {
|
||||
throw new Error('Please define the MONGODB_URI environment variable');
|
||||
}
|
||||
|
||||
let cached = global.mongoose;
|
||||
|
||||
if (!cached) {
|
||||
cached = global.mongoose = { conn: null, promise: null };
|
||||
}
|
||||
|
||||
export async function connectDB() {
|
||||
if (cached.conn) {
|
||||
return cached.conn;
|
||||
}
|
||||
|
||||
if (!cached.promise) {
|
||||
const opts = {
|
||||
bufferCommands: false,
|
||||
};
|
||||
|
||||
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
|
||||
return mongoose;
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
cached.conn = await cached.promise;
|
||||
} catch (e) {
|
||||
cached.promise = null;
|
||||
throw e;
|
||||
}
|
||||
|
||||
return cached.conn;
|
||||
}
|
||||
11
src/lib/email.ts
Normal file
11
src/lib/email.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const sendVerificationEmail = async (email: string, token: string) => {
|
||||
// Заглушка для функции отправки email
|
||||
console.log(`Sending verification email to ${email} with token ${token}`);
|
||||
return true;
|
||||
};
|
||||
|
||||
export const sendPasswordResetEmail = async (email: string, token: string) => {
|
||||
// Заглушка для функции отправки email
|
||||
console.log(`Sending password reset email to ${email} with token ${token}`);
|
||||
return true;
|
||||
};
|
||||
25
src/lib/jwt.ts
Normal file
25
src/lib/jwt.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET!;
|
||||
|
||||
if (!JWT_SECRET) {
|
||||
throw new Error('JWT_SECRET is not defined');
|
||||
}
|
||||
|
||||
export interface JWTPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export function generateToken(payload: JWTPayload): string {
|
||||
return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' });
|
||||
}
|
||||
|
||||
export function verifyToken(token: string): Promise<JWTPayload> {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, JWT_SECRET, (err, decoded) => {
|
||||
if (err) reject(err);
|
||||
resolve(decoded as JWTPayload);
|
||||
});
|
||||
});
|
||||
}
|
||||
44
src/lib/mailer.ts
Normal file
44
src/lib/mailer.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: process.env.GMAIL_USER,
|
||||
pass: process.env.GMAIL_APP_PASSWORD, // Пароль приложения из Google Account
|
||||
},
|
||||
});
|
||||
|
||||
export async function sendVerificationEmail(to: string, code: string) {
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: process.env.GMAIL_USER,
|
||||
to,
|
||||
subject: 'Подтверждение регистрации Neo Movies',
|
||||
html: `
|
||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h1 style="color: #2196f3;">Neo Movies</h1>
|
||||
<p>Здравствуйте!</p>
|
||||
<p>Для завершения регистрации введите этот код:</p>
|
||||
<div style="
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
letter-spacing: 4px;
|
||||
margin: 20px 0;
|
||||
">
|
||||
${code}
|
||||
</div>
|
||||
<p>Код действителен в течение 10 минут.</p>
|
||||
<p>Если вы не регистрировались на нашем сайте, просто проигнорируйте это письмо.</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
return { error: 'Failed to send email' };
|
||||
}
|
||||
}
|
||||
30
src/lib/mongodb.ts
Normal file
30
src/lib/mongodb.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { MongoClient } from 'mongodb';
|
||||
|
||||
if (!process.env.MONGODB_URI) {
|
||||
throw new Error('Please add your Mongo URI to .env');
|
||||
}
|
||||
|
||||
const uri = process.env.MONGODB_URI;
|
||||
let client: MongoClient;
|
||||
let clientPromise: Promise<MongoClient>;
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
let globalWithMongo = global as typeof globalThis & {
|
||||
_mongoClientPromise?: Promise<MongoClient>;
|
||||
};
|
||||
|
||||
if (!globalWithMongo._mongoClientPromise) {
|
||||
client = new MongoClient(uri);
|
||||
globalWithMongo._mongoClientPromise = client.connect();
|
||||
}
|
||||
clientPromise = globalWithMongo._mongoClientPromise;
|
||||
} else {
|
||||
client = new MongoClient(uri);
|
||||
clientPromise = client.connect();
|
||||
}
|
||||
|
||||
export async function connectToDatabase() {
|
||||
const client = await clientPromise;
|
||||
const db = client.db();
|
||||
return { db, client };
|
||||
}
|
||||
26
src/lib/movieSync.ts
Normal file
26
src/lib/movieSync.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export interface Movie {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
posterUrl: string;
|
||||
year: number;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
export const syncMovies = async (): Promise<Movie[]> => {
|
||||
// Заглушка для синхронизации фильмов
|
||||
console.log('Syncing movies...');
|
||||
return [];
|
||||
};
|
||||
|
||||
export const updateMovie = async (movie: Movie): Promise<Movie> => {
|
||||
// Заглушка для обновления фильма
|
||||
console.log(`Updating movie ${movie.title}`);
|
||||
return movie;
|
||||
};
|
||||
|
||||
export const deleteMovie = async (id: string): Promise<boolean> => {
|
||||
// Заглушка для удаления фильма
|
||||
console.log(`Deleting movie ${id}`);
|
||||
return true;
|
||||
};
|
||||
29
src/lib/registry.tsx
Normal file
29
src/lib/registry.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useServerInsertedHTML } from 'next/navigation';
|
||||
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
|
||||
|
||||
export default function StyledComponentsRegistry({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
|
||||
|
||||
useServerInsertedHTML(() => {
|
||||
const styles = styledComponentsStyleSheet.getStyleElement();
|
||||
styledComponentsStyleSheet.instance.clearTag();
|
||||
return <>{styles}</>;
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
|
||||
{children}
|
||||
</StyleSheetManager>
|
||||
);
|
||||
}
|
||||
16
src/lib/utils.ts
Normal file
16
src/lib/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const generateVerificationToken = () => {
|
||||
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
};
|
||||
|
||||
export const validateEmail = (email: string) => {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(email);
|
||||
};
|
||||
|
||||
export const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}).format(date);
|
||||
};
|
||||
Reference in New Issue
Block a user