mirror of
https://gitlab.com/foxixus/neomovies.git
synced 2025-10-28 01:48: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 : 'Произошла ошибка');
|
||||
|
||||
Reference in New Issue
Block a user