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 ? ( -Загрузка...
-Загрузка...
-Загрузка...
+Пока нет комментариев
Будьте первым! @@ -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 ({decodeHtmlEntities(c.content)}
+ {isEditing ? ( +{decodeHtmlEntities(c.content)}
+ )}