mirror of
https://gitlab.com/foxixus/neomovies.git
synced 2025-10-27 17:38:50 +05:00
Release 2.3
This commit is contained in:
@@ -31,7 +31,6 @@ export default function LoginClient() {
|
||||
await login(email, password);
|
||||
} else {
|
||||
await register(email, password, name);
|
||||
localStorage.setItem('password', password);
|
||||
router.push(`/verify?email=${encodeURIComponent(email)}`);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -124,6 +124,7 @@ export default function MovieContent({ movieId, initialMovie }: MovieContentProp
|
||||
title={movie.title}
|
||||
posterPath={movie.poster_path}
|
||||
className="!bg-secondary !text-secondary-foreground hover:!bg-secondary/80"
|
||||
showText={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Loader2, LogOut } from 'lucide-react';
|
||||
import { Loader2, User, LogOut, Trash2 } from 'lucide-react';
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { logout } = useAuth();
|
||||
@@ -23,41 +23,51 @@ export default function ProfilePage() {
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
const handleSignOut = () => {
|
||||
logout();
|
||||
const handleDeleteAccount = () => {
|
||||
// TODO: Implement account deletion logic
|
||||
alert('Функция удаления аккаунта в разработке.');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full items-center justify-center bg-[#F9F6EE] dark:bg-gray-900">
|
||||
<Loader2 className="h-16 w-16 animate-spin text-red-500" />
|
||||
<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-accent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!userName) {
|
||||
// This can happen briefly before redirect, or if localStorage is cleared.
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full bg-[#F9F6EE] dark:bg-[#1e1e1e] pt-24 sm:pt-32">
|
||||
<div className="flex justify-center px-4">
|
||||
<div className="w-full max-w-md rounded-2xl bg-white dark:bg-[#49372E] p-8 shadow-lg">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<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() || ''}
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">{userName}</h1>
|
||||
<p className="mt-2 text-base text-gray-500 dark:text-gray-300">{userEmail}</p>
|
||||
<button
|
||||
onClick={handleSignOut}
|
||||
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"
|
||||
<div className="min-h-screen w-full bg-background text-foreground flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="bg-warm-50 dark:bg-warm-900 rounded-lg shadow-lg p-8 text-center mb-6">
|
||||
<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">
|
||||
{userName?.split(' ').map(n => n[0]).join('').toUpperCase() || ''}
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-foreground">{userName}</h1>
|
||||
<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
|
||||
onClick={logout}
|
||||
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} />
|
||||
<span>Выйти</span>
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import SettingsContent from '@/components/SettingsContent';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="min-h-screen flex items-center justify-center p-4 bg-background text-foreground">
|
||||
<SettingsContent />
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -128,6 +128,7 @@ export default function TVContent({ showId, initialShow }: TVContentProps) {
|
||||
title={show.name}
|
||||
posterPath={show.poster_path}
|
||||
className="!bg-secondary !text-secondary-foreground hover:!bg-secondary/80"
|
||||
showText={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,102 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Image from 'next/image';
|
||||
import type { TVShow } from '@/types/movie';
|
||||
import { tvShowsAPI, getImageUrl } from '@/lib/neoApi';
|
||||
import MoviePlayer from '@/components/MoviePlayer';
|
||||
import FavoriteButton from '@/components/FavoriteButton';
|
||||
|
||||
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));
|
||||
};
|
||||
import { formatDate } from '@/lib/utils';
|
||||
|
||||
interface TVShowContentProps {
|
||||
tvShowId: string;
|
||||
@@ -122,67 +30,69 @@ export default function TVShowContent({ tvShowId, initialShow }: TVShowContentPr
|
||||
}, [tvShowId]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Content>
|
||||
<PosterContainer>
|
||||
<div className="w-full max-w-6xl mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-[300px_1fr] gap-8 mb-8">
|
||||
<div className="relative w-full h-[450px] rounded-lg overflow-hidden">
|
||||
{show.poster_path && (
|
||||
<Image
|
||||
src={getImageUrl(show.poster_path, 'w500')}
|
||||
alt={show.name}
|
||||
width={300}
|
||||
height={450}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
unoptimized
|
||||
/>
|
||||
)}
|
||||
</PosterContainer>
|
||||
</div>
|
||||
|
||||
<Info>
|
||||
<Title>{show.name}</Title>
|
||||
<Overview>{show.overview}</Overview>
|
||||
<div className="text-white">
|
||||
<h1 className="text-4xl font-bold mb-4">{show.name}</h1>
|
||||
<p className="text-gray-300 leading-relaxed mb-4">{show.overview}</p>
|
||||
|
||||
<Details>
|
||||
<DetailItem>
|
||||
<Label>Дата выхода:</Label>
|
||||
<Value>
|
||||
{show.first_air_date ?
|
||||
(parseRussianDate(show.first_air_date)?.toLocaleDateString('ru-RU') || 'Неизвестно')
|
||||
: 'Неизвестно'
|
||||
}
|
||||
</Value>
|
||||
</DetailItem>
|
||||
<DetailItem>
|
||||
<Label>Сезонов:</Label>
|
||||
<Value>{show.number_of_seasons || 'Неизвестно'}</Value>
|
||||
</DetailItem>
|
||||
<DetailItem>
|
||||
<Label>Эпизодов:</Label>
|
||||
<Value>{show.number_of_episodes || 'Неизвестно'}</Value>
|
||||
</DetailItem>
|
||||
<DetailItem>
|
||||
<Label>Рейтинг:</Label>
|
||||
<Value>{show.vote_average.toFixed(1)}</Value>
|
||||
</DetailItem>
|
||||
</Details>
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 text-gray-300">
|
||||
<span className="text-gray-400 mr-2">Дата выхода:</span>
|
||||
<span className="text-white">
|
||||
{show.first_air_date ? formatDate(show.first_air_date) : 'Неизвестно'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-2 text-gray-300">
|
||||
<span className="text-gray-400 mr-2">Сезонов:</span>
|
||||
<span className="text-white">{show.number_of_seasons || 'Неизвестно'}</span>
|
||||
</div>
|
||||
<div className="mb-2 text-gray-300">
|
||||
<span className="text-gray-400 mr-2">Эпизодов:</span>
|
||||
<span className="text-white">{show.number_of_episodes || 'Неизвестно'}</span>
|
||||
</div>
|
||||
<div className="mb-2 text-gray-300">
|
||||
<span className="text-gray-400 mr-2">Рейтинг:</span>
|
||||
<span className="text-white">{show.vote_average.toFixed(1)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ButtonContainer>
|
||||
<div className="mt-4">
|
||||
<FavoriteButton
|
||||
mediaId={tvShowId}
|
||||
mediaType="tv"
|
||||
title={show.name}
|
||||
posterPath={show.poster_path || ''}
|
||||
showText={true}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
</Info>
|
||||
</Content>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{imdbId && (
|
||||
<PlayerContainer>
|
||||
<div className="relative w-full aspect-video bg-black/30 rounded-lg overflow-hidden">
|
||||
<MoviePlayer
|
||||
id={tvShowId}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
import TVShowContent from './TVShowContent';
|
||||
import type { TVShow } from '@/types/movie';
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
padding: 0 24px;
|
||||
`;
|
||||
|
||||
interface TVShowPageProps {
|
||||
tvShowId: string;
|
||||
showId: string;
|
||||
show: TVShow | null;
|
||||
}
|
||||
|
||||
export default function TVShowPage({ tvShowId, show }: TVShowPageProps) {
|
||||
export default function TVShowPage({ showId, show }: TVShowPageProps) {
|
||||
if (!show) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<Container>
|
||||
<div className="flex flex-col items-center justify-center min-h-screen p-4 text-center">
|
||||
<h1 className="text-3xl font-bold mb-4">Сериал не найден</h1>
|
||||
<p className="text-gray-400">К сожалению, запрашиваемый сериал не существует или был удален.</p>
|
||||
</div>
|
||||
</Container>
|
||||
<div className="w-full min-h-screen">
|
||||
<div>Сериал не найден</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<Container>
|
||||
<TVShowContent tvShowId={tvShowId} initialShow={show} />
|
||||
</Container>
|
||||
<div className="w-full">
|
||||
<TVShowContent tvShowId={showId} initialShow={show} />
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -40,14 +40,10 @@ export default function VerificationClient() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const password = localStorage.getItem('password');
|
||||
if (!password || !email) {
|
||||
throw new Error('Не удалось получить данные для входа');
|
||||
if (!email) {
|
||||
throw new Error('Не удалось получить email для подтверждения');
|
||||
}
|
||||
|
||||
await verifyCode(code);
|
||||
await login(email, password);
|
||||
localStorage.removeItem('password');
|
||||
router.replace('/');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Произошла ошибка');
|
||||
|
||||
@@ -10,9 +10,10 @@ interface FavoriteButtonProps {
|
||||
title: string;
|
||||
posterPath: string | null;
|
||||
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 [isFavorite, setIsFavorite] = useState(false);
|
||||
|
||||
@@ -23,7 +24,7 @@ export default function FavoriteButton({ mediaId, mediaType, title, posterPath,
|
||||
if (!token) return;
|
||||
try {
|
||||
const { data } = await favoritesAPI.checkFavorite(mediaIdString);
|
||||
setIsFavorite(!!data.isFavorite);
|
||||
setIsFavorite(!!data.exists);
|
||||
} catch (error) {
|
||||
console.error('Error checking favorite status:', error);
|
||||
}
|
||||
@@ -59,10 +60,12 @@ export default function FavoriteButton({ mediaId, mediaType, title, posterPath,
|
||||
};
|
||||
|
||||
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-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
|
||||
);
|
||||
@@ -70,7 +73,7 @@ export default function FavoriteButton({ mediaId, mediaType, title, posterPath,
|
||||
return (
|
||||
<button type="button" onClick={toggleFavorite} className={buttonClasses}>
|
||||
<Heart size={20} className={cn({ 'fill-current': isFavorite })} />
|
||||
<span>{isFavorite ? 'В избранном' : 'В избранное'}</span>
|
||||
{showText && <span>{isFavorite ? 'В избранном' : 'В избранное'}</span>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,16 +85,14 @@ export default function HeaderBar({ onBurgerClick }: { onBurgerClick?: () => voi
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<ThemeToggleButton />
|
||||
<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" />
|
||||
</Link>
|
||||
{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">
|
||||
<Settings size={20} className="text-gray-800 dark:text-gray-300 hover:text-accent-orange" />
|
||||
</Link>
|
||||
<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" />
|
||||
<span className="text-sm font-medium hidden sm:block text-gray-800 dark:text-white">{userName}</span>
|
||||
</Link>
|
||||
</div>
|
||||
<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" />
|
||||
<span className="text-sm font-medium hidden sm:block text-gray-800 dark:text-white">{userName}</span>
|
||||
</Link>
|
||||
) : (
|
||||
<Link href="/login" className="text-sm font-medium p-2 rounded-md bg-red-600 hover:bg-red-700 text-white transition-colors">
|
||||
Вход
|
||||
|
||||
@@ -1,108 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
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() {
|
||||
const { settings, updateSettings } = useSettings();
|
||||
const router = useRouter();
|
||||
|
||||
const players = [
|
||||
{
|
||||
id: 'alloha',
|
||||
name: 'Alloha',
|
||||
description: 'Основной плеер с высоким качеством',
|
||||
description: 'Основной плеер с высоким качеством и быстрой загрузкой.',
|
||||
},
|
||||
{
|
||||
id: 'lumex',
|
||||
name: 'Lumex',
|
||||
description: 'Плеер с возможностью скачивания фильмов',
|
||||
description: 'Альтернативный плеер, может быть полезен при проблемах с основным.',
|
||||
},
|
||||
];
|
||||
|
||||
const handlePlayerSelect = (playerId: string) => {
|
||||
updateSettings({ defaultPlayer: playerId as 'alloha' | 'lumex' });
|
||||
// Возвращаемся на предыдущую страницу
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Title>Настройки плеера</Title>
|
||||
<PlayersList>
|
||||
{players.map((player) => (
|
||||
<PlayerCard
|
||||
key={player.id}
|
||||
$isSelected={settings.defaultPlayer === player.id}
|
||||
onClick={() => handlePlayerSelect(player.id)}
|
||||
>
|
||||
<PlayerName>{player.name}</PlayerName>
|
||||
<PlayerDescription>{player.description}</PlayerDescription>
|
||||
</PlayerCard>
|
||||
))}
|
||||
</PlayersList>
|
||||
</Container>
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="bg-warm-50 dark:bg-warm-900 rounded-lg shadow-lg p-6 sm:p-8">
|
||||
<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) => (
|
||||
<div
|
||||
key={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'
|
||||
}`}
|
||||
>
|
||||
<h3 className="font-semibold text-foreground">{player.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{player.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ export function useAuth() {
|
||||
localStorage.setItem('token', data.token);
|
||||
|
||||
// Extract name/email either from API response or JWT payload
|
||||
// Пытаемся достать имя/почту из JWT либо из ответа
|
||||
let name: string | undefined = undefined;
|
||||
let email: string | undefined = undefined;
|
||||
try {
|
||||
@@ -31,14 +30,12 @@ export function useAuth() {
|
||||
} catch {
|
||||
// silent
|
||||
}
|
||||
// fallback к полям ответа
|
||||
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'));
|
||||
}
|
||||
@@ -52,16 +49,34 @@ export function useAuth() {
|
||||
|
||||
const register = async (email: string, password: string, name: string) => {
|
||||
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);
|
||||
setPending({ email, password, name });
|
||||
setPending(pendingData);
|
||||
};
|
||||
|
||||
const verifyCode = async (code: string) => {
|
||||
if (!pending) throw new Error('no pending');
|
||||
await authAPI.verify(pending.email, code);
|
||||
// auto login
|
||||
await login(pending.email, pending.password);
|
||||
let pendingData = pending;
|
||||
if (!pendingData && typeof window !== 'undefined') {
|
||||
const storedPending = localStorage.getItem('pendingVerification');
|
||||
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);
|
||||
setPending(null);
|
||||
};
|
||||
|
||||
@@ -53,11 +53,6 @@ export function useMovies({ initialPage = 1, category = 'popular' }: UseMoviesPr
|
||||
fetchMovies(page, category);
|
||||
}, [page, category, fetchMovies]);
|
||||
|
||||
// Сбрасываем страницу на 1 при смене категории
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [category]);
|
||||
|
||||
const handlePageChange = useCallback((newPage: number) => {
|
||||
if (newPage < 1 || newPage > totalPages) return;
|
||||
setPage(newPage);
|
||||
|
||||
@@ -8,7 +8,7 @@ export const authAPI = {
|
||||
return api.post('/auth/resend-code', { email });
|
||||
},
|
||||
verify(email: string, code: string) {
|
||||
return api.put('/auth/verify', { email, code });
|
||||
return api.post('/auth/verify', { email, code });
|
||||
},
|
||||
login(email: string, password: string) {
|
||||
return api.post('/auth/login', { email, password });
|
||||
|
||||
@@ -9,7 +9,8 @@ export const favoritesAPI = {
|
||||
|
||||
// Добавить в избранное
|
||||
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 });
|
||||
},
|
||||
|
||||
// Удалить из избранного
|
||||
|
||||
Reference in New Issue
Block a user