Update files

This commit is contained in:
glpshchn 2025-12-01 08:40:27 +03:00
parent cfce134654
commit 01f1e1ae94
9 changed files with 305 additions and 44 deletions

156
EMPTY_SCREEN_FIX.md Normal file
View File

@ -0,0 +1,156 @@
# Исправление: Пустой экран после верификации
## 🔴 Проблема
После успешной верификации UI открывается, но моментально пропадает (полностью пустой экран).
---
## ✅ Что исправлено
### 1. **Улучшена обработка ошибок**
- Добавлено подробное логирование в консоль браузера
- Теперь видно, на каком этапе происходит ошибка
### 2. **Исправлен пустой экран**
- Вместо `return null` теперь показывается сообщение с кнопкой перезагрузки
- Это помогает понять, что происходит
### 3. **Отложен запуск initDataChecker**
- `initDataChecker` теперь запускается только после успешной загрузки пользователя
- Это предотвращает преждевременную перезагрузку страницы
### 4. **Улучшена валидация данных**
- Проверяется, что `userData` действительно получен
- Если `null` или `undefined` - показывается ошибка
---
## 🔍 Как диагностировать
### Шаг 1: Откройте консоль браузера
1. В Telegram откройте приложение
2. Нажмите **F12** или **Cmd+Option+I** (Mac)
3. Перейдите на вкладку **Console**
### Шаг 2: Посмотрите логи
**Должны увидеть:**
```
[App] Начало инициализации...
[App] Telegram WebApp найден, initData: есть
[API] verifyAuth: отправка запроса...
[API] verifyAuth: получен ответ: { hasUser: true, userId: "...", username: "..." }
[App] verifyAuth вернул: данные пользователя
[App] Пользователь установлен, ID: ...
[App] Инициализация завершена, loading: false
```
**Если видите ошибку:**
```
[API] verifyAuth: ошибка: { message: "...", status: 401/500 }
[App] Ошибка инициализации: ...
```
---
## 🐛 Возможные причины
### 1. **Ошибка 401 (Unauthorized)**
**Причина:** `initData` невалиден или истек
**Решение:**
- Перезагрузите приложение в Telegram
- Убедитесь, что используете официальный клиент Telegram
### 2. **Ошибка 500 (Server Error)**
**Причина:** Проблема на backend
**Решение:**
```bash
# Проверьте логи backend
docker logs nakama-backend --tail 100
# Ищите ошибки:
# - "Ошибка verify"
# - "MongoDB connection"
# - "MinIO connection"
```
### 3. **userData = null/undefined**
**Причина:** Backend не вернул данные пользователя
**Решение:**
- Проверьте маршрут `/api/auth/verify` на backend
- Убедитесь, что пользователь существует в БД
- Проверьте, что `respondWithUser` работает правильно
### 4. **Ошибка в компонентах Feed/Layout**
**Причина:** Компонент падает при рендеринге
**Решение:**
- В консоли браузера будет **красная ошибка** с указанием файла и строки
- Проверьте, что все данные пользователя присутствуют (`user.settings`, `user.photoUrl`, etc.)
---
## 🔧 Быстрое решение
### 1. Перезагрузите приложение
```javascript
// В консоли браузера
window.location.reload()
```
### 2. Проверьте backend
```bash
# Проверьте, что backend работает
curl http://your-backend-url/api/health
# Проверьте логи
docker logs nakama-backend -f
```
### 3. Проверьте MongoDB
```bash
# Подключитесь к MongoDB
docker exec -it nakama-mongodb mongosh
# Проверьте пользователей
use nakama
db.users.find().limit(5)
```
---
## 📋 Контрольный список
- [ ] Консоль браузера открыта (F12)
- [ ] Видны логи `[App]` и `[API]`
- [ ] Нет красных ошибок в консоли
- [ ] Backend доступен (`/api/health`)
- [ ] MongoDB подключена
- [ ] Пользователь существует в БД
- [ ] `initData` валиден (не истек)
---
## 💡 Если все еще не работает
### Отправьте мне:
1. **Логи из консоли браузера** (скопируйте все сообщения)
2. **Логи backend** (`docker logs nakama-backend --tail 200`)
3. **Скриншот пустого экрана**
4. **Ошибки из консоли** (красные сообщения)
---
## ✅ После исправления
После применения исправлений вы должны видеть:
- ✅ Логи в консоли на каждом этапе
- ✅ Сообщение об ошибке вместо пустого экрана
- ✅ Кнопку "Перезагрузить" если что-то пошло не так
**Теперь откройте приложение и посмотрите консоль браузера!** 🔍

View File

@ -39,7 +39,7 @@ router.get('/', authenticate, async (req, res) => {
query.isNSFW = false; query.isNSFW = false;
} }
const posts = await Post.find(query) let posts = await Post.find(query)
.populate('author', 'username firstName lastName photoUrl') .populate('author', 'username firstName lastName photoUrl')
.populate('mentionedUsers', 'username firstName lastName') .populate('mentionedUsers', 'username firstName lastName')
.populate('comments.author', 'username firstName lastName photoUrl') .populate('comments.author', 'username firstName lastName photoUrl')
@ -48,6 +48,9 @@ router.get('/', authenticate, async (req, res) => {
.skip((page - 1) * limit) .skip((page - 1) * limit)
.exec(); .exec();
// Фильтруем посты без автора (защита от ошибок)
posts = posts.filter(post => post.author !== null && post.author !== undefined);
const count = await Post.countDocuments(query); const count = await Post.countDocuments(query);
res.json({ res.json({

View File

@ -30,8 +30,7 @@ function AppContent() {
initApp() initApp()
} }
// Запустить проверку initData // НЕ запускать initDataChecker здесь - он запустится после успешной загрузки пользователя
startInitDataChecker()
return () => { return () => {
stopInitDataChecker() stopInitDataChecker()
@ -40,6 +39,7 @@ function AppContent() {
const initApp = async () => { const initApp = async () => {
try { try {
console.log('[App] Начало инициализации...')
initTelegramApp() initTelegramApp()
const tg = window.Telegram?.WebApp const tg = window.Telegram?.WebApp
@ -52,12 +52,25 @@ function AppContent() {
throw new Error('Telegram не передал initData. Откройте приложение из официального клиента.') throw new Error('Telegram не передал initData. Откройте приложение из официального клиента.')
} }
console.log('[App] Telegram WebApp найден, initData:', tg.initData ? 'есть' : 'нет')
tg.disableVerticalSwipes?.() tg.disableVerticalSwipes?.()
tg.expand?.() tg.expand?.()
console.log('[App] Вызов verifyAuth...')
const userData = await verifyAuth() const userData = await verifyAuth()
console.log('[App] verifyAuth вернул:', userData ? 'данные пользователя' : 'null/undefined', userData)
if (!userData) {
throw new Error('Не удалось получить данные пользователя. Попробуйте перезагрузить страницу.')
}
setUser(userData) setUser(userData)
setError(null) setError(null)
console.log('[App] Пользователь установлен, ID:', userData.id || userData._id)
// Запустить проверку initData только после успешной загрузки
startInitDataChecker()
if (!startParamProcessed.current && tg?.startParam?.startsWith('post_')) { if (!startParamProcessed.current && tg?.startParam?.startsWith('post_')) {
startParamProcessed.current = true startParamProcessed.current = true
@ -67,10 +80,17 @@ function AppContent() {
}, 200) }, 200)
} }
} catch (err) { } catch (err) {
console.error('Ошибка инициализации:', err) console.error('[App] Ошибка инициализации:', err)
console.error('[App] Детали ошибки:', {
message: err.message,
response: err.response?.data,
status: err.response?.status
})
setError(err?.response?.data?.error || err.message || 'Ошибка авторизации') setError(err?.response?.data?.error || err.message || 'Ошибка авторизации')
setUser(null) // Явно сбросить user при ошибке
} finally { } finally {
setLoading(false) setLoading(false)
console.log('[App] Инициализация завершена, loading:', false)
} }
} }
@ -126,7 +146,36 @@ function AppContent() {
} }
if (!user) { if (!user) {
return null // Показываем сообщение вместо пустого экрана
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>
<button
onClick={() => window.location.reload()}
style={{
padding: '12px 24px',
borderRadius: '12px',
background: 'var(--button-accent)',
color: 'white',
border: 'none',
cursor: 'pointer',
fontSize: '16px'
}}
>
Перезагрузить
</button>
</div>
)
} }
return ( return (

View File

@ -64,16 +64,17 @@ export default function CommentsModal({ post, onClose, onUpdate }) {
<div className="post-preview"> <div className="post-preview">
<div className="preview-author"> <div className="preview-author">
<img <img
src={post.author.photoUrl || '/default-avatar.png'} src={post.author?.photoUrl || '/default-avatar.png'}
alt={post.author.username || post.author.firstName || 'User'} alt={post.author?.username || post.author?.firstName || 'User'}
className="preview-avatar" className="preview-avatar"
onError={(e) => { e.target.src = '/default-avatar.png' }}
/> />
<div> <div>
<div className="preview-name"> <div className="preview-name">
{post.author.firstName || ''} {post.author.lastName || ''} {post.author?.firstName || ''} {post.author?.lastName || ''}
{!post.author.firstName && !post.author.lastName && 'Пользователь'} {!post.author?.firstName && !post.author?.lastName && 'Пользователь'}
</div> </div>
<div className="preview-username">@{post.author.username || post.author.firstName || 'user'}</div> <div className="preview-username">@{post.author?.username || post.author?.firstName || 'user'}</div>
</div> </div>
</div> </div>
@ -96,18 +97,21 @@ export default function CommentsModal({ post, onClose, onUpdate }) {
<span>Будьте первым!</span> <span>Будьте первым!</span>
</div> </div>
) : ( ) : (
comments.map((c, index) => ( comments
.filter(c => c.author) // Фильтруем комментарии без автора
.map((c, index) => (
<div key={index} className="comment-item fade-in"> <div key={index} className="comment-item fade-in">
<img <img
src={c.author.photoUrl || '/default-avatar.png'} src={c.author?.photoUrl || '/default-avatar.png'}
alt={c.author.username || c.author.firstName || 'User'} alt={c.author?.username || c.author?.firstName || 'User'}
className="comment-avatar" className="comment-avatar"
onError={(e) => { e.target.src = '/default-avatar.png' }}
/> />
<div className="comment-content"> <div className="comment-content">
<div className="comment-header"> <div className="comment-header">
<span className="comment-author"> <span className="comment-author">
{c.author.firstName || ''} {c.author.lastName || ''} {c.author?.firstName || ''} {c.author?.lastName || ''}
{!c.author.firstName && !c.author.lastName && 'Пользователь'} {!c.author?.firstName && !c.author?.lastName && 'Пользователь'}
</span> </span>
<span className="comment-time">{formatDate(c.createdAt)}</span> <span className="comment-time">{formatDate(c.createdAt)}</span>
</div> </div>

View File

@ -19,13 +19,28 @@ const TAG_NAMES = {
export default function PostCard({ post, currentUser, onUpdate }) { export default function PostCard({ post, currentUser, onUpdate }) {
const navigate = useNavigate() const navigate = useNavigate()
const [liked, setLiked] = useState(post.likes.includes(currentUser.id)) const [liked, setLiked] = useState(post.likes?.includes(currentUser.id) || false)
const [likesCount, setLikesCount] = useState(post.likes.length) const [likesCount, setLikesCount] = useState(post.likes?.length || 0)
const [currentImageIndex, setCurrentImageIndex] = useState(0) const [currentImageIndex, setCurrentImageIndex] = useState(0)
const [showFullView, setShowFullView] = useState(false) const [showFullView, setShowFullView] = useState(false)
// Проверка на существование автора
if (!post.author) {
console.warn('[PostCard] Post without author:', post._id)
return null // Не показываем посты без автора
}
// Поддержка и старого поля imageUrl и нового images // Поддержка и старого поля imageUrl и нового images
const images = post.images && post.images.length > 0 ? post.images : (post.imageUrl ? [post.imageUrl] : []) // Фильтруем старые URL из Telegram API
const allImages = post.images && post.images.length > 0 ? post.images : (post.imageUrl ? [post.imageUrl] : [])
const images = allImages.filter(img => {
// Игнорируем старые URL из Telegram API
if (img && typeof img === 'string' && img.includes('api.telegram.org/file/bot')) {
console.warn('[PostCard] Skipping old Telegram URL:', img)
return false
}
return true
})
const handleLike = async () => { const handleLike = async () => {
try { try {
@ -61,7 +76,9 @@ export default function PostCard({ post, currentUser, onUpdate }) {
} }
const goToProfile = () => { const goToProfile = () => {
navigate(`/user/${post.author._id}`) if (post.author?._id) {
navigate(`/user/${post.author._id}`)
}
} }
const openFullView = () => { const openFullView = () => {
@ -103,17 +120,20 @@ export default function PostCard({ post, currentUser, onUpdate }) {
<div className="post-header"> <div className="post-header">
<div className="post-author" onClick={goToProfile}> <div className="post-author" onClick={goToProfile}>
<img <img
src={post.author.photoUrl || '/default-avatar.png'} src={post.author?.photoUrl || '/default-avatar.png'}
alt={post.author.username || post.author.firstName || 'User'} alt={post.author?.username || post.author?.firstName || 'User'}
className="author-avatar" className="author-avatar"
onError={(e) => {
e.target.src = '/default-avatar.png'
}}
/> />
<div className="author-info"> <div className="author-info">
<div className="author-name"> <div className="author-name">
{post.author.firstName || ''} {post.author.lastName || ''} {post.author?.firstName || ''} {post.author?.lastName || ''}
{!post.author.firstName && !post.author.lastName && 'Пользователь'} {!post.author?.firstName && !post.author?.lastName && 'Пользователь'}
</div> </div>
<div className="post-date"> <div className="post-date">
@{post.author.username || post.author.firstName || 'user'} · {formatDate(post.createdAt)} @{post.author?.username || post.author?.firstName || 'user'} · {formatDate(post.createdAt)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -122,16 +122,17 @@ export default function CommentsPage({ user }) {
<div className="post-preview"> <div className="post-preview">
<div className="preview-author"> <div className="preview-author">
<img <img
src={post.author.photoUrl || '/default-avatar.png'} src={post.author?.photoUrl || '/default-avatar.png'}
alt={post.author.username || post.author.firstName || 'User'} alt={post.author?.username || post.author?.firstName || 'User'}
className="preview-avatar" className="preview-avatar"
onError={(e) => { e.target.src = '/default-avatar.png' }}
/> />
<div> <div>
<div className="preview-name"> <div className="preview-name">
{post.author.firstName || ''} {post.author.lastName || ''} {post.author?.firstName || ''} {post.author?.lastName || ''}
{!post.author.firstName && !post.author.lastName && 'Пользователь'} {!post.author?.firstName && !post.author?.lastName && 'Пользователь'}
</div> </div>
<div className="preview-username">@{post.author.username || post.author.firstName || 'user'}</div> <div className="preview-username">@{post.author?.username || post.author?.firstName || 'user'}</div>
</div> </div>
</div> </div>
@ -156,23 +157,26 @@ export default function CommentsPage({ user }) {
<span>Будьте первым!</span> <span>Будьте первым!</span>
</div> </div>
) : ( ) : (
comments.map((c, index) => { comments
.filter(c => c.author) // Фильтруем комментарии без автора
.map((c, index) => {
const isEditing = editingCommentId === c._id const isEditing = editingCommentId === c._id
const isOwnComment = c.author._id === user.id const isOwnComment = c.author?._id === user.id
const canEdit = isOwnComment || user.role === 'moderator' || user.role === 'admin' const canEdit = isOwnComment || user.role === 'moderator' || user.role === 'admin'
return ( return (
<div key={index} className="comment-item fade-in"> <div key={index} className="comment-item fade-in">
<img <img
src={c.author.photoUrl || '/default-avatar.png'} src={c.author?.photoUrl || '/default-avatar.png'}
alt={c.author.username || c.author.firstName || 'User'} alt={c.author?.username || c.author?.firstName || 'User'}
className="comment-avatar" className="comment-avatar"
onError={(e) => { e.target.src = '/default-avatar.png' }}
/> />
<div className="comment-content"> <div className="comment-content">
<div className="comment-header"> <div className="comment-header">
<span className="comment-author"> <span className="comment-author">
{c.author.firstName || ''} {c.author.lastName || ''} {c.author?.firstName || ''} {c.author?.lastName || ''}
{!c.author.firstName && !c.author.lastName && 'Пользователь'} {!c.author?.firstName && !c.author?.lastName && 'Пользователь'}
</span> </span>
<span className="comment-time"> <span className="comment-time">
{formatDate(c.createdAt)} {formatDate(c.createdAt)}

View File

@ -71,10 +71,13 @@ export default function Feed({ user }) {
const data = await getPosts(params) const data = await getPosts(params)
// Фильтруем посты без автора (защита от ошибок)
const validPosts = data.posts.filter(post => post.author)
if (pageNum === 1) { if (pageNum === 1) {
setPosts(data.posts) setPosts(validPosts)
} else { } else {
setPosts(prev => [...prev, ...data.posts]) setPosts(prev => [...prev, ...validPosts])
} }
setHasMore(pageNum < data.totalPages) setHasMore(pageNum < data.totalPages)

View File

@ -188,16 +188,17 @@ export default function PostMenuPage({ user }) {
<div className="post-preview"> <div className="post-preview">
<div className="preview-author"> <div className="preview-author">
<img <img
src={post.author.photoUrl || '/default-avatar.png'} src={post.author?.photoUrl || '/default-avatar.png'}
alt={post.author.username || post.author.firstName || 'User'} alt={post.author?.username || post.author?.firstName || 'User'}
className="preview-avatar" className="preview-avatar"
onError={(e) => { e.target.src = '/default-avatar.png' }}
/> />
<div> <div>
<div className="preview-name"> <div className="preview-name">
{post.author.firstName || ''} {post.author.lastName || ''} {post.author?.firstName || ''} {post.author?.lastName || ''}
{!post.author.firstName && !post.author.lastName && 'Пользователь'} {!post.author?.firstName && !post.author?.lastName && 'Пользователь'}
</div> </div>
<div className="preview-username">@{post.author.username || post.author.firstName || 'user'}</div> <div className="preview-username">@{post.author?.username || post.author?.firstName || 'user'}</div>
</div> </div>
</div> </div>

View File

@ -79,8 +79,29 @@ export const signInWithTelegram = async (initData) => {
} }
export const verifyAuth = async () => { export const verifyAuth = async () => {
const response = await api.post('/auth/verify') try {
return response.data.user console.log('[API] verifyAuth: отправка запроса...')
const response = await api.post('/auth/verify')
console.log('[API] verifyAuth: получен ответ:', {
hasUser: !!response.data?.user,
userId: response.data?.user?.id || response.data?.user?._id,
username: response.data?.user?.username
})
if (!response.data?.user) {
console.error('[API] verifyAuth: ответ не содержит user:', response.data)
throw new Error('Сервер не вернул данные пользователя')
}
return response.data.user
} catch (error) {
console.error('[API] verifyAuth: ошибка:', {
message: error.message,
response: error.response?.data,
status: error.response?.status
})
throw error
}
} }
// Авторизация через Telegram OAuth (Login Widget) // Авторизация через Telegram OAuth (Login Widget)