+
+
+
+ {torrent.title || 'Раздача'}
+
+
+ {torrent.quality && torrent.quality !== 'UNKNOWN' && (
+
+ {torrent.quality}
+
+ )}
+ {type === 'tv' && torrent.season && (
+
+ Сезон {torrent.season}
+
+ )}
+ {torrent.sizeFormatted && (
+
+ {torrent.sizeFormatted}
+
+ )}
+ {additionalInfo.map(info => (
+
+ {info}
+
+ ))}
+
+
+
+
+
+ handleCopy(torrent.magnet)}
+ variant="outline"
+ size="sm"
+ className="flex-1 border-gray-300 text-gray-700 dark:border-zinc-600 dark:text-zinc-300"
+ >
+ {copiedMagnet === torrent.magnet ? (
+ <>
+
+ Скопировано
+ >
+ ) : (
+ <>
+
+ Копировать
+ >
+ )}
+
+
+ handleDownload(torrent.magnet)}
+ size="sm"
+ className="flex-1 bg-blue-600 hover:bg-blue-700 text-white dark:bg-blue-700 dark:hover:bg-blue-800"
+ >
+
+ Скачать
+
+
+
+ );
};
if (loading) {
return (
-
+
);
}
- if (!torrents) return null;
-
- const renderTorrentButtons = (list: Torrent[]) => {
- if (!list?.length) {
- return (
-
- Торрентов для выбранного сезона нет.
-
- );
- }
-
- return list.map(torrent => {
- const size = torrent.size_gb;
- const label = torrent.title || torrent.name || 'Раздача';
-
- return (
-
handleQualitySelect(torrent)}
- variant="outline"
- className="w-full items-center text-left px-3 py-2"
- >
-
- {label}
- {size !== undefined && (
- {size.toFixed(2)} GB
- )}
-
-
- );
- });
- };
+ if (!torrents || torrents.length === 0) {
+ return null;
+ }
return (
-
- {type === 'tv' && totalSeasons && totalSeasons > 0 && (
-
-
Сезоны
-
- {Array.from({ length: totalSeasons }, (_, i) => i + 1).map(season => (
- {setSelectedSeason(season); setSelectedMagnet(null);}}
- variant={selectedSeason === season ? 'default' : 'outline'}
- >
- Сезон {season}
-
- ))}
-
-
- )}
-
- {selectedSeason && torrents && (
-
-
Раздачи
-
- {renderTorrentButtons(torrents)}
-
-
- )}
-
- {selectedMagnet && (
-
-
Magnet-ссылка
-
-
- {selectedMagnet}
+
+
+
+
+
+ Скачать ({torrents.length} {torrents.length === 1 ? 'раздача' : torrents.length < 5 ? 'раздачи' : 'раздач'})
+
+
+
+
+
+
+ Выберите раздачу для скачивания
+
+
+ Найдено {filteredTorrents.length} из {torrents.length} раздач
+
+
+
+
+
+ {availableQualities.length > 0 && (
+
+
Качество
+
+ setSelectedQuality('all')}
+ className={selectedQuality === 'all' ? 'bg-zinc-800 text-white dark:bg-white dark:text-black' : 'text-gray-700 border-gray-300 dark:text-zinc-300 dark:border-zinc-600'}
+ >
+ Все ({torrents.length})
+
+ {availableQualities.map(quality => {
+ const count = torrents.filter(t =>
+ t.quality === quality &&
+ (type !== 'tv' || selectedSeason === null || availableSeasons.length === 0 || t.season === selectedSeason)
+ ).length;
+ return (
+ setSelectedQuality(quality!)}
+ className={selectedQuality === quality ? 'bg-zinc-800 text-white dark:bg-white dark:text-black' : 'text-gray-700 border-gray-300 dark:text-zinc-300 dark:border-zinc-600'}
+ >
+ {quality} ({count})
+
+ );
+ })}
+
+
+ )}
+
+ {type === 'tv' && availableSeasons.length > 0 && (
+
+
Сезон
+
+ setSelectedSeason(null)}
+ className={selectedSeason === null ? 'bg-zinc-800 text-white dark:bg-white dark:text-black' : 'text-gray-700 border-gray-300 dark:text-zinc-300 dark:border-zinc-600'}
+ >
+ Все сезоны
+
+ {availableSeasons.map(season => {
+ const count = torrents?.filter(t => t.season === season && (selectedQuality === 'all' || t.quality === selectedQuality)).length || 0;
+ return (
+ setSelectedSeason(season)}
+ className={selectedSeason === season ? 'bg-zinc-800 text-white dark:bg-white dark:text-black' : 'text-gray-700 border-gray-300 dark:text-zinc-300 dark:border-zinc-600'}
+ >
+ Сезон {season} ({count})
+
+ );
+ })}
+
+
+ )}
+
+
+
+ {filteredTorrents.length === 0 ? (
+
+ Нет раздач, соответствующих выбранным фильтрам
+
+ ) : (
+ filteredTorrents.map((torrent, index) => (
+
+ ))
+ )}
-
- {isCopied ? : }
-
-
- )}
+
+
);
}
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..4327940
--- /dev/null
+++ b/src/components/ui/dialog.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef
,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
\ No newline at end of file
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
new file mode 100644
index 0000000..c546248
--- /dev/null
+++ b/src/components/ui/select.tsx
@@ -0,0 +1,160 @@
+"use client"
+
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
\ No newline at end of file
diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts
index dccf6fe..93fea9a 100644
--- a/src/hooks/useAuth.ts
+++ b/src/hooks/useAuth.ts
@@ -2,7 +2,7 @@
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { authAPI } from '../lib/authApi';
-import { api } from '../lib/api';
+import { neoApi } from '../lib/neoApi';
interface PendingRegistration {
email: string;
@@ -16,34 +16,35 @@ export function useAuth() {
const [pending, setPending] = useState(null);
const login = async (email: string, password: string) => {
- const { data } = await authAPI.login(email, password);
- if (data?.token) {
- localStorage.setItem('token', data.token);
-
- // Extract name/email either from API response or JWT payload
- let name: string | undefined = undefined;
- let email: string | undefined = undefined;
- try {
- const payload = JSON.parse(atob(data.token.split('.')[1]));
- name = payload.name || payload.username || payload.userName || payload.sub || undefined;
- email = payload.email || undefined;
- } catch {
- // silent
+ try {
+ const response = await authAPI.login(email, password);
+ const data = response.data.data || response.data;
+ if (data?.token) {
+ localStorage.setItem('token', data.token);
+ let name: string | undefined = undefined;
+ let emailVal: string | undefined = undefined;
+ try {
+ const payload = JSON.parse(atob(data.token.split('.')[1]));
+ name = payload.name || payload.username || payload.userName || payload.sub || undefined;
+ emailVal = payload.email || undefined;
+ } catch {}
+ if (!name) name = data.user?.name || data.name || data.userName;
+ if (!emailVal) emailVal = data.user?.email || data.email;
+ if (name) localStorage.setItem('userName', name);
+ if (emailVal) localStorage.setItem('userEmail', emailVal);
+ if (typeof window !== 'undefined') {
+ window.dispatchEvent(new Event('auth-changed'));
+ }
+ neoApi.defaults.headers.common['Authorization'] = `Bearer ${data.token}`;
+ router.push('/');
+ } else {
+ throw new Error(data?.error || 'Login failed');
}
- if (!name) name = data.user?.name || data.name || data.userName;
- if (!email) email = data.user?.email || data.email;
-
- if (name) localStorage.setItem('userName', name);
- if (email) localStorage.setItem('userEmail', email);
-
- if (typeof window !== 'undefined') {
- window.dispatchEvent(new Event('auth-changed'));
+ } catch (err: any) {
+ if (err?.response?.status === 401 || err?.response?.status === 400) {
+ throw new Error('Неверный логин или пароль');
}
-
- api.defaults.headers.common['Authorization'] = `Bearer ${data.token}`;
- router.push('/');
- } else {
- throw new Error(data?.error || 'Login failed');
+ throw new Error(err?.message || 'Произошла ошибка');
}
};
@@ -66,14 +67,11 @@ export function useAuth() {
setPending(pendingData);
}
}
-
if (!pendingData) {
throw new Error('Сессия подтверждения истекла. Пожалуйста, попробуйте зарегистрироваться снова.');
}
-
await authAPI.verify(pendingData.email, code);
await login(pendingData.email, pendingData.password);
-
if (typeof window !== 'undefined') {
localStorage.removeItem('pendingVerification');
}
@@ -83,7 +81,7 @@ export function useAuth() {
const logout = () => {
localStorage.removeItem('token');
- delete api.defaults.headers.common['Authorization'];
+ delete neoApi.defaults.headers.common['Authorization'];
localStorage.removeItem('userName');
localStorage.removeItem('userEmail');
if (typeof window !== 'undefined') {
diff --git a/src/hooks/useMovies.ts b/src/hooks/useMovies.ts
index e242cb2..3d145d9 100644
--- a/src/hooks/useMovies.ts
+++ b/src/hooks/useMovies.ts
@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import { moviesAPI } from '@/lib/neoApi';
import type { Movie, MovieResponse } from '@/lib/neoApi';
-export type MovieCategory = 'popular' | 'top_rated' | 'now_playing';
+export type MovieCategory = 'popular' | 'top-rated' | 'now-playing' | 'upcoming';
interface UseMoviesProps {
initialPage?: number;
@@ -26,12 +26,15 @@ export function useMovies({ initialPage = 1, category = 'popular' }: UseMoviesPr
let response: { data: MovieResponse };
switch (movieCategory) {
- case 'top_rated':
+ case 'top-rated':
response = await moviesAPI.getTopRated(pageNum);
break;
- case 'now_playing':
+ case 'now-playing':
response = await moviesAPI.getNowPlaying(pageNum);
break;
+ case 'upcoming':
+ response = await moviesAPI.getUpcoming(pageNum);
+ break;
case 'popular':
default:
response = await moviesAPI.getPopular(pageNum);
diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts
index a8eca0b..2f400a8 100644
--- a/src/hooks/useSearch.ts
+++ b/src/hooks/useSearch.ts
@@ -1,8 +1,8 @@
'use client';
import { useState } from 'react';
-import { moviesAPI } from '@/lib/api';
-import type { Movie } from '@/lib/api';
+import { searchAPI } from '@/lib/neoApi';
+import type { Movie } from '@/lib/neoApi';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
@@ -48,7 +48,7 @@ export function useSearch() {
setCurrentQuery(query);
setCurrentPage(1);
- const response = await moviesAPI.searchMovies(query, 1);
+ const response = await searchAPI.multiSearch(query, 1);
const filteredMovies = filterMovies(response.data.results);
if (filteredMovies.length === 0) {
@@ -74,7 +74,7 @@ export function useSearch() {
setLoading(true);
const nextPage = currentPage + 1;
- const response = await moviesAPI.searchMovies(currentQuery, nextPage);
+ const response = await searchAPI.multiSearch(currentQuery, nextPage);
const filteredMovies = filterMovies(response.data.results);
setResults(prev => [...prev, ...filteredMovies]);
diff --git a/src/hooks/useTMDBMovies.ts b/src/hooks/useTMDBMovies.ts
deleted file mode 100644
index 11af920..0000000
--- a/src/hooks/useTMDBMovies.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-'use client';
-
-import { useState, useEffect, useCallback } from 'react';
-import axios from 'axios';
-import type { Movie } from '@/lib/api';
-
-const api = axios.create({
- baseURL: '/api/bridge/tmdb',
- headers: {
- 'Content-Type': 'application/json'
- }
-});
-
-export function useTMDBMovies(initialPage = 1) {
- const [movies, setMovies] = useState([]);
- const [featuredMovie, setFeaturedMovie] = useState(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [page, setPage] = useState(initialPage);
- const [totalPages, setTotalPages] = useState(0);
-
- const filterMovies = useCallback((movies: Movie[]) => {
- return movies.filter(movie => {
- if (movie.vote_average === 0) return false;
- const hasRussianLetters = /[а-яА-ЯёЁ]/.test(movie.title);
- if (!hasRussianLetters) return false;
- if (/^\d+$/.test(movie.title)) return false;
- const releaseDate = new Date(movie.release_date);
- const now = new Date();
- if (releaseDate > now) return false;
- return true;
- });
- }, []);
-
- const fetchFeaturedMovie = useCallback(async () => {
- try {
- const response = await api.get('/movie/popular', {
- params: {
- page: 1,
- language: 'ru-RU'
- }
- });
- const filteredMovies = filterMovies(response.data.results);
- if (filteredMovies.length > 0) {
- const featuredMovieData = await api.get(`/movie/${filteredMovies[0].id}`, {
- params: {
- language: 'ru-RU',
- append_to_response: 'credits,videos'
- }
- });
- setFeaturedMovie(featuredMovieData.data);
- }
- } catch (err) {
- console.error('Ошибка при загрузке featured фильма:', err);
- }
- }, [filterMovies]);
-
- const fetchMovies = useCallback(async (pageNum: number) => {
- try {
- setLoading(true);
- setError(null);
- const response = await api.get('/discover/movie', {
- params: {
- page: pageNum,
- language: 'ru-RU',
- 'vote_count.gte': 100,
- 'vote_average.gte': 1,
- sort_by: 'popularity.desc',
- include_adult: false
- }
- });
- const filteredMovies = filterMovies(response.data.results);
- setMovies(filteredMovies);
- setTotalPages(response.data.total_pages);
- setPage(pageNum);
- } catch (err) {
- console.error('Ошибка при загрузке фильмов:', err);
- setError('Произошла ошибка при загрузке фильмов');
- } finally {
- setLoading(false);
- }
- }, [filterMovies]);
-
- useEffect(() => {
- fetchFeaturedMovie();
- }, [fetchFeaturedMovie]);
-
- useEffect(() => {
- fetchMovies(page);
- }, [page, fetchMovies]);
-
- const handlePageChange = useCallback((newPage: number) => {
- window.scrollTo({ top: 0, behavior: 'smooth' });
- setPage(newPage);
- }, []);
-
- const searchMovies = useCallback(async (query: string, pageNum: number = 1) => {
- try {
- setLoading(true);
- setError(null);
- const response = await api.get('/search/movie', {
- params: {
- query,
- page: pageNum,
- language: 'ru-RU',
- include_adult: false
- }
- });
- const filteredMovies = filterMovies(response.data.results);
- setMovies(filteredMovies);
- setTotalPages(response.data.total_pages);
- setPage(pageNum);
- } catch (err) {
- console.error('Ошибка при поиске фильмов:', err);
- setError('Произошла ошибка при поиске фильмов');
- } finally {
- setLoading(false);
- }
- }, [filterMovies]);
-
- const getUpcomingMovies = useCallback(async (pageNum: number = 1) => {
- try {
- setLoading(true);
- setError(null);
- const response = await api.get('/movie/upcoming', {
- params: {
- page: pageNum,
- language: 'ru-RU',
- 'vote_count.gte': 100,
- 'vote_average.gte': 1
- }
- });
- const filteredMovies = filterMovies(response.data.results);
- setMovies(filteredMovies);
- setTotalPages(response.data.total_pages);
- setPage(pageNum);
- } catch (err) {
- console.error('Ошибка при загрузке предстоящих фильмов:', err);
- setError('Произошла ошибка при загрузке предстоящих фильмов');
- } finally {
- setLoading(false);
- }
- }, [filterMovies]);
-
- const getTopRatedMovies = useCallback(async (pageNum: number = 1) => {
- try {
- setLoading(true);
- setError(null);
- const response = await api.get('/movie/top_rated', {
- params: {
- page: pageNum,
- language: 'ru-RU',
- 'vote_count.gte': 100,
- 'vote_average.gte': 1
- }
- });
- const filteredMovies = filterMovies(response.data.results);
- setMovies(filteredMovies);
- setTotalPages(response.data.total_pages);
- setPage(pageNum);
- } catch (err) {
- console.error('Ошибка при загрузке топ фильмов:', err);
- setError('Произошла ошибка при загрузке топ фильмов');
- } finally {
- setLoading(false);
- }
- }, [filterMovies]);
-
- return {
- movies,
- featuredMovie,
- loading,
- error,
- totalPages,
- currentPage: page,
- setPage: handlePageChange,
- searchMovies,
- getUpcomingMovies,
- getTopRatedMovies
- };
-}
diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts
index e9127be..ea2610e 100644
--- a/src/hooks/useUser.ts
+++ b/src/hooks/useUser.ts
@@ -17,8 +17,7 @@ export function useUser() {
const login = async (email: string, password: string) => {
try {
- // Сначала проверяем, верифицирован ли аккаунт
- const verificationCheck = await fetch('/api/auth/check-verification', {
+ const verificationCheck = await fetch('/api/v1/auth/check-verification', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
@@ -27,8 +26,7 @@ export function useUser() {
const { isVerified } = await verificationCheck.json();
if (!isVerified) {
- // Если аккаунт не верифицирован, отправляем новый код и переходим к верификации
- const verificationResponse = await fetch('/api/auth/verify', {
+ const verificationResponse = await fetch('/api/v1/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
@@ -43,15 +41,28 @@ export function useUser() {
return;
}
- // Если аккаунт верифицирован, выполняем вход
- const result = await signIn('credentials', {
- redirect: false,
- email,
- password,
+ const loginResponse = await fetch('/api/v1/auth/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, password })
});
- if (result?.error) {
- throw new Error(result.error);
+ if (!loginResponse.ok) {
+ const data = await loginResponse.json();
+ throw new Error(data.error || 'Неверный email или пароль');
+ }
+
+ const loginData = await loginResponse.json();
+ const { token, user } = loginData.data || loginData;
+
+ if (token) {
+ localStorage.setItem('token', token);
+ if (user?.name) localStorage.setItem('userName', user.name);
+ if (user?.email) localStorage.setItem('userEmail', user.email);
+
+ if (typeof window !== 'undefined') {
+ window.dispatchEvent(new Event('auth-changed'));
+ }
}
router.push('/');
@@ -62,7 +73,7 @@ export function useUser() {
const register = async (email: string, password: string, name: string) => {
try {
- const response = await fetch('/api/auth/register', {
+ const response = await fetch('/api/v1/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, name }),
@@ -73,8 +84,7 @@ export function useUser() {
throw new Error(data.error || 'Ошибка при регистрации');
}
- // Отправляем код подтверждения
- const verificationResponse = await fetch('/api/auth/verify', {
+ const verificationResponse = await fetch('/api/v1/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
@@ -98,7 +108,7 @@ export function useUser() {
}
try {
- const response = await fetch('/api/auth/verify', {
+ const response = await fetch('/api/v1/auth/verify', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -112,15 +122,31 @@ export function useUser() {
throw new Error(data.error || 'Неверный код подтверждения');
}
- // После успешной верификации выполняем вход
- const result = await signIn('credentials', {
- redirect: false,
- email: pendingRegistration.email,
- password: pendingRegistration.password,
+ const loginResponse = await fetch('/api/v1/auth/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ email: pendingRegistration.email,
+ password: pendingRegistration.password
+ })
});
- if (result?.error) {
- throw new Error(result.error);
+ if (!loginResponse.ok) {
+ const data = await loginResponse.json();
+ throw new Error(data.error || 'Ошибка входа после верификации');
+ }
+
+ const loginData = await loginResponse.json();
+ const { token, user } = loginData.data || loginData;
+
+ if (token) {
+ localStorage.setItem('token', token);
+ if (user?.name) localStorage.setItem('userName', user.name);
+ if (user?.email) localStorage.setItem('userEmail', user.email);
+
+ if (typeof window !== 'undefined') {
+ window.dispatchEvent(new Event('auth-changed'));
+ }
}
setIsVerifying(false);
@@ -132,7 +158,15 @@ export function useUser() {
};
const logout = () => {
- signOut({ callbackUrl: '/login' });
+ localStorage.removeItem('token');
+ localStorage.removeItem('userName');
+ localStorage.removeItem('userEmail');
+
+ if (typeof window !== 'undefined') {
+ window.dispatchEvent(new Event('auth-changed'));
+ }
+
+ router.push('/login');
};
return {
diff --git a/src/lib/api.ts b/src/lib/api.ts
deleted file mode 100644
index 96e6530..0000000
--- a/src/lib/api.ts
+++ /dev/null
@@ -1,262 +0,0 @@
-import axios from 'axios';
-
-const API_URL = process.env.NEXT_PUBLIC_API_URL;
-
-if (!API_URL) {
- throw new Error('NEXT_PUBLIC_API_URL is not defined in environment variables');
-}
-
-export const api = axios.create({
- baseURL: API_URL,
- headers: {
- 'Content-Type': 'application/json'
- }
-});
-
-// Attach JWT token if present in localStorage
-if (typeof window !== 'undefined') {
- const token = localStorage.getItem('token');
- if (token) {
- api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
- }
-}
-
-// Update stored token on login response with { token }
-api.interceptors.response.use((response) => {
- if (response.config.url?.includes('/auth/login') && response.data?.token) {
- if (typeof window !== 'undefined') {
- localStorage.setItem('token', response.data.token);
- api.defaults.headers.common['Authorization'] = `Bearer ${response.data.token}`;
- }
- }
- return response;
-});
-
-export interface Category {
- id: number;
- name: string;
- slug: string;
-}
-
-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;
- imdb_id?: string | null;
- 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;
-}
-
-export interface MovieResponse {
- page: number;
- results: Movie[];
- total_pages: number;
- total_results: number;
-}
-
-export interface TVShowResponse {
- page: number;
- results: TVShow[];
- total_pages: number;
- total_results: number;
-}
-
-export const categoriesAPI = {
- // Получение всех категорий
- getCategories() {
- return api.get<{ categories: Category[] }>('/categories');
- },
-
- // Получение категории по ID
- getCategory(id: number) {
- return api.get(`/categories/${id}`);
- },
-
- // Получение фильмов по категории
- getMoviesByCategory(categoryId: number, page = 1) {
- return api.get(`/categories/${categoryId}/movies`, {
- params: { page }
- });
- },
-
- // Получение сериалов по категории
- getTVShowsByCategory(categoryId: number, page = 1) {
- return api.get(`/categories/${categoryId}/tv`, {
- params: { page }
- });
- }
-};
-
-export const moviesAPI = {
- // Получение популярных фильмов
- getPopular(page = 1) {
- return api.get('/movies/popular', {
- params: { page }
- });
- },
-
- // Получение данных о фильме по его TMDB ID
- getMovie(id: string | number) {
- return api.get(`/movies/${id}`);
- },
-
- // Получение IMDb ID по TMDB ID для плеера
- getImdbId(tmdbId: string | number) {
- return api.get<{ imdb_id: string }>(`/movies/${tmdbId}/external-ids`);
- },
-
- // Получение видео по TMDB ID для плеера
- getVideo(tmdbId: string | number) {
- return api.get<{ results: Video[] }>(`/movies/${tmdbId}/videos`);
- },
-
- // Поиск фильмов
- searchMovies(query: string, page = 1) {
- return api.get('/movies/search', {
- params: { query, page }
- });
- },
-
- // Получение предстоящих фильмов
- getUpcoming(page = 1) {
- return api.get('/movies/upcoming', {
- params: { page }
- });
- },
-
- // Получение лучших фильмов
- getTopRated(page = 1) {
- return api.get('/movies/top-rated', {
- params: { page }
- });
- },
-
- // Получение фильмов по жанру
- getMoviesByGenre(genreId: number, page = 1) {
- return api.get('/movies/discover', {
- params: { with_genres: genreId, page }
- });
- },
-
- // Получение жанров
- getGenres() {
- return api.get<{ genres: Genre[] }>('/movies/genres');
- }
-};
-
-export const tvAPI = {
- // Получение популярных сериалов
- getPopular(page = 1) {
- return api.get('/tv/popular', {
- params: { page }
- });
- },
-
- // Получение данных о сериале по его TMDB ID
- getShow(id: string | number) {
- return api.get(`/tv/${id}`);
- },
-
- // Получение IMDb ID по TMDB ID для плеера
- getImdbId(tmdbId: string | number) {
- return api.get<{ imdb_id: string }>(`/tv/${tmdbId}/external-ids`);
- },
-
- // Поиск сериалов
- searchShows(query: string, page = 1) {
- return api.get('/tv/search', {
- params: { query, page }
- });
- }
-};
-
-// Мультипоиск (фильмы и сериалы)
-export const searchAPI = {
- multiSearch(query: string, page = 1) {
- return api.get('/search/multi', {
- params: { query, page }
- });
- }
-};
diff --git a/src/lib/authApi.ts b/src/lib/authApi.ts
index 8602c24..1522598 100644
--- a/src/lib/authApi.ts
+++ b/src/lib/authApi.ts
@@ -1,19 +1,10 @@
-import { api } from './api';
+import { neoApi } from './neoApi';
export const authAPI = {
- register(data: { email: string; password: string; name?: string }) {
- return api.post('/auth/register', data);
- },
- resendCode(email: string) {
- return api.post('/auth/resend-code', { email });
- },
- verify(email: string, code: string) {
- return api.post('/auth/verify', { email, code });
- },
- login(email: string, password: string) {
- return api.post('/auth/login', { email, password });
- },
- deleteAccount() {
- return api.delete('/auth/profile');
- }
+ register: (data: any) => neoApi.post('/api/v1/auth/register', data),
+ resendCode: (email: string) => neoApi.post('/api/v1/auth/resend-code', { email }),
+ verify: (email: string, code: string) => neoApi.post('/api/v1/auth/verify', { email, code }),
+ checkVerification: (email: string) => neoApi.post('/api/v1/auth/check-verification', { email }),
+ login: (email: string, password: string) => neoApi.post('/api/v1/auth/login', { email, password }),
+ deleteAccount: () => neoApi.delete('/api/v1/auth/profile'),
};
diff --git a/src/lib/favoritesApi.ts b/src/lib/favoritesApi.ts
index 791ed5d..9ca25a9 100644
--- a/src/lib/favoritesApi.ts
+++ b/src/lib/favoritesApi.ts
@@ -1,25 +1,24 @@
-import { api } from './api';
-
+import { neoApi } from './neoApi';
export const favoritesAPI = {
- // Получить все избранные
+ // Получение всех избранных
getFavorites() {
- return api.get('/favorites');
+ return neoApi.get('/api/v1/favorites');
},
- // Добавить в избранное
- addFavorite(data: { mediaId: string; mediaType: 'movie' | 'tv', title: string, posterPath: string }) {
- const { mediaId, mediaType, title, posterPath } = data;
- return api.post(`/favorites/${mediaId}?mediaType=${mediaType}`, { title, posterPath });
+ // Добавление в избранное
+ addFavorite(data: { mediaId: string; mediaType: string; title: string; posterPath?: string }) {
+ const { mediaId, mediaType, ...rest } = data;
+ return neoApi.post(`/api/v1/favorites/${mediaId}?mediaType=${mediaType}`, rest);
},
- // Удалить из избранного
+ // Удаление из избранного
removeFavorite(mediaId: string) {
- return api.delete(`/favorites/${mediaId}`);
+ return neoApi.delete(`/api/v1/favorites/${mediaId}`);
},
- // Проверить есть ли в избранном
+ // Проверка, добавлен ли в избранное
checkFavorite(mediaId: string) {
- return api.get(`/favorites/check/${mediaId}`);
+ return neoApi.get(`/api/v1/favorites/check/${mediaId}`);
}
};
diff --git a/src/lib/mongodb.ts b/src/lib/mongodb.ts
index 6949022..90e5a04 100644
--- a/src/lib/mongodb.ts
+++ b/src/lib/mongodb.ts
@@ -29,32 +29,26 @@ export async function connectToDatabase() {
return { db, client };
}
-// Инициализация MongoDB
export async function initMongoDB() {
try {
const { db } = await connectToDatabase();
- // Создаем уникальный индекс для избранного
await db.collection('favorites').createIndex(
{ userId: 1, mediaId: 1, mediaType: 1 },
{ unique: true }
);
- console.log('MongoDB initialized successfully');
} catch (error) {
console.error('Error initializing MongoDB:', error);
throw error;
}
}
-// Функция для сброса и создания индексов
export async function resetIndexes() {
const { db } = await connectToDatabase();
- // Удаляем все индексы из коллекции favorites
await db.collection('favorites').dropIndexes();
- // Создаем новый правильный индекс
await db.collection('favorites').createIndex(
{ userId: 1, mediaId: 1, mediaType: 1 },
{ unique: true }
diff --git a/src/lib/neoApi.ts b/src/lib/neoApi.ts
index f4ba8b3..d4fa90f 100644
--- a/src/lib/neoApi.ts
+++ b/src/lib/neoApi.ts
@@ -1,18 +1,25 @@
import axios from 'axios';
-const API_URL = process.env.NEXT_PUBLIC_API_URL;
+const API_URL = process.env.NEXT_PUBLIC_API_URL || 'https://neomovies-test-api.vercel.app';
+// Создание экземпляра Axios с базовыми настройками
export const neoApi = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json'
},
- timeout: 30000 // Увеличиваем таймаут до 30 секунд
+ timeout: 30000
});
-// Добавляем перехватчики запросов
+// Перехватчик запросов
neoApi.interceptors.request.use(
(config) => {
+ // Получение токена из localStorage или другого хранилища
+ const token = localStorage.getItem('token');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ // Логика для пагинации
if (config.params?.page) {
const page = parseInt(config.params.page);
if (isNaN(page) || page < 1) {
@@ -27,29 +34,34 @@ neoApi.interceptors.request.use(
}
);
-// Добавляем перехватчики ответов
+// Перехватчик ответов
neoApi.interceptors.response.use(
(response) => {
+ if (response.data && response.data.success && response.data.data !== undefined) {
+ response.data = response.data.data;
+ }
return response;
},
(error) => {
console.error('❌ Response Error:', {
status: error.response?.status,
+ statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method,
- message: error.message
+ message: error.message,
+ data: error.response?.data
});
return Promise.reject(error);
}
);
-// Функция для получения URL изображения
export const getImageUrl = (path: string | null, size: string = 'w500'): string => {
if (!path) return '/images/placeholder.jpg';
- // Извлекаем только ID изображения из полного пути
- const imageId = path.split('/').pop();
- if (!imageId) return '/images/placeholder.jpg';
- return `${API_URL}/images/${size}/${imageId}`;
+ if (path.startsWith('http')) {
+ return path;
+ }
+ const cleanPath = path.startsWith('/') ? path.slice(1) : path;
+ return `${API_URL}/api/v1/images/${size}/${cleanPath}`;
};
export interface Genre {
@@ -80,10 +92,44 @@ export interface MovieResponse {
total_results: number;
}
+export interface TorrentResult {
+ title: string;
+ tracker: string;
+ size: string;
+ seeders: number;
+ peers: number;
+ leechers: number;
+ quality: string;
+ voice?: string[];
+ types?: string[];
+ seasons?: number[];
+ category: string;
+ magnet: string;
+ torrent_link?: string;
+ details?: string;
+ publish_date: string;
+ added_date?: string;
+ source: string;
+}
+
+export interface TorrentSearchResponse {
+ query: string;
+ results: TorrentResult[];
+ total: number;
+}
+
+export interface AvailableSeasonsResponse {
+ title: string;
+ originalTitle: string;
+ year: string;
+ seasons: number[];
+ total: number;
+}
+
export const searchAPI = {
// Поиск фильмов
searchMovies(query: string, page = 1) {
- return neoApi.get('/movies/search', {
+ return neoApi.get('/api/v1/movies/search', {
params: {
query,
page
@@ -94,7 +140,7 @@ export const searchAPI = {
// Поиск сериалов
searchTV(query: string, page = 1) {
- return neoApi.get('/tv/search', {
+ return neoApi.get('/api/v1/tv/search', {
params: {
query,
page
@@ -103,46 +149,19 @@ export const searchAPI = {
});
},
- // Мультипоиск (фильмы и сериалы)
+ // Мультипоиск (фильмы и сериалы) - новый эндпоинт
async multiSearch(query: string, page = 1) {
- // Запускаем параллельные запросы к фильмам и сериалам
try {
- const [moviesResponse, tvResponse] = await Promise.all([
- this.searchMovies(query, page),
- this.searchTV(query, page)
- ]);
+ // Используем новый эндпоинт Go API
+ const response = await neoApi.get('/search/multi', {
+ params: {
+ query,
+ page
+ },
+ timeout: 30000
+ });
- // Объединяем результаты
- const moviesData = moviesResponse.data;
- const tvData = tvResponse.data;
-
- // Метаданные для пагинации
- const totalResults = (moviesData.total_results || 0) + (tvData.total_results || 0);
- const totalPages = Math.max(moviesData.total_pages || 0, tvData.total_pages || 0);
-
- // Добавляем информацию о типе контента
- const moviesWithType = (moviesData.results || []).map(movie => ({
- ...movie,
- media_type: 'movie'
- }));
-
- const tvWithType = (tvData.results || []).map(show => ({
- ...show,
- media_type: 'tv'
- }));
-
- // Объединяем и сортируем по популярности
- const combinedResults = [...moviesWithType, ...tvWithType]
- .sort((a, b) => (b.popularity || 0) - (a.popularity || 0));
-
- return {
- data: {
- page: parseInt(String(page)),
- results: combinedResults,
- total_pages: totalPages,
- total_results: totalResults
- }
- };
+ return response;
} catch (error) {
console.error('Error in multiSearch:', error);
throw error;
@@ -153,7 +172,7 @@ export const searchAPI = {
export const moviesAPI = {
// Получение популярных фильмов
getPopular(page = 1) {
- return neoApi.get('/movies/popular', {
+ return neoApi.get('/api/v1/movies/popular', {
params: { page },
timeout: 30000
});
@@ -161,7 +180,7 @@ export const moviesAPI = {
// Получение фильмов с высоким рейтингом
getTopRated(page = 1) {
- return neoApi.get('/movies/top_rated', {
+ return neoApi.get('/api/v1/movies/top-rated', {
params: { page },
timeout: 30000
});
@@ -169,7 +188,15 @@ export const moviesAPI = {
// Получение новинок
getNowPlaying(page = 1) {
- return neoApi.get('/movies/now_playing', {
+ return neoApi.get('/api/v1/movies/now-playing', {
+ params: { page },
+ timeout: 30000
+ });
+ },
+
+ // Получение предстоящих фильмов
+ getUpcoming(page = 1) {
+ return neoApi.get('/api/v1/movies/upcoming', {
params: { page },
timeout: 30000
});
@@ -177,12 +204,12 @@ export const moviesAPI = {
// Получение данных о фильме по его ID
getMovie(id: string | number) {
- return neoApi.get(`/movies/${id}`, { timeout: 30000 });
+ return neoApi.get(`/api/v1/movies/${id}`, { timeout: 30000 });
},
// Поиск фильмов
searchMovies(query: string, page = 1) {
- return neoApi.get('/movies/search', {
+ return neoApi.get('/api/v1/movies/search', {
params: {
query,
page
@@ -191,16 +218,40 @@ export const moviesAPI = {
});
},
- // Получение IMDB ID
- getImdbId(id: string | number) {
- return neoApi.get(`/movies/${id}/external_ids`, { timeout: 30000 }).then(res => res.data.imdb_id);
+ // Получение IMDB и других external ids
+ getExternalIds(id: string | number) {
+ return neoApi.get(`/api/v1/movies/${id}/external-ids`, { timeout: 30000 }).then(res => res.data);
}
};
export const tvShowsAPI = {
// Получение популярных сериалов
getPopular(page = 1) {
- return neoApi.get('/tv/popular', {
+ return neoApi.get('/api/v1/tv/popular', {
+ params: { page },
+ timeout: 30000
+ });
+ },
+
+ // Получение сериалов с высоким рейтингом
+ getTopRated(page = 1) {
+ return neoApi.get('/api/v1/tv/top-rated', {
+ params: { page },
+ timeout: 30000
+ });
+ },
+
+ // Получение сериалов в эфире
+ getOnTheAir(page = 1) {
+ return neoApi.get('/api/v1/tv/on-the-air', {
+ params: { page },
+ timeout: 30000
+ });
+ },
+
+ // Получение сериалов, которые выходят сегодня
+ getAiringToday(page = 1) {
+ return neoApi.get('/api/v1/tv/airing-today', {
params: { page },
timeout: 30000
});
@@ -208,12 +259,12 @@ export const tvShowsAPI = {
// Получение данных о сериале по его ID
getTVShow(id: string | number) {
- return neoApi.get(`/tv/${id}`, { timeout: 30000 });
+ return neoApi.get(`/api/v1/tv/${id}`, { timeout: 30000 });
},
// Поиск сериалов
searchTVShows(query: string, page = 1) {
- return neoApi.get('/tv/search', {
+ return neoApi.get('/api/v1/tv/search', {
params: {
query,
page
@@ -222,8 +273,101 @@ export const tvShowsAPI = {
});
},
- // Получение IMDB ID
- getImdbId(id: string | number) {
- return neoApi.get(`/tv/${id}/external-ids`, { timeout: 30000 }).then(res => res.data.imdb_id);
+ // Получение IMDB и других external ids
+ getExternalIds(id: string | number) {
+ return neoApi.get(`/api/v1/tv/${id}/external-ids`, { timeout: 30000 }).then(res => res.data);
+ }
+};
+
+export const torrentsAPI = {
+ // Поиск торрентов по IMDB ID
+ searchTorrents(imdbId: string, type: 'movie' | 'tv', options?: {
+ season?: number;
+ quality?: string;
+ minQuality?: string;
+ maxQuality?: string;
+ excludeQualities?: string;
+ hdr?: boolean;
+ hevc?: boolean;
+ sortBy?: string;
+ sortOrder?: string;
+ groupByQuality?: boolean;
+ groupBySeason?: boolean;
+ }) {
+ const params: any = { type };
+
+ if (options) {
+ Object.entries(options).forEach(([key, value]) => {
+ if (value !== undefined) {
+ if (key === 'excludeQualities' && Array.isArray(value)) {
+ params[key] = value.join(',');
+ } else {
+ params[key] = value;
+ }
+ }
+ });
+ }
+
+ return neoApi.get(`/api/v1/torrents/search/${imdbId}`, {
+ params,
+ timeout: 30000
+ });
+ },
+
+ // Получение доступных сезонов для сериала
+ getAvailableSeasons(title: string, originalTitle?: string, year?: string) {
+ const params: any = { title };
+ if (originalTitle) params.originalTitle = originalTitle;
+ if (year) params.year = year;
+
+ return neoApi.get('/api/v1/torrents/seasons', {
+ params,
+ timeout: 30000
+ });
+ },
+
+ // Универсальный поиск торрентов по запросу
+ searchByQuery(query: string, type: 'movie' | 'tv' | 'anime' = 'movie', year?: string) {
+ const params: any = { query, type };
+ if (year) params.year = year;
+
+ return neoApi.get('/api/v1/torrents/search', {
+ params,
+ timeout: 30000
+ });
+ }
+};
+
+export const categoriesAPI = {
+ // Получение всех категорий
+ getCategories() {
+ return neoApi.get<{ categories: Category[] }>('/api/v1/categories');
+ },
+
+ // Получение категории по ID
+ getCategory(id: number) {
+ return neoApi.get(`/api/v1/categories/${id}`);
+ },
+
+ // Получение фильмов по категории
+ getMoviesByCategory(categoryId: number, page = 1) {
+ return neoApi.get(`/api/v1/categories/${categoryId}/movies`, {
+ params: { page }
+ });
+ },
+
+ // Получение сериалов по категории
+ getTVShowsByCategory(categoryId: number, page = 1) {
+ return neoApi.get(`/api/v1/categories/${categoryId}/tv`, {
+ params: { page }
+ });
+ }
+};
+
+// Новый API-клиент для работы с аутентификацией и профилем
+export const authAPI = {
+ // Новый метод для удаления аккаунта
+ deleteAccount() {
+ return neoApi.delete('/api/v1/profile');
}
};
diff --git a/src/lib/reactionsApi.ts b/src/lib/reactionsApi.ts
index 9a1fac0..ad99200 100644
--- a/src/lib/reactionsApi.ts
+++ b/src/lib/reactionsApi.ts
@@ -1,28 +1,30 @@
-import { api } from './api';
+import { neoApi } from './neoApi';
export interface Reaction {
- _id: string;
- userId: string;
+ type: 'like' | 'dislike';
mediaId: string;
mediaType: 'movie' | 'tv';
- type: 'fire' | 'nice' | 'think' | 'bore' | 'shit';
- createdAt: string;
}
export const reactionsAPI = {
- // [PUBLIC] Получить счетчики для всех типов реакций
- getReactionCounts(mediaType: string, mediaId: string): Promise<{ data: Record }> {
- return api.get(`/reactions/${mediaType}/${mediaId}/counts`);
+ // Получение счетчиков реакций
+ getReactionCounts(mediaType: string, mediaId: string) {
+ return neoApi.get(`/api/v1/reactions/${mediaType}/${mediaId}/counts`);
},
- // [AUTH] Получить реакцию пользователя для медиа
- getMyReaction(mediaType: string, mediaId: string): Promise<{ data: Reaction | null }> {
- return api.get(`/reactions/${mediaType}/${mediaId}/my-reaction`);
+ // Получение моей реакции
+ getMyReaction(mediaType: string, mediaId: string) {
+ return neoApi.get(`/api/v1/reactions/${mediaType}/${mediaId}/my-reaction`);
},
- // [AUTH] Установить/обновить/удалить реакцию
- setReaction(mediaType: string, mediaId: string, type: Reaction['type']): Promise<{ data: Reaction }> {
+ // Установка реакции
+ setReaction(mediaType: string, mediaId: string, type: 'like' | 'dislike') {
const fullMediaId = `${mediaType}_${mediaId}`;
- return api.post('/reactions', { mediaId: fullMediaId, type });
+ return neoApi.post('/api/v1/reactions', { mediaId: fullMediaId, type });
},
+
+ // Удаление реакции
+ removeReaction(mediaType: string, mediaId: string) {
+ return neoApi.delete(`/api/v1/reactions/${mediaType}/${mediaId}`);
+ }
};
\ No newline at end of file
diff --git a/src/models/Favorite.ts b/src/models/Favorite.ts
index 591f2d2..702bfec 100644
--- a/src/models/Favorite.ts
+++ b/src/models/Favorite.ts
@@ -35,7 +35,6 @@ const FavoriteSchema: Schema = new Schema({
timestamps: true,
});
-// Ensure a user can't favorite the same item multiple times
FavoriteSchema.index({ userId: 1, mediaId: 1 }, { unique: true });
export default mongoose.models.Favorite || mongoose.model('Favorite', FavoriteSchema);
diff --git a/src/models/User.ts b/src/models/User.ts
index aa109ee..9176d16 100644
--- a/src/models/User.ts
+++ b/src/models/User.ts
@@ -40,7 +40,6 @@ const userSchema = new Schema({
timestamps: true,
});
-// Не включаем пароль в запросы по умолчанию
userSchema.set('toJSON', {
transform: function(doc, ret) {
delete ret.password;
@@ -48,7 +47,6 @@ userSchema.set('toJSON', {
}
});
-// Хэшируем пароль перед сохранением
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
@@ -61,7 +59,6 @@ userSchema.pre('save', async function(next) {
}
});
-// Метод для проверки пароля
userSchema.methods.comparePassword = async function(candidatePassword: string) {
try {
return await bcrypt.compare(candidatePassword, this.password);