Release 2.3

This commit is contained in:
2025-07-08 13:41:04 +03:00
parent c8988b4979
commit bf3e231f67
15 changed files with 160 additions and 300 deletions

View File

@@ -31,7 +31,6 @@ export default function LoginClient() {
await login(email, password); await login(email, password);
} else { } else {
await register(email, password, name); await register(email, password, name);
localStorage.setItem('password', password);
router.push(`/verify?email=${encodeURIComponent(email)}`); router.push(`/verify?email=${encodeURIComponent(email)}`);
} }
} catch (err) { } catch (err) {

View File

@@ -124,6 +124,7 @@ export default function MovieContent({ movieId, initialMovie }: MovieContentProp
title={movie.title} title={movie.title}
posterPath={movie.poster_path} posterPath={movie.poster_path}
className="!bg-secondary !text-secondary-foreground hover:!bg-secondary/80" className="!bg-secondary !text-secondary-foreground hover:!bg-secondary/80"
showText={true}
/> />
</div> </div>

View File

@@ -3,7 +3,7 @@
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Loader2, LogOut } from 'lucide-react'; import { Loader2, User, LogOut, Trash2 } from 'lucide-react';
export default function ProfilePage() { export default function ProfilePage() {
const { logout } = useAuth(); const { logout } = useAuth();
@@ -23,44 +23,54 @@ export default function ProfilePage() {
} }
}, [router]); }, [router]);
const handleSignOut = () => { const handleDeleteAccount = () => {
logout(); // TODO: Implement account deletion logic
alert('Функция удаления аккаунта в разработке.');
}; };
if (loading) { if (loading) {
return ( return (
<div className="flex min-h-screen w-full items-center justify-center bg-[#F9F6EE] dark:bg-gray-900"> <div className="flex min-h-screen w-full items-center justify-center bg-background text-foreground">
<Loader2 className="h-16 w-16 animate-spin text-red-500" /> <Loader2 className="h-16 w-16 animate-spin text-accent" />
</div> </div>
); );
} }
if (!userName) {
// This can happen briefly before redirect, or if localStorage is cleared.
return null;
}
return ( return (
<div className="min-h-screen w-full bg-[#F9F6EE] dark:bg-[#1e1e1e] pt-24 sm:pt-32"> <div className="min-h-screen w-full bg-background text-foreground flex items-center justify-center p-4">
<div className="flex justify-center px-4"> <div className="w-full max-w-md">
<div className="w-full max-w-md rounded-2xl bg-white dark:bg-[#49372E] p-8 shadow-lg"> <div className="bg-warm-50 dark:bg-warm-900 rounded-lg shadow-lg p-8 text-center mb-6">
<div className="flex flex-col items-center text-center"> <div className="mb-6 mx-auto flex h-28 w-28 items-center justify-center rounded-full bg-gray-200 dark:bg-white/10 text-4xl font-bold text-gray-700 dark:text-gray-200 ring-4 ring-gray-100 dark:ring-white/5">
<div className="mb-6 flex h-28 w-28 items-center justify-center rounded-full bg-gray-200 dark:bg-white/10 text-4xl font-bold text-gray-700 dark:text-gray-200 ring-4 ring-gray-100 dark:ring-white/5">
{userName?.split(' ').map(n => n[0]).join('').toUpperCase() || ''} {userName?.split(' ').map(n => n[0]).join('').toUpperCase() || ''}
</div> </div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">{userName}</h1> <h1 className="text-3xl font-bold text-foreground">{userName}</h1>
<p className="mt-2 text-base text-gray-500 dark:text-gray-300">{userEmail}</p> <p className="mt-2 text-base text-muted-foreground">{userEmail}</p>
</div>
<div className="bg-warm-50 dark:bg-warm-900 rounded-lg shadow-lg p-6 sm:p-8 mb-6">
<h2 className="text-xl font-bold text-foreground mb-4 text-left">Управление аккаунтом</h2>
<button <button
onClick={handleSignOut} onClick={logout}
className="mt-8 inline-flex items-center gap-2.5 rounded-lg bg-red-600 px-6 py-3 text-base font-semibold text-white shadow-md transition-colors hover:bg-red-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600" className="w-full sm:w-auto px-6 py-3 border border-transparent text-base font-medium rounded-lg text-white bg-accent hover:bg-accent/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent flex items-center justify-center gap-2"
> >
<LogOut size={20} /> <LogOut size={20} />
<span>Выйти</span> <span>Выйти из аккаунта</span>
</button>
</div>
<div className="bg-red-500/10 border-2 border-dashed border-red-500/50 rounded-lg p-6 sm:p-8 text-center">
<h2 className="text-xl font-bold text-red-500 mb-4">Опасная зона</h2>
<p className="text-red-400 mb-6">Это действие нельзя будет отменить. Все ваши данные, включая избранное, будут удалены.</p>
<button
onClick={handleDeleteAccount}
className="w-full sm:w-auto px-6 py-3 border border-transparent text-base font-medium rounded-lg text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 flex items-center justify-center gap-2"
>
<Trash2 size={20} />
<span>Удалить аккаунт</span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div>
); );
} }

View File

@@ -1,12 +1,11 @@
'use client'; 'use client';
import SettingsContent from '@/components/SettingsContent'; import SettingsContent from '@/components/SettingsContent';
import PageLayout from '@/components/PageLayout';
export default function SettingsPage() { export default function SettingsPage() {
return ( return (
<PageLayout> <div className="min-h-screen flex items-center justify-center p-4 bg-background text-foreground">
<SettingsContent /> <SettingsContent />
</PageLayout> </div>
); );
} }

View File

@@ -128,6 +128,7 @@ export default function TVContent({ showId, initialShow }: TVContentProps) {
title={show.name} title={show.name}
posterPath={show.poster_path} posterPath={show.poster_path}
className="!bg-secondary !text-secondary-foreground hover:!bg-secondary/80" className="!bg-secondary !text-secondary-foreground hover:!bg-secondary/80"
showText={true}
/> />
</div> </div>

View File

@@ -1,102 +1,10 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import styled from 'styled-components';
import Image from 'next/image'; import Image from 'next/image';
import type { TVShow } from '@/types/movie'; import type { TVShow } from '@/types/movie';
import { tvShowsAPI, getImageUrl } from '@/lib/neoApi'; import { tvShowsAPI, getImageUrl } from '@/lib/neoApi';
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';
const Container = styled.div`
width: 100%;
max-width: 1200px;
margin: 0 auto;
`;
const PosterContainer = styled.div`
position: relative;
width: 100%;
height: 450px;
border-radius: 0.5rem;
overflow: hidden;
`;
const Info = styled.div`
color: white;
`;
const Title = styled.h1`
font-size: 2rem;
margin-bottom: 1rem;
color: white;
`;
const Overview = styled.p`
color: rgba(255, 255, 255, 0.8);
line-height: 1.6;
margin-bottom: 1rem;
`;
const Details = styled.div`
flex: 1;
`;
const DetailItem = styled.div`
margin-bottom: 0.5rem;
color: rgba(255, 255, 255, 0.8);
`;
const Label = styled.span`
color: rgba(255, 255, 255, 0.6);
margin-right: 0.5rem;
`;
const Value = styled.span`
color: white;
`;
const ButtonContainer = styled.div`
margin-top: 1rem;
`;
const PlayerContainer = styled.div`
position: relative;
width: 100%;
aspect-ratio: 16/9;
background: rgba(0, 0, 0, 0.3);
border-radius: 0.5rem;
overflow: hidden;
`;
const Content = styled.div`
display: grid;
grid-template-columns: 300px 1fr;
gap: 2rem;
margin-bottom: 2rem;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
`;
const parseRussianDate = (dateStr: string): Date | null => {
if (!dateStr) return null;
const months: { [key: string]: number } = {
'января': 0, 'февраля': 1, 'марта': 2, 'апреля': 3,
'мая': 4, 'июня': 5, 'июля': 6, 'августа': 7,
'сентября': 8, 'октября': 9, 'ноября': 10, 'декабря': 11
};
const match = dateStr.match(/(\d+)\s+([а-яё]+)\s+(\d{4})/i);
if (!match) return null;
const [, day, month, year] = match;
const monthIndex = months[month.toLowerCase()];
if (monthIndex === undefined) return null;
return new Date(parseInt(year), monthIndex, parseInt(day));
};
interface TVShowContentProps { interface TVShowContentProps {
tvShowId: string; tvShowId: string;
@@ -122,67 +30,69 @@ export default function TVShowContent({ tvShowId, initialShow }: TVShowContentPr
}, [tvShowId]); }, [tvShowId]);
return ( return (
<Container> <div className="w-full max-w-6xl mx-auto">
<Content> <div className="grid grid-cols-1 md:grid-cols-[300px_1fr] gap-8 mb-8">
<PosterContainer> <div className="relative w-full h-[450px] rounded-lg overflow-hidden">
{show.poster_path && ( {show.poster_path && (
<Image <Image
src={getImageUrl(show.poster_path, 'w500')} src={getImageUrl(show.poster_path, 'w500')}
alt={show.name} alt={show.name}
width={300} fill
height={450} className="object-cover"
priority priority
unoptimized
/> />
)} )}
</PosterContainer> </div>
<Info> <div className="text-white">
<Title>{show.name}</Title> <h1 className="text-4xl font-bold mb-4">{show.name}</h1>
<Overview>{show.overview}</Overview> <p className="text-gray-300 leading-relaxed mb-4">{show.overview}</p>
<Details> <div className="flex-1">
<DetailItem> <div className="mb-2 text-gray-300">
<Label>Дата выхода:</Label> <span className="text-gray-400 mr-2">Дата выхода:</span>
<Value> <span className="text-white">
{show.first_air_date ? {show.first_air_date ? formatDate(show.first_air_date) : 'Неизвестно'}
(parseRussianDate(show.first_air_date)?.toLocaleDateString('ru-RU') || 'Неизвестно') </span>
: 'Неизвестно' </div>
} <div className="mb-2 text-gray-300">
</Value> <span className="text-gray-400 mr-2">Сезонов:</span>
</DetailItem> <span className="text-white">{show.number_of_seasons || 'Неизвестно'}</span>
<DetailItem> </div>
<Label>Сезонов:</Label> <div className="mb-2 text-gray-300">
<Value>{show.number_of_seasons || 'Неизвестно'}</Value> <span className="text-gray-400 mr-2">Эпизодов:</span>
</DetailItem> <span className="text-white">{show.number_of_episodes || 'Неизвестно'}</span>
<DetailItem> </div>
<Label>Эпизодов:</Label> <div className="mb-2 text-gray-300">
<Value>{show.number_of_episodes || 'Неизвестно'}</Value> <span className="text-gray-400 mr-2">Рейтинг:</span>
</DetailItem> <span className="text-white">{show.vote_average.toFixed(1)}</span>
<DetailItem> </div>
<Label>Рейтинг:</Label> </div>
<Value>{show.vote_average.toFixed(1)}</Value>
</DetailItem>
</Details>
<ButtonContainer> <div className="mt-4">
<FavoriteButton <FavoriteButton
mediaId={tvShowId} mediaId={tvShowId}
mediaType="tv" mediaType="tv"
title={show.name} title={show.name}
posterPath={show.poster_path || ''} posterPath={show.poster_path || ''}
showText={true}
/> />
</ButtonContainer> </div>
</Info> </div>
</Content> </div>
{imdbId && ( {imdbId && (
<PlayerContainer> <div className="relative w-full aspect-video bg-black/30 rounded-lg overflow-hidden">
<MoviePlayer <MoviePlayer
id={tvShowId}
imdbId={imdbId} imdbId={imdbId}
backdrop={show.backdrop_path ? getImageUrl(show.backdrop_path, 'original') : undefined} title={show.name}
poster={show.poster_path || ''}
isFullscreen={false}
/> />
</PlayerContainer> </div>
)} )}
</Container> </div>
); );
} }

View File

@@ -1,40 +1,30 @@
'use client'; 'use client';
import styled from 'styled-components';
import PageLayout from '@/components/PageLayout'; import PageLayout from '@/components/PageLayout';
import TVShowContent from './TVShowContent'; import TVShowContent from './TVShowContent';
import type { TVShow } from '@/types/movie'; import type { TVShow } from '@/types/movie';
const Container = styled.div`
width: 100%;
min-height: 100vh;
padding: 0 24px;
`;
interface TVShowPageProps { interface TVShowPageProps {
tvShowId: string; showId: string;
show: TVShow | null; show: TVShow | null;
} }
export default function TVShowPage({ tvShowId, show }: TVShowPageProps) { export default function TVShowPage({ showId, show }: TVShowPageProps) {
if (!show) { if (!show) {
return ( return (
<PageLayout> <PageLayout>
<Container> <div className="w-full min-h-screen">
<div className="flex flex-col items-center justify-center min-h-screen p-4 text-center"> <div>Сериал не найден</div>
<h1 className="text-3xl font-bold mb-4">Сериал не найден</h1>
<p className="text-gray-400">К сожалению, запрашиваемый сериал не существует или был удален.</p>
</div> </div>
</Container>
</PageLayout> </PageLayout>
); );
} }
return ( return (
<PageLayout> <PageLayout>
<Container> <div className="w-full">
<TVShowContent tvShowId={tvShowId} initialShow={show} /> <TVShowContent tvShowId={showId} initialShow={show} />
</Container> </div>
</PageLayout> </PageLayout>
); );
} }

View File

@@ -40,14 +40,10 @@ export default function VerificationClient() {
setIsLoading(true); setIsLoading(true);
try { try {
const password = localStorage.getItem('password'); if (!email) {
if (!password || !email) { throw new Error('Не удалось получить email для подтверждения');
throw new Error('Не удалось получить данные для входа');
} }
await verifyCode(code); await verifyCode(code);
await login(email, password);
localStorage.removeItem('password');
router.replace('/'); router.replace('/');
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Произошла ошибка'); setError(err instanceof Error ? err.message : 'Произошла ошибка');

View File

@@ -10,9 +10,10 @@ interface FavoriteButtonProps {
title: string; title: string;
posterPath: string | null; posterPath: string | null;
className?: string; className?: string;
showText?: boolean;
} }
export default function FavoriteButton({ mediaId, mediaType, title, posterPath, className }: FavoriteButtonProps) { export default function FavoriteButton({ mediaId, mediaType, title, posterPath, className, showText = false }: FavoriteButtonProps) {
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
const [isFavorite, setIsFavorite] = useState(false); const [isFavorite, setIsFavorite] = useState(false);
@@ -23,7 +24,7 @@ export default function FavoriteButton({ mediaId, mediaType, title, posterPath,
if (!token) return; if (!token) return;
try { try {
const { data } = await favoritesAPI.checkFavorite(mediaIdString); const { data } = await favoritesAPI.checkFavorite(mediaIdString);
setIsFavorite(!!data.isFavorite); setIsFavorite(!!data.exists);
} catch (error) { } catch (error) {
console.error('Error checking favorite status:', error); console.error('Error checking favorite status:', error);
} }
@@ -59,10 +60,12 @@ export default function FavoriteButton({ mediaId, mediaType, title, posterPath,
}; };
const buttonClasses = cn( const buttonClasses = cn(
'flex items-center gap-2 rounded-md px-4 py-3 text-base font-semibold shadow-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2', 'flex items-center justify-center font-semibold shadow-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
{ {
'rounded-full p-3 text-base': !showText,
'gap-2 rounded-md px-4 py-3 text-base': showText,
'bg-red-100 text-red-700 hover:bg-red-200 focus-visible:outline-red-600': isFavorite, 'bg-red-100 text-red-700 hover:bg-red-200 focus-visible:outline-red-600': isFavorite,
'bg-warm-200 text-warm-800 hover:bg-warm-300 focus-visible:outline-warm-400': !isFavorite, 'bg-warm-200/80 text-warm-800 hover:bg-warm-300/90 backdrop-blur-sm': !isFavorite,
}, },
className className
); );
@@ -70,7 +73,7 @@ export default function FavoriteButton({ mediaId, mediaType, title, posterPath,
return ( return (
<button type="button" onClick={toggleFavorite} className={buttonClasses}> <button type="button" onClick={toggleFavorite} className={buttonClasses}>
<Heart size={20} className={cn({ 'fill-current': isFavorite })} /> <Heart size={20} className={cn({ 'fill-current': isFavorite })} />
<span>{isFavorite ? 'В избранном' : 'В избранное'}</span> {showText && <span>{isFavorite ? 'В избранном' : 'В избранное'}</span>}
</button> </button>
); );
} }

View File

@@ -85,16 +85,14 @@ export default function HeaderBar({ onBurgerClick }: { onBurgerClick?: () => voi
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<ThemeToggleButton /> <ThemeToggleButton />
{userName ? (
<div className="flex items-center space-x-2">
<Link href="/settings" className="p-2 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"> <Link href="/settings" className="p-2 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
<Settings size={20} className="text-gray-800 dark:text-gray-300 hover:text-accent-orange" /> <Settings size={20} className="text-gray-800 dark:text-gray-300 hover:text-accent-orange" />
</Link> </Link>
{userName ? (
<Link href="/profile" className="flex items-center space-x-2 p-2 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"> <Link href="/profile" className="flex items-center space-x-2 p-2 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
<User size={20} className="text-gray-800 dark:text-gray-300" /> <User size={20} className="text-gray-800 dark:text-gray-300" />
<span className="text-sm font-medium hidden sm:block text-gray-800 dark:text-white">{userName}</span> <span className="text-sm font-medium hidden sm:block text-gray-800 dark:text-white">{userName}</span>
</Link> </Link>
</div>
) : ( ) : (
<Link href="/login" className="text-sm font-medium p-2 rounded-md bg-red-600 hover:bg-red-700 text-white transition-colors"> <Link href="/login" className="text-sm font-medium p-2 rounded-md bg-red-600 hover:bg-red-700 text-white transition-colors">
Вход Вход

View File

@@ -1,108 +1,50 @@
"use client"; "use client";
import { useState, useEffect } from 'react';
import { useSettings } from '@/hooks/useSettings'; import { useSettings } from '@/hooks/useSettings';
import styled from 'styled-components';
import { useRouter } from 'next/navigation';
const Container = styled.div`
width: 100%;
max-width: 800px;
padding: 0 1rem;
`;
const Title = styled.h1`
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 2rem;
color: white;
`;
const PlayersList = styled.div`
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
`;
const PlayerCard = styled.div<{ $isSelected: boolean }>`
background: rgba(255, 255, 255, 0.1);
border-radius: 0.5rem;
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
border: 2px solid ${props => props.$isSelected ? '#2196f3' : 'transparent'};
&:hover {
background: rgba(255, 255, 255, 0.15);
}
`;
const PlayerName = styled.h2`
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: white;
`;
const PlayerDescription = styled.p`
color: rgba(255, 255, 255, 0.7);
font-size: 0.875rem;
`;
const SaveButton = styled.button`
margin-top: 1rem;
padding: 0.75rem 1.5rem;
background: #2196f3;
color: white;
border: none;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
&:hover {
background: #1976d2;
}
`;
export default function SettingsContent() { export default function SettingsContent() {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const router = useRouter();
const players = [ const players = [
{ {
id: 'alloha', id: 'alloha',
name: 'Alloha', name: 'Alloha',
description: 'Основной плеер с высоким качеством', description: 'Основной плеер с высоким качеством и быстрой загрузкой.',
}, },
{ {
id: 'lumex', id: 'lumex',
name: 'Lumex', name: 'Lumex',
description: 'Плеер с возможностью скачивания фильмов', description: 'Альтернативный плеер, может быть полезен при проблемах с основным.',
}, },
]; ];
const handlePlayerSelect = (playerId: string) => { const handlePlayerSelect = (playerId: string) => {
updateSettings({ defaultPlayer: playerId as 'alloha' | 'lumex' }); updateSettings({ defaultPlayer: playerId as 'alloha' | 'lumex' });
// Возвращаемся на предыдущую страницу
window.history.back();
}; };
return ( return (
<Container> <div className="w-full max-w-2xl">
<Title>Настройки плеера</Title> <div className="bg-warm-50 dark:bg-warm-900 rounded-lg shadow-lg p-6 sm:p-8">
<PlayersList> <h2 className="text-xl font-bold text-foreground mb-4">Настройки плеера</h2>
<p className="text-muted-foreground mb-6">Выберите плеер, который будет использоваться по умолчанию для просмотра.</p>
<div className="space-y-4">
{players.map((player) => ( {players.map((player) => (
<PlayerCard <div
key={player.id} key={player.id}
$isSelected={settings.defaultPlayer === player.id}
onClick={() => handlePlayerSelect(player.id)} onClick={() => handlePlayerSelect(player.id)}
className={`rounded-lg p-4 cursor-pointer border-2 transition-all ${
settings.defaultPlayer === player.id
? 'border-accent bg-accent/10'
: 'border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 hover:border-accent/50'
}`}
> >
<PlayerName>{player.name}</PlayerName> <h3 className="font-semibold text-foreground">{player.name}</h3>
<PlayerDescription>{player.description}</PlayerDescription> <p className="text-sm text-muted-foreground">{player.description}</p>
</PlayerCard> </div>
))} ))}
</PlayersList> </div>
</Container> </div>
</div>
); );
} }

View File

@@ -21,7 +21,6 @@ export function useAuth() {
localStorage.setItem('token', data.token); localStorage.setItem('token', data.token);
// Extract name/email either from API response or JWT payload // Extract name/email either from API response or JWT payload
// Пытаемся достать имя/почту из JWT либо из ответа
let name: string | undefined = undefined; let name: string | undefined = undefined;
let email: string | undefined = undefined; let email: string | undefined = undefined;
try { try {
@@ -31,14 +30,12 @@ export function useAuth() {
} catch { } catch {
// silent // silent
} }
// fallback к полям ответа
if (!name) name = data.user?.name || data.name || data.userName; if (!name) name = data.user?.name || data.name || data.userName;
if (!email) email = data.user?.email || data.email; if (!email) email = data.user?.email || data.email;
if (name) localStorage.setItem('userName', name); if (name) localStorage.setItem('userName', name);
if (email) localStorage.setItem('userEmail', email); if (email) localStorage.setItem('userEmail', email);
// уведомляем другие компоненты о смене авторизации
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.dispatchEvent(new Event('auth-changed')); window.dispatchEvent(new Event('auth-changed'));
} }
@@ -52,16 +49,34 @@ export function useAuth() {
const register = async (email: string, password: string, name: string) => { const register = async (email: string, password: string, name: string) => {
await authAPI.register({ email, password, name }); await authAPI.register({ email, password, name });
await authAPI.resendCode(email); const pendingData = { email, password, name };
if (typeof window !== 'undefined') {
localStorage.setItem('pendingVerification', JSON.stringify(pendingData));
}
setIsVerifying(true); setIsVerifying(true);
setPending({ email, password, name }); setPending(pendingData);
}; };
const verifyCode = async (code: string) => { const verifyCode = async (code: string) => {
if (!pending) throw new Error('no pending'); let pendingData = pending;
await authAPI.verify(pending.email, code); if (!pendingData && typeof window !== 'undefined') {
// auto login const storedPending = localStorage.getItem('pendingVerification');
await login(pending.email, pending.password); if (storedPending) {
pendingData = JSON.parse(storedPending);
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');
}
setIsVerifying(false); setIsVerifying(false);
setPending(null); setPending(null);
}; };

View File

@@ -53,11 +53,6 @@ export function useMovies({ initialPage = 1, category = 'popular' }: UseMoviesPr
fetchMovies(page, category); fetchMovies(page, category);
}, [page, category, fetchMovies]); }, [page, category, fetchMovies]);
// Сбрасываем страницу на 1 при смене категории
useEffect(() => {
setPage(1);
}, [category]);
const handlePageChange = useCallback((newPage: number) => { const handlePageChange = useCallback((newPage: number) => {
if (newPage < 1 || newPage > totalPages) return; if (newPage < 1 || newPage > totalPages) return;
setPage(newPage); setPage(newPage);

View File

@@ -8,7 +8,7 @@ export const authAPI = {
return api.post('/auth/resend-code', { email }); return api.post('/auth/resend-code', { email });
}, },
verify(email: string, code: string) { verify(email: string, code: string) {
return api.put('/auth/verify', { email, code }); return api.post('/auth/verify', { email, code });
}, },
login(email: string, password: string) { login(email: string, password: string) {
return api.post('/auth/login', { email, password }); return api.post('/auth/login', { email, password });

View File

@@ -9,7 +9,8 @@ export const favoritesAPI = {
// Добавить в избранное // Добавить в избранное
addFavorite(data: { mediaId: string; mediaType: 'movie' | 'tv', title: string, posterPath: string }) { addFavorite(data: { mediaId: string; mediaType: 'movie' | 'tv', title: string, posterPath: string }) {
return api.post(`/favorites`, data); const { mediaId, mediaType, title, posterPath } = data;
return api.post(`/favorites/${mediaId}?mediaType=${mediaType}`, { title, posterPath });
}, },
// Удалить из избранного // Удалить из избранного