From fbeb53d96f5891edd46ee4788d1d7cae3c562f60 Mon Sep 17 00:00:00 2001 From: glpshchn <464976@niuitmo.ru> Date: Fri, 5 Dec 2025 00:45:02 +0300 Subject: [PATCH] Update files --- frontend/src/components/CommentsModal.css | 96 ++++++++++- frontend/src/components/CommentsModal.jsx | 196 ++++++++++++++++------ frontend/src/components/PostCard.jsx | 1 + moderation/frontend/src/App.jsx | 186 +++++++++++++++++++- moderation/frontend/src/utils/api.js | 6 + 5 files changed, 429 insertions(+), 56 deletions(-) diff --git a/frontend/src/components/CommentsModal.css b/frontend/src/components/CommentsModal.css index d6dc958..effd109 100644 --- a/frontend/src/components/CommentsModal.css +++ b/frontend/src/components/CommentsModal.css @@ -5,19 +5,23 @@ left: 0; right: 0; bottom: 0; - background: var(--bg-secondary); + background: rgba(0, 0, 0, 0.5); z-index: 10500; overflow: hidden; touch-action: none; overscroll-behavior: contain; + display: flex; + align-items: flex-end; } .comments-modal { width: 100%; - height: 100%; + max-height: 50vh; + height: 50vh; background: var(--bg-secondary); display: flex; flex-direction: column; + border-radius: 20px 20px 0 0; /* Убираем pointer-events и touch-action чтобы клики работали */ } @@ -163,6 +167,8 @@ .comment-item { display: flex; gap: 12px; + position: relative; + padding-right: 40px; } .comment-avatar { @@ -256,3 +262,89 @@ [data-theme="dark"] .send-btn svg { stroke: #000000; } + +.comment-actions { + position: absolute; + right: 0; + top: 0; + display: flex; + gap: 4px; + align-items: center; +} + +.comment-action-btn { + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--bg-primary); + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: center; + border: none; + cursor: pointer; + transition: all 0.2s; +} + +.comment-action-btn:hover { + background: var(--divider-color); + color: var(--text-primary); +} + +.comment-action-btn.delete:hover { + background: #FF3B30; + color: white; +} + +.comment-action-btn svg { + stroke: currentColor; +} + +.comment-edit-form { + margin-top: 8px; +} + +.comment-edit-input { + width: 100%; + padding: 8px 12px; + border-radius: 12px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 14px; + border: 1px solid var(--divider-color); + margin-bottom: 8px; +} + +.comment-edit-actions { + display: flex; + gap: 8px; +} + +.comment-edit-btn { + padding: 6px 12px; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + border: none; + cursor: pointer; + transition: all 0.2s; +} + +.comment-edit-btn.save { + background: var(--button-accent); + color: white; +} + +.comment-edit-btn.save:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.comment-edit-btn.cancel { + background: var(--bg-primary); + color: var(--text-secondary); +} + +.comment-edit-btn:hover:not(:disabled) { + opacity: 0.8; +} diff --git a/frontend/src/components/CommentsModal.jsx b/frontend/src/components/CommentsModal.jsx index e94f2be..5a24306 100644 --- a/frontend/src/components/CommentsModal.jsx +++ b/frontend/src/components/CommentsModal.jsx @@ -1,18 +1,20 @@ import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' -import { X, Send } from 'lucide-react' -import { commentPost, getPosts } from '../utils/api' -import { hapticFeedback } from '../utils/telegram' +import { X, Send, Trash2, Edit2 } from 'lucide-react' +import { commentPost, getPosts, deleteComment, editComment } from '../utils/api' +import { hapticFeedback, showConfirm } from '../utils/telegram' import { decodeHtmlEntities } from '../utils/htmlEntities' import './CommentsModal.css' -export default function CommentsModal({ post, onClose, onUpdate }) { +export default function CommentsModal({ post, onClose, onUpdate, currentUser }) { // ВСЕ хуки должны вызываться всегда, до любых условных возвратов const [comment, setComment] = useState('') const [loading, setLoading] = useState(false) const [comments, setComments] = useState([]) const [fullPost, setFullPost] = useState(null) const [loadingPost, setLoadingPost] = useState(false) + const [editingCommentId, setEditingCommentId] = useState(null) + const [editText, setEditText] = useState('') // Загрузить полные данные поста с комментариями // ВАЖНО: useEffect всегда вызывается, даже если post отсутствует @@ -139,6 +141,86 @@ export default function CommentsModal({ post, onClose, onUpdate }) { } } + const handleDeleteComment = async (commentId) => { + if (!post || !post._id) return + + const confirmed = await showConfirm('Удалить этот комментарий?') + if (!confirmed) return + + try { + hapticFeedback('light') + const result = await deleteComment(post._id, commentId) + + if (result && result.comments && Array.isArray(result.comments)) { + const commentsWithAuthors = result.comments.filter(c => { + return c && c.author && (typeof c.author === 'object') + }) + setComments(commentsWithAuthors) + if (fullPost) { + setFullPost({ ...fullPost, comments: commentsWithAuthors }) + } + hapticFeedback('success') + + if (onUpdate) { + onUpdate() + } + } + } catch (error) { + console.error('[CommentsModal] Ошибка удаления комментария:', error) + hapticFeedback('error') + } + } + + const handleStartEdit = (comment) => { + setEditingCommentId(comment._id) + setEditText(comment.content) + } + + const handleCancelEdit = () => { + setEditingCommentId(null) + setEditText('') + } + + const handleSaveEdit = async (commentId) => { + if (!post || !post._id || !editText.trim()) return + + try { + hapticFeedback('light') + const result = await editComment(post._id, commentId, editText.trim()) + + if (result && result.comments && Array.isArray(result.comments)) { + const commentsWithAuthors = result.comments.filter(c => { + return c && c.author && (typeof c.author === 'object') + }) + setComments(commentsWithAuthors) + if (fullPost) { + setFullPost({ ...fullPost, comments: commentsWithAuthors }) + } + setEditingCommentId(null) + setEditText('') + hapticFeedback('success') + + if (onUpdate) { + onUpdate() + } + } + } catch (error) { + console.error('[CommentsModal] Ошибка редактирования комментария:', error) + hapticFeedback('error') + } + } + + const isCommentAuthor = (comment) => { + if (!currentUser || !comment.author) return false + const authorId = comment.author._id || comment.author + const userId = currentUser.id || currentUser._id + return authorId === userId + } + + const isModerator = () => { + return currentUser && (currentUser.role === 'moderator' || currentUser.role === 'admin') + } + // ВСЕГДА рендерим createPortal, даже если пост не валиден // Это критично для соблюдения правил хуков return createPortal( @@ -159,56 +241,15 @@ export default function CommentsModal({ post, onClose, onUpdate }) {
- {/* Пост */} - {!hasValidPost ? ( -
-
-

Загрузка...

-
-
- ) : loadingPost ? ( -
-
-
-

Загрузка...

-
-
- ) : ( -
- {displayPost.author && ( -
- {displayPost.author?.username { e.target.src = '/default-avatar.png' }} - /> -
-
- {displayPost.author?.firstName || ''} {displayPost.author?.lastName || ''} - {!displayPost.author?.firstName && !displayPost.author?.lastName && 'Пользователь'} -
-
@{displayPost.author?.username || displayPost.author?.firstName || 'user'}
-
-
- )} - - {displayPost.content && ( -
{decodeHtmlEntities(displayPost.content)}
- )} - - {((displayPost.images && displayPost.images.length > 0) || displayPost.imageUrl) && ( -
- Post -
- )} -
- )} - {/* Список комментариев */} {hasValidPost && (
- {comments.length === 0 ? ( + {loadingPost ? ( +
+
+

Загрузка...

+
+ ) : comments.length === 0 ? (

Пока нет комментариев

Будьте первым! @@ -227,6 +268,10 @@ export default function CommentsModal({ post, onClose, onUpdate }) { console.warn('[CommentsModal] Комментарий без автора:', c) return null } + + const canEdit = isCommentAuthor(c) || isModerator() + const isEditing = editingCommentId === commentId + return (
{formatDate(c.createdAt)}
-

{decodeHtmlEntities(c.content)}

+ {isEditing ? ( +
+ setEditText(e.target.value)} + className="comment-edit-input" + autoFocus + /> +
+ + +
+
+ ) : ( +

{decodeHtmlEntities(c.content)}

+ )}
+ {canEdit && !isEditing && ( +
+ {isCommentAuthor(c) && ( + + )} + +
+ )}
) }) diff --git a/frontend/src/components/PostCard.jsx b/frontend/src/components/PostCard.jsx index 43eb710..61e01cd 100644 --- a/frontend/src/components/PostCard.jsx +++ b/frontend/src/components/PostCard.jsx @@ -334,6 +334,7 @@ export default function PostCard({ post, currentUser, onUpdate }) { {showComments && createPortal( setShowComments(false)} onUpdate={onUpdate} />, diff --git a/moderation/frontend/src/App.jsx b/moderation/frontend/src/App.jsx index 84cbc3f..00d7315 100644 --- a/moderation/frontend/src/App.jsx +++ b/moderation/frontend/src/App.jsx @@ -15,7 +15,9 @@ import { initiateAddAdmin, confirmAddAdmin, initiateRemoveAdmin, - confirmRemoveAdmin + confirmRemoveAdmin, + getPostComments, + deleteComment } from './utils/api'; import { io } from 'socket.io-client'; import { @@ -31,7 +33,8 @@ import { Ban, UserPlus, UserMinus, - Crown + Crown, + X } from 'lucide-react'; const TABS = [ @@ -112,6 +115,10 @@ export default function App() { const chatSocketRef = useRef(null); const chatListRef = useRef(null); + // Comments modal + const [commentsModal, setCommentsModal] = useState(null); // { postId, comments: [] } + const [commentsLoading, setCommentsLoading] = useState(false); + useEffect(() => { let cancelled = false; @@ -415,6 +422,41 @@ export default function App() { loadUsers(); }; + const handleOpenComments = async (postId) => { + setCommentsLoading(true); + try { + const post = await getPostComments(postId); + setCommentsModal({ + postId, + comments: post.comments || [], + postContent: post.content + }); + } catch (error) { + console.error('Ошибка загрузки комментариев:', error); + alert('Ошибка загрузки комментариев'); + } finally { + setCommentsLoading(false); + } + }; + + const handleDeleteComment = async (commentId) => { + if (!commentsModal) return; + if (!window.confirm('Удалить этот комментарий?')) return; + + try { + await deleteComment(commentsModal.postId, commentId); + // Обновить список комментариев + const post = await getPostComments(commentsModal.postId); + setCommentsModal({ + ...commentsModal, + comments: post.comments || [] + }); + } catch (error) { + console.error('Ошибка удаления комментария:', error); + alert('Ошибка удаления комментария'); + } + }; + const handleReportStatus = async (reportId, status) => { await updateReportStatus(reportId, { status }); loadReports(); @@ -581,6 +623,10 @@ export default function App() { ) : null}
+ +
+ +
+ {commentsLoading ? ( +
+ +
+ ) : commentsModal.comments.length === 0 ? ( +
+ Нет комментариев +
+ ) : ( +
+ {commentsModal.comments.map((comment) => ( +
+
+
+ + {comment.author?.firstName || comment.author?.username || 'Пользователь'} + + + {formatDate(comment.createdAt)} + +
+

+ {comment.content} +

+
+ +
+ ))} +
+ )} +
+
+ + )} ); } diff --git a/moderation/frontend/src/utils/api.js b/moderation/frontend/src/utils/api.js index 9bcf09a..f26c267 100644 --- a/moderation/frontend/src/utils/api.js +++ b/moderation/frontend/src/utils/api.js @@ -104,5 +104,11 @@ export const initiateRemoveAdmin = (adminId) => export const confirmRemoveAdmin = (adminId, code) => api.post('/mod-app/admins/confirm-remove', { adminId, code }).then((res) => res.data) +export const getPostComments = (postId) => + api.get(`/posts/${postId}`).then((res) => res.data.post) + +export const deleteComment = (postId, commentId) => + api.delete(`/posts/${postId}/comments/${commentId}`).then((res) => res.data) + export default api