From bf3e231f671a938abcf943425c035e42eadac62a Mon Sep 17 00:00:00 2001 From: Foxix Date: Tue, 8 Jul 2025 13:41:04 +0300 Subject: [PATCH] Release 2.3 --- src/app/login/LoginClient.tsx | 1 - src/app/movie/[id]/MovieContent.tsx | 1 + src/app/profile/page.tsx | 60 +++++---- src/app/settings/page.tsx | 5 +- src/app/tv/[id]/TVContent.tsx | 1 + src/app/tv/[id]/TVShowContent.tsx | 176 +++++++------------------- src/app/tv/[id]/TVShowPage.tsx | 26 ++-- src/app/verify/VerificationClient.tsx | 8 +- src/components/FavoriteButton.tsx | 13 +- src/components/HeaderBar.tsx | 16 +-- src/components/SettingsContent.tsx | 110 ++++------------ src/hooks/useAuth.ts | 33 +++-- src/hooks/useMovies.ts | 5 - src/lib/authApi.ts | 2 +- src/lib/favoritesApi.ts | 3 +- 15 files changed, 160 insertions(+), 300 deletions(-) diff --git a/src/app/login/LoginClient.tsx b/src/app/login/LoginClient.tsx index eba3431..636ebbd 100644 --- a/src/app/login/LoginClient.tsx +++ b/src/app/login/LoginClient.tsx @@ -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) { diff --git a/src/app/movie/[id]/MovieContent.tsx b/src/app/movie/[id]/MovieContent.tsx index cfc0c67..5f314db 100644 --- a/src/app/movie/[id]/MovieContent.tsx +++ b/src/app/movie/[id]/MovieContent.tsx @@ -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} /> diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 6a770bb..855d81d 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -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 ( -
- +
+
); } - if (!userName) { - // This can happen briefly before redirect, or if localStorage is cleared. - return null; - } - return ( -
-
-
-
-
- {userName?.split(' ').map(n => n[0]).join('').toUpperCase() || ''} -
-

{userName}

-

{userEmail}

- -
+ Выйти из аккаунта + +
+ +
+

Опасная зона

+

Это действие нельзя будет отменить. Все ваши данные, включая избранное, будут удалены.

+
diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 96a1bc5..11fe968 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -1,12 +1,11 @@ 'use client'; import SettingsContent from '@/components/SettingsContent'; -import PageLayout from '@/components/PageLayout'; export default function SettingsPage() { return ( - +
- +
); } \ No newline at end of file diff --git a/src/app/tv/[id]/TVContent.tsx b/src/app/tv/[id]/TVContent.tsx index 5d89a83..949e4c6 100644 --- a/src/app/tv/[id]/TVContent.tsx +++ b/src/app/tv/[id]/TVContent.tsx @@ -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} />
diff --git a/src/app/tv/[id]/TVShowContent.tsx b/src/app/tv/[id]/TVShowContent.tsx index 83223b3..1af5d26 100644 --- a/src/app/tv/[id]/TVShowContent.tsx +++ b/src/app/tv/[id]/TVShowContent.tsx @@ -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 ( - - - +
+
+
{show.poster_path && ( {show.name} )} - +
- - {show.name} - {show.overview} +
+

{show.name}

+

{show.overview}

-
- - - - {show.first_air_date ? - (parseRussianDate(show.first_air_date)?.toLocaleDateString('ru-RU') || 'Неизвестно') - : 'Неизвестно' - } - - - - - {show.number_of_seasons || 'Неизвестно'} - - - - {show.number_of_episodes || 'Неизвестно'} - - - - {show.vote_average.toFixed(1)} - -
+
+
+ Дата выхода: + + {show.first_air_date ? formatDate(show.first_air_date) : 'Неизвестно'} + +
+
+ Сезонов: + {show.number_of_seasons || 'Неизвестно'} +
+
+ Эпизодов: + {show.number_of_episodes || 'Неизвестно'} +
+
+ Рейтинг: + {show.vote_average.toFixed(1)} +
+
- +
- - - +
+
+
{imdbId && ( - +
- +
)} - +
); } diff --git a/src/app/tv/[id]/TVShowPage.tsx b/src/app/tv/[id]/TVShowPage.tsx index 3d27a97..172e1e5 100644 --- a/src/app/tv/[id]/TVShowPage.tsx +++ b/src/app/tv/[id]/TVShowPage.tsx @@ -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 ( - -
-

Сериал не найден

-

К сожалению, запрашиваемый сериал не существует или был удален.

-
-
+
+
Сериал не найден
+
); } return ( - - - +
+ +
); } \ No newline at end of file diff --git a/src/app/verify/VerificationClient.tsx b/src/app/verify/VerificationClient.tsx index 39afaf1..af626b3 100644 --- a/src/app/verify/VerificationClient.tsx +++ b/src/app/verify/VerificationClient.tsx @@ -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 : 'Произошла ошибка'); diff --git a/src/components/FavoriteButton.tsx b/src/components/FavoriteButton.tsx index d771c9f..b4596d8 100644 --- a/src/components/FavoriteButton.tsx +++ b/src/components/FavoriteButton.tsx @@ -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 ( ); } diff --git a/src/components/HeaderBar.tsx b/src/components/HeaderBar.tsx index 4398077..1c5edfa 100644 --- a/src/components/HeaderBar.tsx +++ b/src/components/HeaderBar.tsx @@ -85,16 +85,14 @@ export default function HeaderBar({ onBurgerClick }: { onBurgerClick?: () => voi
+ + + {userName ? ( -
- - - - - - {userName} - -
+ + + {userName} + ) : ( Вход diff --git a/src/components/SettingsContent.tsx b/src/components/SettingsContent.tsx index ba47d17..93deaba 100644 --- a/src/components/SettingsContent.tsx +++ b/src/components/SettingsContent.tsx @@ -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 ( - - Настройки плеера - - {players.map((player) => ( - handlePlayerSelect(player.id)} - > - {player.name} - {player.description} - - ))} - - +
+
+

Настройки плеера

+

Выберите плеер, который будет использоваться по умолчанию для просмотра.

+
+ {players.map((player) => ( +
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' + }`} + > +

{player.name}

+

{player.description}

+
+ ))} +
+
+
); } diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 3ab6109..dccf6fe 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -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); }; diff --git a/src/hooks/useMovies.ts b/src/hooks/useMovies.ts index a969335..e242cb2 100644 --- a/src/hooks/useMovies.ts +++ b/src/hooks/useMovies.ts @@ -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); diff --git a/src/lib/authApi.ts b/src/lib/authApi.ts index a8ec941..92175e0 100644 --- a/src/lib/authApi.ts +++ b/src/lib/authApi.ts @@ -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 }); diff --git a/src/lib/favoritesApi.ts b/src/lib/favoritesApi.ts index 369cecc..791ed5d 100644 --- a/src/lib/favoritesApi.ts +++ b/src/lib/favoritesApi.ts @@ -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 }); }, // Удалить из избранного