import { useState, useEffect, useRef } from 'react' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' import { useAuthStore } from '@/store/auth' import { usersApi, telegramApi, authApi } from '@/api' import type { UserStats } from '@/types' import { useToast } from '@/store/toast' import { Button, Input, Card, CardHeader, CardTitle, CardContent } from '@/components/ui' import { User, Camera, Trophy, Target, CheckCircle, Flame, Loader2, MessageCircle, Link2, Link2Off, ExternalLink, Eye, EyeOff, Save, KeyRound } from 'lucide-react' // Схемы валидации const nicknameSchema = z.object({ nickname: z.string().min(2, 'Минимум 2 символа').max(50, 'Максимум 50 символов'), }) const passwordSchema = z.object({ current_password: z.string().min(6, 'Минимум 6 символов'), new_password: z.string().min(6, 'Минимум 6 символов').max(100, 'Максимум 100 символов'), confirm_password: z.string(), }).refine((data) => data.new_password === data.confirm_password, { message: 'Пароли не совпадают', path: ['confirm_password'], }) type NicknameForm = z.infer type PasswordForm = z.infer export function ProfilePage() { const { user, updateUser } = useAuthStore() const toast = useToast() // Состояние const [stats, setStats] = useState(null) const [isLoadingStats, setIsLoadingStats] = useState(true) const [isUploadingAvatar, setIsUploadingAvatar] = useState(false) const [showPasswordForm, setShowPasswordForm] = useState(false) const [showCurrentPassword, setShowCurrentPassword] = useState(false) const [showNewPassword, setShowNewPassword] = useState(false) // Telegram state const [telegramLoading, setTelegramLoading] = useState(false) const [isPolling, setIsPolling] = useState(false) const pollingRef = useRef | null>(null) const fileInputRef = useRef(null) // Формы const nicknameForm = useForm({ resolver: zodResolver(nicknameSchema), defaultValues: { nickname: user?.nickname || '' }, }) const passwordForm = useForm({ resolver: zodResolver(passwordSchema), defaultValues: { current_password: '', new_password: '', confirm_password: '' }, }) // Загрузка статистики useEffect(() => { loadStats() return () => { if (pollingRef.current) clearInterval(pollingRef.current) } }, []) // Обновляем форму никнейма при изменении user useEffect(() => { if (user?.nickname) { nicknameForm.reset({ nickname: user.nickname }) } }, [user?.nickname]) const loadStats = async () => { try { const data = await usersApi.getMyStats() setStats(data) } catch (error) { console.error('Failed to load stats:', error) } finally { setIsLoadingStats(false) } } // Обновление никнейма const onNicknameSubmit = async (data: NicknameForm) => { try { const updatedUser = await usersApi.updateNickname(data) updateUser({ nickname: updatedUser.nickname }) toast.success('Никнейм обновлен') } catch { toast.error('Не удалось обновить никнейм') } } // Загрузка аватара const handleAvatarClick = () => { fileInputRef.current?.click() } const handleAvatarChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return // Валидация if (!file.type.startsWith('image/')) { toast.error('Файл должен быть изображением') return } if (file.size > 5 * 1024 * 1024) { toast.error('Максимальный размер файла 5 МБ') return } setIsUploadingAvatar(true) try { const updatedUser = await usersApi.uploadAvatar(file) updateUser({ avatar_url: updatedUser.avatar_url }) toast.success('Аватар обновлен') } catch { toast.error('Не удалось загрузить аватар') } finally { setIsUploadingAvatar(false) } } // Смена пароля const onPasswordSubmit = async (data: PasswordForm) => { try { await usersApi.changePassword({ current_password: data.current_password, new_password: data.new_password, }) toast.success('Пароль успешно изменен') passwordForm.reset() setShowPasswordForm(false) } catch (error: unknown) { const err = error as { response?: { data?: { detail?: string } } } const message = err.response?.data?.detail || 'Не удалось сменить пароль' toast.error(message) } } // Telegram функции const startPolling = () => { setIsPolling(true) let attempts = 0 pollingRef.current = setInterval(async () => { attempts++ try { const userData = await authApi.me() if (userData.telegram_id) { updateUser({ telegram_id: userData.telegram_id, telegram_username: userData.telegram_username, telegram_first_name: userData.telegram_first_name, telegram_last_name: userData.telegram_last_name, telegram_avatar_url: userData.telegram_avatar_url, }) toast.success('Telegram привязан!') setIsPolling(false) if (pollingRef.current) clearInterval(pollingRef.current) } } catch { /* ignore */ } if (attempts >= 60) { setIsPolling(false) if (pollingRef.current) clearInterval(pollingRef.current) } }, 5000) } const handleLinkTelegram = async () => { setTelegramLoading(true) try { const { bot_url } = await telegramApi.generateLinkToken() window.open(bot_url, '_blank') startPolling() } catch { toast.error('Не удалось сгенерировать ссылку') } finally { setTelegramLoading(false) } } const handleUnlinkTelegram = async () => { setTelegramLoading(true) try { await telegramApi.unlinkTelegram() updateUser({ telegram_id: null, telegram_username: null, telegram_first_name: null, telegram_last_name: null, telegram_avatar_url: null, }) toast.success('Telegram отвязан') } catch { toast.error('Не удалось отвязать Telegram') } finally { setTelegramLoading(false) } } const isLinked = !!user?.telegram_id const displayAvatar = user?.telegram_avatar_url || user?.avatar_url return (

Мой профиль

{/* Карточка профиля */}
{/* Аватар */}
{/* Форма никнейма */}
{/* Статистика */} Статистика {isLoadingStats ? (
) : stats ? (
{stats.marathons_count}
Марафонов
{stats.wins_count}
Побед
{stats.completed_assignments}
Заданий
{stats.total_points_earned}
Очков
) : (

Не удалось загрузить статистику

)}
{/* Telegram */} Telegram {isLinked ? (
{user?.telegram_avatar_url ? ( Telegram avatar ) : ( )}

{user?.telegram_first_name} {user?.telegram_last_name}

{user?.telegram_username && (

@{user.telegram_username}

)}
) : (

Привяжи Telegram для получения уведомлений о событиях и марафонах.

{isPolling ? (

Ожидание привязки...

) : ( )}
)}
{/* Смена пароля */} Безопасность {!showPasswordForm ? ( ) : (
)}
) }