nakama/frontend/src/App.jsx

262 lines
9.7 KiB
React
Raw Normal View History

2025-11-04 22:31:04 +00:00
import { BrowserRouter, Routes, Route, Navigate, useNavigate } from 'react-router-dom'
import { useState, useEffect, useRef } from 'react'
2025-11-10 20:13:22 +00:00
import { initTelegramApp } from './utils/telegram'
import { verifyAuth, verifySession } from './utils/api'
2025-11-03 20:35:01 +00:00
import { initTheme } from './utils/theme'
import Layout from './components/Layout'
import Feed from './pages/Feed'
import Search from './pages/Search'
import Notifications from './pages/Notifications'
import Profile from './pages/Profile'
import UserProfile from './pages/UserProfile'
2025-11-03 22:51:17 +00:00
import CommentsPage from './pages/CommentsPage'
import PostMenuPage from './pages/PostMenuPage'
2025-11-03 20:35:01 +00:00
import './styles/index.css'
2025-11-04 22:31:04 +00:00
function AppContent() {
2025-11-03 20:35:01 +00:00
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
2025-11-04 21:51:05 +00:00
const [showLogin, setShowLogin] = useState(false)
2025-11-04 22:31:04 +00:00
const navigate = useNavigate()
const startParamProcessed = useRef(false) // Флаг для обработки startParam только один раз
2025-11-04 22:41:35 +00:00
const initAppCalled = useRef(false) // Флаг чтобы initApp вызывался только один раз
2025-11-03 20:35:01 +00:00
useEffect(() => {
2025-11-04 22:41:35 +00:00
// Инициализировать тему только один раз
2025-11-03 20:35:01 +00:00
initTheme()
2025-11-04 22:41:35 +00:00
// Инициализировать приложение только если еще не было вызвано
if (!initAppCalled.current) {
initAppCalled.current = true
initApp()
}
2025-11-03 20:35:01 +00:00
}, [])
const initApp = async () => {
try {
// Инициализация Telegram Web App
initTelegramApp()
2025-11-04 22:02:23 +00:00
// Проверить наличие Telegram Web App API
const tg = window.Telegram?.WebApp
2025-11-03 20:35:01 +00:00
2025-11-04 22:23:33 +00:00
// Дать время на полную инициализацию Telegram Web App
await new Promise(resolve => setTimeout(resolve, 300))
// Проверить наличие initData (главный индикатор что мы в Telegram)
2025-11-04 22:41:35 +00:00
const initData = tg?.initData || ''
2025-11-04 22:23:33 +00:00
if (initData) {
// Есть initData - пробуем авторизоваться через API
try {
const userData = await verifyAuth()
2025-11-04 22:41:35 +00:00
// Сохранить сессию для будущих загрузок
localStorage.setItem('nakama_user', JSON.stringify(userData))
localStorage.setItem('nakama_auth_type', 'telegram')
// КРИТИЧНО: Сначала установить loading в false, потом user
// Это предотвращает бесконечную загрузку
setLoading(false)
2025-11-04 22:23:33 +00:00
setUser(userData)
2025-11-04 22:31:04 +00:00
// Обработать параметр start из Telegram (только один раз)
if (!startParamProcessed.current && tg?.startParam?.startsWith('post_')) {
startParamProcessed.current = true // Пометить как обработанный
2025-11-04 22:23:33 +00:00
const postId = tg.startParam.replace('post_', '')
2025-11-04 22:31:04 +00:00
// Использовать navigate вместо window.location.href (не вызывает перезагрузку)
2025-11-04 22:41:35 +00:00
// Задержка чтобы компонент успел отрендериться с user
setTimeout(() => {
navigate(`/feed?post=${postId}`, { replace: true })
}, 200)
2025-11-04 22:23:33 +00:00
}
return
} catch (authError) {
console.error('Ошибка авторизации через API:', authError)
2025-11-04 22:41:35 +00:00
// Если авторизация не удалась, проверяем сохраненную сессию
const savedUser = localStorage.getItem('nakama_user')
if (savedUser) {
try {
const userData = JSON.parse(savedUser)
// Проверить что сессия еще актуальна (можно добавить проверку времени)
setUser(userData)
setLoading(false)
return
} catch (e) {
console.error('Ошибка восстановления сессии:', e)
localStorage.removeItem('nakama_user')
localStorage.removeItem('nakama_auth_type')
}
}
// Если нет сохраненной сессии, показываем Login Widget
2025-11-04 22:02:23 +00:00
setShowLogin(true)
setLoading(false)
return
}
}
2025-11-04 21:51:05 +00:00
2025-11-04 22:41:35 +00:00
// Если нет initData, проверяем сохраненную сессию OAuth
const savedUser = localStorage.getItem('nakama_user')
const authType = localStorage.getItem('nakama_auth_type')
if (savedUser && authType === 'oauth') {
try {
const userData = JSON.parse(savedUser)
// Проверить сессию на сервере (обновить данные пользователя)
try {
const freshUserData = await verifySession(userData.telegramId)
// Обновить сохраненную сессию
localStorage.setItem('nakama_user', JSON.stringify(freshUserData))
setUser(freshUserData)
} catch (sessionError) {
console.error('Ошибка проверки сессии:', sessionError)
// Если проверка не удалась, использовать сохраненные данные
setUser(userData)
}
setLoading(false)
return
} catch (e) {
console.error('Ошибка восстановления сессии:', e)
localStorage.removeItem('nakama_user')
localStorage.removeItem('nakama_auth_type')
}
}
// Если нет сохраненной сессии и нет initData, показываем Login Widget
2025-11-04 22:23:33 +00:00
setShowLogin(true)
setLoading(false)
2025-11-03 20:35:01 +00:00
} catch (err) {
console.error('Ошибка инициализации:', err)
setError(err.message)
setLoading(false)
}
}
if (loading) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
flexDirection: 'column',
gap: '16px'
}}>
<div className="spinner" />
<p style={{ color: 'var(--text-secondary)' }}>Загрузка...</p>
</div>
)
}
2025-11-04 21:51:05 +00:00
// Показать Login Widget если нет авторизации
if (showLogin) {
2025-11-10 20:13:22 +00:00
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
padding: '24px',
textAlign: 'center',
flexDirection: 'column',
gap: '16px'
}}>
<h2 style={{ color: 'var(--text-primary)', margin: 0 }}>Используйте официальный клиент Telegram</h2>
<p style={{ color: 'var(--text-secondary)', margin: 0, maxWidth: '360px', lineHeight: 1.5 }}>
Для доступа к NakamaHost откройте бота в официальном приложении Telegram.
Если вы уже используете официальный клиент и видите это сообщение,
пожалуйста сообщите об ошибке в&nbsp;
<a href="https://t.me/NakamaReportbot" style={{ color: 'var(--text-primary)', textDecoration: 'underline' }}>
@NakamaReportbot
</a>.
</p>
<button
onClick={() => window.location.reload()}
style={{
padding: '12px 24px',
borderRadius: '12px',
border: 'none',
background: 'var(--bg-primary)',
color: 'var(--text-primary)',
fontSize: '16px',
cursor: 'pointer'
}}
>
Перезагрузить
</button>
</div>
)
2025-11-04 21:51:05 +00:00
}
2025-11-03 20:35:01 +00:00
if (error) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
flexDirection: 'column',
gap: '16px',
padding: '20px'
}}>
<p style={{ color: 'var(--text-primary)', textAlign: 'center' }}>
Ошибка загрузки приложения
</p>
<p style={{ color: 'var(--text-secondary)', textAlign: 'center' }}>
{error}
</p>
2025-11-04 21:51:05 +00:00
<button
onClick={() => {
setError(null)
setShowLogin(true)
}}
style={{
padding: '12px 24px',
borderRadius: '12px',
background: 'var(--button-accent)',
color: 'white',
border: 'none',
cursor: 'pointer',
fontSize: '16px'
}}
>
Попробовать снова
</button>
2025-11-03 20:35:01 +00:00
</div>
)
}
2025-11-04 21:51:05 +00:00
if (!user) {
return null
}
2025-11-04 22:31:04 +00:00
return (
<Routes>
<Route path="/" element={<Layout user={user} />}>
<Route index element={<Navigate to="/feed" replace />} />
<Route path="feed" element={<Feed user={user} />} />
<Route path="search" element={<Search user={user} />} />
<Route path="notifications" element={<Notifications user={user} />} />
<Route path="profile" element={<Profile user={user} setUser={setUser} />} />
<Route path="user/:id" element={<UserProfile currentUser={user} />} />
<Route path="post/:postId/comments" element={<CommentsPage user={user} />} />
<Route path="post/:postId/menu" element={<PostMenuPage user={user} />} />
</Route>
</Routes>
)
}
function App() {
2025-11-03 20:35:01 +00:00
return (
<BrowserRouter>
2025-11-04 22:31:04 +00:00
<AppContent />
2025-11-03 20:35:01 +00:00
</BrowserRouter>
)
}
export default App