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 { NeonButton, Input, GlassCard, StatsCard, clearAvatarCache } from '@/components/ui' import { User, Camera, Trophy, Target, CheckCircle, Flame, Loader2, MessageCircle, Link2, Link2Off, ExternalLink, Eye, EyeOff, Save, KeyRound, Shield } from 'lucide-react' // Schemas 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, avatarVersion, bumpAvatarVersion } = useAuthStore() const toast = useToast() // State 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) const [avatarBlobUrl, setAvatarBlobUrl] = useState(null) const [isLoadingAvatar, setIsLoadingAvatar] = useState(true) // Telegram state const [telegramLoading, setTelegramLoading] = useState(false) const [isPolling, setIsPolling] = useState(false) const pollingRef = useRef | null>(null) const fileInputRef = useRef(null) // Forms const nicknameForm = useForm({ resolver: zodResolver(nicknameSchema), defaultValues: { nickname: user?.nickname || '' }, }) const passwordForm = useForm({ resolver: zodResolver(passwordSchema), defaultValues: { current_password: '', new_password: '', confirm_password: '' }, }) // Load stats useEffect(() => { loadStats() return () => { if (pollingRef.current) clearInterval(pollingRef.current) } }, []) // Ref для отслеживания текущего blob URL const avatarBlobRef = useRef(null) // Load avatar via API useEffect(() => { if (!user?.id || !user?.avatar_url) { setIsLoadingAvatar(false) return } let cancelled = false const bustCache = avatarVersion > 0 setIsLoadingAvatar(true) usersApi.getAvatarUrl(user.id, bustCache) .then(url => { if (cancelled) { URL.revokeObjectURL(url) return } // Очищаем старый blob URL if (avatarBlobRef.current) { URL.revokeObjectURL(avatarBlobRef.current) } avatarBlobRef.current = url setAvatarBlobUrl(url) }) .catch(() => { if (!cancelled) { setAvatarBlobUrl(null) } }) .finally(() => { if (!cancelled) { setIsLoadingAvatar(false) } }) return () => { cancelled = true } }, [user?.id, user?.avatar_url, avatarVersion]) // Cleanup blob URL on unmount useEffect(() => { return () => { if (avatarBlobRef.current) { URL.revokeObjectURL(avatarBlobRef.current) } } }, []) // Update nickname form when user changes 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) } } // Update nickname const onNicknameSubmit = async (data: NicknameForm) => { try { const updatedUser = await usersApi.updateNickname(data) updateUser({ nickname: updatedUser.nickname }) toast.success('Никнейм обновлен') } catch { toast.error('Не удалось обновить никнейм') } } // Upload avatar 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 }) if (user?.id) { clearAvatarCache(user.id) } // Bump version - это вызовет перезагрузку через useEffect bumpAvatarVersion() toast.success('Аватар обновлен') } catch { toast.error('Не удалось загрузить аватар') } finally { setIsUploadingAvatar(false) } } // Change password 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 functions 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 = avatarBlobUrl || user?.telegram_avatar_url return (
{/* Header */}

Мой профиль

Настройки вашего аккаунта

{/* Profile Card */}
{/* Avatar */}
{isLoadingAvatar ? (
) : ( )}
{/* Nickname Form */}
} > Сохранить
{/* Stats */}

Статистика

{isLoadingStats ? (
{[...Array(4)].map((_, i) => (
))}
) : stats ? (
} color="neon" /> } color="purple" /> } color="neon" /> } color="pink" />
) : (

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

)}
{/* Telegram */}

Telegram

{isLinked ? 'Аккаунт привязан' : 'Привяжите для уведомлений'}

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

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

{user?.telegram_username && (

@{user.telegram_username}

)}
} > Отвязать
) : (

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

{isPolling ? (

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

) : ( } > Привязать Telegram )}
)}
{/* Security */}

Безопасность

Управление паролем

{!showPasswordForm ? ( setShowPasswordForm(true)} icon={} > Сменить пароль ) : (
} > Сменить пароль { setShowPasswordForm(false) passwordForm.reset() }} > Отмена
)}
) }