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);
} else {
await register(email, password, name);
localStorage.setItem('password', password);
router.push(`/verify?email=${encodeURIComponent(email)}`);
}
} catch (err) {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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 : 'Произошла ошибка');