nakama/frontend/src/components/CommentsModal.jsx

281 lines
11 KiB
React
Raw Normal View History

2025-12-04 21:30:35 +00:00
import { useState, useEffect } from 'react'
2025-12-04 20:27:45 +00:00
import { createPortal } from 'react-dom'
2025-11-03 20:35:01 +00:00
import { X, Send } from 'lucide-react'
2025-12-04 20:47:07 +00:00
import { commentPost, getPosts } from '../utils/api'
2025-11-03 21:52:13 +00:00
import { hapticFeedback } from '../utils/telegram'
2025-12-04 20:00:39 +00:00
import { decodeHtmlEntities } from '../utils/htmlEntities'
2025-11-03 20:35:01 +00:00
import './CommentsModal.css'
export default function CommentsModal({ post, onClose, onUpdate }) {
2025-12-04 21:23:24 +00:00
// ВСЕ хуки должны вызываться всегда, до любых условных возвратов
2025-11-03 20:35:01 +00:00
const [comment, setComment] = useState('')
const [loading, setLoading] = useState(false)
2025-12-04 20:47:07 +00:00
const [comments, setComments] = useState([])
2025-12-04 20:53:54 +00:00
const [fullPost, setFullPost] = useState(null)
2025-12-04 20:47:07 +00:00
const [loadingPost, setLoadingPost] = useState(false)
// Загрузить полные данные поста с комментариями
2025-12-04 21:36:49 +00:00
// ВАЖНО: useEffect всегда вызывается, даже если post отсутствует
2025-12-04 21:23:24 +00:00
useEffect(() => {
2025-12-04 21:36:49 +00:00
// Если пост не передан, очищаем состояние и выходим
2025-12-04 21:23:24 +00:00
if (!post || !post._id) {
2025-12-04 21:30:35 +00:00
setFullPost(null)
setComments([])
2025-12-04 21:36:49 +00:00
setLoadingPost(false)
2025-12-04 20:53:54 +00:00
return
2025-12-04 20:47:07 +00:00
}
2025-12-04 20:53:54 +00:00
2025-12-04 21:23:24 +00:00
// Сначала установим переданные данные
setFullPost(post)
const initialComments = (post.comments || []).filter(c => {
return c && c.author && (typeof c.author === 'object')
})
setComments(initialComments)
// Затем загрузим полные данные для обновления
2025-12-04 21:36:49 +00:00
let cancelled = false
2025-12-04 21:23:24 +00:00
const loadFullPost = async () => {
try {
setLoadingPost(true)
// Загрузить посты с фильтром по автору поста для оптимизации
const authorId = post?.author?._id || post?.author
const response = authorId
? await getPosts({ userId: authorId, limit: 100 })
: await getPosts({ limit: 200 })
2025-12-04 21:36:49 +00:00
// Проверяем, что запрос не был отменен
if (cancelled) return
2025-12-04 21:23:24 +00:00
const foundPost = response.posts?.find(p => p._id === post._id)
if (foundPost) {
// Проверяем, что комментарии populate'ены с авторами
const commentsWithAuthors = (foundPost.comments || []).filter(c => {
return c && c.author && (typeof c.author === 'object')
})
setComments(commentsWithAuthors)
setFullPost(foundPost)
}
} catch (error) {
console.error('[CommentsModal] Ошибка загрузки поста:', error)
// Оставляем переданные данные
} finally {
2025-12-04 21:36:49 +00:00
if (!cancelled) {
setLoadingPost(false)
}
2025-12-04 20:47:07 +00:00
}
}
2025-12-04 21:23:24 +00:00
loadFullPost()
2025-12-04 21:36:49 +00:00
// Cleanup функция для отмены запроса при размонтировании
return () => {
cancelled = true
}
2025-12-04 21:39:03 +00:00
}, [post?._id]) // Используем просто post?._id без || null
2025-12-04 20:53:54 +00:00
// Проверка на существование поста ПОСЛЕ хуков
2025-12-04 20:47:07 +00:00
const displayPost = fullPost || post
2025-12-04 21:36:49 +00:00
const hasValidPost = post && post._id && displayPost && displayPost.author
2025-11-03 20:35:01 +00:00
const handleSubmit = async () => {
2025-12-04 21:36:49 +00:00
if (!comment.trim() || loading || !post || !post._id) return
2025-11-03 20:35:01 +00:00
try {
setLoading(true)
hapticFeedback('light')
const result = await commentPost(post._id, comment)
2025-12-04 21:16:35 +00:00
console.log('[CommentsModal] Результат добавления комментария:', result)
if (result && result.comments && Array.isArray(result.comments)) {
// Фильтруем комментарии с авторами (проверяем, что author - объект)
const commentsWithAuthors = result.comments.filter(c => {
return c && c.author && (typeof c.author === 'object')
})
console.log('[CommentsModal] Отфильтрованные комментарии:', commentsWithAuthors.length, 'из', result.comments.length)
setComments(commentsWithAuthors)
2025-12-04 21:10:11 +00:00
// Обновить полный пост
if (fullPost) {
2025-12-04 21:16:35 +00:00
setFullPost({ ...fullPost, comments: commentsWithAuthors })
2025-12-04 21:10:11 +00:00
}
2025-12-04 21:16:35 +00:00
setComment('')
hapticFeedback('success')
if (onUpdate) {
onUpdate()
}
} else {
console.error('[CommentsModal] Неожиданный формат ответа:', result)
hapticFeedback('error')
}
2025-12-04 21:30:35 +00:00
} catch (error) {
console.error('[CommentsModal] Ошибка добавления комментария:', error)
hapticFeedback('error')
2025-11-03 20:35:01 +00:00
} finally {
setLoading(false)
}
}
const formatDate = (date) => {
2025-12-04 21:39:03 +00:00
if (!date) return 'только что'
2025-11-03 20:35:01 +00:00
const d = new Date(date)
const now = new Date()
const diff = Math.floor((now - d) / 1000) // секунды
if (diff < 60) return 'только что'
if (diff < 3600) return `${Math.floor(diff / 60)} мин`
if (diff < 86400) return `${Math.floor(diff / 3600)} ч`
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' })
}
2025-11-03 22:41:34 +00:00
const handleOverlayClick = (e) => {
// Закрывать только при клике на overlay, не на контент
if (e.target === e.currentTarget) {
onClose()
}
}
2025-12-04 21:39:03 +00:00
// ВСЕГДА рендерим createPortal, даже если пост не валиден
// Это критично для соблюдения правил хуков
2025-12-04 20:27:45 +00:00
return createPortal(
2025-12-04 20:11:28 +00:00
<div
className="comments-modal-overlay"
onMouseDown={(e) => e.stopPropagation()}
onTouchStart={(e) => e.stopPropagation()}
onClick={handleOverlayClick}
2025-12-04 21:39:03 +00:00
style={{ display: hasValidPost ? 'flex' : 'none' }}
2025-12-04 20:11:28 +00:00
>
2025-11-03 22:41:34 +00:00
<div className="comments-modal" onClick={(e) => e.stopPropagation()}>
2025-11-03 20:35:01 +00:00
{/* Хедер */}
<div className="modal-header">
<button className="close-btn" onClick={onClose}>
<X size={24} />
</button>
2025-11-03 21:57:35 +00:00
<h2>Комментарии</h2>
<div style={{ width: 40 }} />
</div>
{/* Пост */}
2025-12-04 21:39:03 +00:00
{!hasValidPost ? (
<div className="post-preview">
<div className="loading-state">
<p>Загрузка...</p>
</div>
</div>
) : loadingPost ? (
2025-12-04 20:47:07 +00:00
<div className="post-preview">
<div className="loading-state">
<div className="spinner" />
<p>Загрузка...</p>
2025-11-03 21:57:35 +00:00
</div>
</div>
2025-12-04 20:47:07 +00:00
) : (
<div className="post-preview">
2025-12-04 21:36:49 +00:00
{displayPost.author && (
<div className="preview-author">
<img
src={displayPost.author?.photoUrl || '/default-avatar.png'}
alt={displayPost.author?.username || displayPost.author?.firstName || 'User'}
className="preview-avatar"
onError={(e) => { e.target.src = '/default-avatar.png' }}
/>
<div>
<div className="preview-name">
{displayPost.author?.firstName || ''} {displayPost.author?.lastName || ''}
{!displayPost.author?.firstName && !displayPost.author?.lastName && 'Пользователь'}
</div>
<div className="preview-username">@{displayPost.author?.username || displayPost.author?.firstName || 'user'}</div>
2025-12-04 20:47:07 +00:00
</div>
</div>
2025-12-04 21:36:49 +00:00
)}
2025-12-04 20:47:07 +00:00
{displayPost.content && (
<div className="preview-content">{decodeHtmlEntities(displayPost.content)}</div>
)}
{((displayPost.images && displayPost.images.length > 0) || displayPost.imageUrl) && (
<div className="preview-image">
<img src={displayPost.images?.[0] || displayPost.imageUrl} alt="Post" />
</div>
)}
</div>
)}
2025-11-03 20:35:01 +00:00
{/* Список комментариев */}
2025-12-04 21:39:03 +00:00
{hasValidPost && (
<div className="comments-list">
{comments.length === 0 ? (
<div className="empty-comments">
<p>Пока нет комментариев</p>
<span>Будьте первым!</span>
2025-11-03 20:35:01 +00:00
</div>
2025-12-04 21:39:03 +00:00
) : (
comments
.filter(c => {
// Фильтруем комментарии без автора или с неполным автором
return c && c.author && (typeof c.author === 'object') && c.content
})
.map((c, index) => {
// Используем _id если есть, иначе index
const commentId = c._id || c.id || `comment-${index}`
// Проверяем, что автор полностью загружен
if (!c.author || typeof c.author !== 'object') {
console.warn('[CommentsModal] Комментарий без автора:', c)
return null
}
return (
<div key={commentId} className="comment-item fade-in">
<img
src={c.author?.photoUrl || '/default-avatar.png'}
alt={c.author?.username || c.author?.firstName || 'User'}
className="comment-avatar"
onError={(e) => { e.target.src = '/default-avatar.png' }}
/>
<div className="comment-content">
<div className="comment-header">
<span className="comment-author">
{c.author?.firstName || ''} {c.author?.lastName || ''}
{!c.author?.firstName && !c.author?.lastName && 'Пользователь'}
</span>
<span className="comment-time">{formatDate(c.createdAt)}</span>
</div>
<p className="comment-text">{decodeHtmlEntities(c.content)}</p>
</div>
</div>
)
})
.filter(Boolean) // Убираем null значения
)}
</div>
)}
2025-11-03 20:35:01 +00:00
{/* Форма добавления комментария */}
2025-12-04 21:39:03 +00:00
{hasValidPost && (
<div className="comment-form">
<input
type="text"
placeholder="Написать комментарий..."
value={comment}
onChange={e => setComment(e.target.value)}
onKeyPress={e => e.key === 'Enter' && handleSubmit()}
maxLength={500}
/>
<button
onClick={handleSubmit}
disabled={loading || !comment.trim()}
className="send-btn"
>
<Send size={20} />
</button>
</div>
)}
2025-11-03 20:35:01 +00:00
</div>
2025-12-04 21:39:03 +00:00
</div>,
document.body
2025-11-03 20:35:01 +00:00
)
}