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);
|
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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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,41 +23,51 @@ 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-foreground">{userName}</h1>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">{userName}</h1>
|
<p className="mt-2 text-base text-muted-foreground">{userEmail}</p>
|
||||||
<p className="mt-2 text-base text-gray-500 dark:text-gray-300">{userEmail}</p>
|
</div>
|
||||||
<button
|
|
||||||
onClick={handleSignOut}
|
<div className="bg-warm-50 dark:bg-warm-900 rounded-lg shadow-lg p-6 sm:p-8 mb-6">
|
||||||
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"
|
<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} />
|
<LogOut size={20} />
|
||||||
<span>Выйти</span>
|
<span>Выйти из аккаунта</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
</div>
|
||||||
<p className="text-gray-400">К сожалению, запрашиваемый сериал не существует или был удален.</p>
|
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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 : 'Произошла ошибка');
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
<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 ? (
|
{userName ? (
|
||||||
<div className="flex items-center space-x-2">
|
<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="/settings" className="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" />
|
||||||
<Settings size={20} className="text-gray-800 dark:text-gray-300 hover:text-accent-orange" />
|
<span className="text-sm font-medium hidden sm:block text-gray-800 dark:text-white">{userName}</span>
|
||||||
</Link>
|
</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="/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">
|
||||||
Вход
|
Вход
|
||||||
|
|||||||
@@ -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>
|
||||||
{players.map((player) => (
|
<p className="text-muted-foreground mb-6">Выберите плеер, который будет использоваться по умолчанию для просмотра.</p>
|
||||||
<PlayerCard
|
<div className="space-y-4">
|
||||||
key={player.id}
|
{players.map((player) => (
|
||||||
$isSelected={settings.defaultPlayer === player.id}
|
<div
|
||||||
onClick={() => handlePlayerSelect(player.id)}
|
key={player.id}
|
||||||
>
|
onClick={() => handlePlayerSelect(player.id)}
|
||||||
<PlayerName>{player.name}</PlayerName>
|
className={`rounded-lg p-4 cursor-pointer border-2 transition-all ${
|
||||||
<PlayerDescription>{player.description}</PlayerDescription>
|
settings.defaultPlayer === player.id
|
||||||
</PlayerCard>
|
? 'border-accent bg-accent/10'
|
||||||
))}
|
: 'border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 hover:border-accent/50'
|
||||||
</PlayersList>
|
}`}
|
||||||
</Container>
|
>
|
||||||
|
<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);
|
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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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 });
|
||||||
},
|
},
|
||||||
|
|
||||||
// Удалить из избранного
|
// Удалить из избранного
|
||||||
|
|||||||
Reference in New Issue
Block a user