From b9298de4664d5c12b60b363d175e31c88f90f1b2 Mon Sep 17 00:00:00 2001 From: Yume <2839681263@qq.com> Date: Fri, 30 May 2025 18:23:04 +0800 Subject: [PATCH] feat(UI): change style of code-content, and add CommentSection --- .../CodeView/BlobView/CodeContent.tsx | 54 +------ .../CodeView/BlobView/CommentEditor.tsx | 106 +++++++++++++ .../CodeView/BlobView/CommentItem.tsx | 84 +++++++++++ .../CodeView/BlobView/CommentSection.tsx | 140 ++++++++++++++++++ .../web/pages/[org]/code/blob/[...path].tsx | 71 +++++++-- 5 files changed, 391 insertions(+), 64 deletions(-) create mode 100644 moon/apps/web/components/CodeView/BlobView/CommentEditor.tsx create mode 100644 moon/apps/web/components/CodeView/BlobView/CommentItem.tsx create mode 100644 moon/apps/web/components/CodeView/BlobView/CommentSection.tsx diff --git a/moon/apps/web/components/CodeView/BlobView/CodeContent.tsx b/moon/apps/web/components/CodeView/BlobView/CodeContent.tsx index d0c0b84c1..87f0bdeef 100644 --- a/moon/apps/web/components/CodeView/BlobView/CodeContent.tsx +++ b/moon/apps/web/components/CodeView/BlobView/CodeContent.tsx @@ -1,16 +1,11 @@ -import Editor from '@/components/CodeView/BlobView/Editor' - import 'github-markdown-css/github-markdown-light.css' - import { useEffect, useRef, useState } from 'react' import { Highlight, themes } from 'prism-react-renderer' -import { createRoot } from 'react-dom/client' import styles from './CodeContent.module.css' // @ts-ignore const CodeContent = ({ fileContent }) => { - const [__showEditor, setShowEditor] = useState(false) const [lfs, setLfs] = useState(false) useEffect(() => { @@ -20,32 +15,6 @@ const CodeContent = ({ fileContent }) => { }, [fileContent]) const lineRef = useRef([]) - // @ts-ignore - const handleLineNumberClick = (lineIndex) => { - setShowEditor(prev => { - const newShowEditor = !prev - const codeLineNumber = lineRef.current[lineIndex] - - if (newShowEditor) { - const editorContainer = document.createElement('div') - - editorContainer.className = 'editor-container' - const root = createRoot(editorContainer) - - root.render() - if (codeLineNumber && codeLineNumber.parentNode) { - codeLineNumber.parentNode.insertBefore(editorContainer, codeLineNumber.nextSibling) - } - } else { - const editorContainer = document.querySelector('.editor-container') - - if (editorContainer && editorContainer.parentNode) { - editorContainer.parentNode.removeChild(editorContainer) - } - } - return newShowEditor - }) - } function isLfsContent(content: string): boolean { const lines = content.split('\n') @@ -70,18 +39,13 @@ const CodeContent = ({ fileContent }) => { return (
-
- - -
- {({ style, tokens, getLineProps, getTokenProps }) => (
@@ -94,22 +58,6 @@ const CodeContent = ({ fileContent }) => {
                   // @ts-ignore
                   ref={(el) => lineRef.current[i] = el as HTMLDivElement}
                 >
-                  
                   {i + 1}
                   {line.map((token, key) => (
                     // eslint-disable-next-line react/no-array-index-key
diff --git a/moon/apps/web/components/CodeView/BlobView/CommentEditor.tsx b/moon/apps/web/components/CodeView/BlobView/CommentEditor.tsx
new file mode 100644
index 000000000..43a367161
--- /dev/null
+++ b/moon/apps/web/components/CodeView/BlobView/CommentEditor.tsx
@@ -0,0 +1,106 @@
+import React, { useState } from 'react'
+import { useEditor, EditorContent } from '@tiptap/react'
+import { getMarkdownExtensions } from '@gitmono/editor/markdown'
+import { Button } from '@gitmono/ui/Button'
+
+interface CommentEditorProps {
+  onSubmit: (content: string) => void
+  onCancel?: () => void
+  placeholder?: string
+  initialContent?: string
+  isSubmitting?: boolean
+}
+
+export function CommentEditor({
+                                onSubmit,
+                                onCancel,
+                                placeholder = '写下您的评论...',
+                                initialContent = '',
+                                isSubmitting = false
+                              }: CommentEditorProps) {
+  const [content, setContent] = useState(initialContent)
+
+  const editor = useEditor({
+    extensions: getMarkdownExtensions({
+      placeholder,
+      enableInlineAttachments: true,
+      blurAtTop: { enabled: false },
+      codeBlockHighlighted: {
+        HTMLAttributes: {
+          class: 'hljs'
+        }
+      }
+    }),
+    content: initialContent,
+    onUpdate: ({ editor }) => {
+      setContent(editor.getHTML())
+    },
+    editorProps: {
+      attributes: {
+        class: 'prose prose-sm max-w-none focus:outline-none min-h-[80px] p-3 border rounded-md'
+      }
+    }
+  })
+
+  const handleSubmit = () => {
+    if (!editor) return
+
+    const markdownContent = editor.getText().trim()
+
+    if (markdownContent) {
+      onSubmit(content)
+      editor.commands.clearContent()
+      setContent('')
+    }
+  }
+
+  const handleKeyDown = (event: React.KeyboardEvent) => {
+    // Ctrl/Cmd + Enter 快速提交
+    if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
+      event.preventDefault()
+      handleSubmit()
+    }
+  }
+
+  return (
+    
+
+ +
+ +
+
+ 支持 Markdown 语法 + + `Ctrl` + `Enter` 快速发布 +
+ +
+ {onCancel && ( + + )} + +
+
+
+ ) +} \ No newline at end of file diff --git a/moon/apps/web/components/CodeView/BlobView/CommentItem.tsx b/moon/apps/web/components/CodeView/BlobView/CommentItem.tsx new file mode 100644 index 000000000..068fc4572 --- /dev/null +++ b/moon/apps/web/components/CodeView/BlobView/CommentItem.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import { Avatar } from '@gitmono/ui/Avatar' +import { formatDistanceToNow } from 'date-fns' +import { zhCN } from 'date-fns/locale' + +interface CommentItemProps { + id: string + content: string + author: { + id: string + name: string + avatar?: string + } + createdAt: Date + onReply?: (commentId: string) => void + onEdit?: (commentId: string) => void + onDelete?: (commentId: string) => void + canEdit?: boolean + canDelete?: boolean +} + +export function CommentItem({ + id, + content, + author, + createdAt, + onReply, + onEdit, + onDelete, + canEdit = false, + canDelete = false + }: CommentItemProps) { + + return ( +
+ + +
+
+ {author.name} + + {formatDistanceToNow(createdAt, { + addSuffix: true, + locale: zhCN + })} + +
+ +
+ {content} +
+ +
+ {onReply && ( + + )} + + {canEdit && onEdit && ( + + )} + + {canDelete && onDelete && ( + + )} +
+
+
+ ) +} \ No newline at end of file diff --git a/moon/apps/web/components/CodeView/BlobView/CommentSection.tsx b/moon/apps/web/components/CodeView/BlobView/CommentSection.tsx new file mode 100644 index 000000000..8aa2579f6 --- /dev/null +++ b/moon/apps/web/components/CodeView/BlobView/CommentSection.tsx @@ -0,0 +1,140 @@ +import React, { useState } from 'react'; +import { Avatar } from '@gitmono/ui/Avatar'; +import { CommentEditor } from './CommentEditor'; +import { CommentItem } from './CommentItem'; + +interface Comment { + id: string + content: string + author: { + id: string + name: string + avatar?: string + } + createdAt: Date + replies?: Comment[] +} + +interface CommentSectionProps { + comments: Comment[] + currentUser?: { + id: string + name: string + avatar?: string + } + onAddComment: (content: string) => Promise + onReplyComment: (parentId: string, content: string) => Promise + onEditComment?: (commentId: string, content: string) => Promise + onDeleteComment?: (commentId: string) => Promise + isLoading?: boolean +} + +export function CommentSection({ + comments, + currentUser, + onAddComment, + onReplyComment, + onEditComment, + onDeleteComment, + isLoading = false +}: CommentSectionProps) { + const [isSubmitting, setIsSubmitting] = useState(false) + const [replyingTo, setReplyingTo] = useState(null) + const [__editCommentId, setEditCommentId] = useState(null) + + const handleSubmitComment = async (content: string) => { + if (!currentUser) return + + setIsSubmitting(true) + try { + await onAddComment(content) + } finally { + setIsSubmitting(false) + } + } + + const handleReply = async (parentId: string, content: string) => { + if (!currentUser) return + + setIsSubmitting(true) + try { + await onReplyComment(parentId, content) + setReplyingTo(null) + } finally { + setIsSubmitting(false) + } + } + + const handleEditComment = async (commentId: string, content: string) => { + if (!currentUser) return + + setIsSubmitting(true) + try { + if (onEditComment) { + await onEditComment(commentId, content) + } + setEditCommentId(null) + } finally { + setIsSubmitting(false) + } + } + + return ( +
+ {currentUser && ( +
+ +
+ +
+
+ )} + + {/*the comment modification function has not been implemented yet*/} +
+ {comments.map((comment) => ( +
+ setReplyingTo(id)} + onEdit={(id) => handleEditComment(id, comment.content)} + onDelete={onDeleteComment} + canEdit={currentUser?.id === comment.author.id} + canDelete={currentUser?.id === comment.author.id} + /> + + {replyingTo === comment.id && currentUser && ( +
+ handleReply(comment.id, content)} + onCancel={() => setReplyingTo(null)} + isSubmitting={isSubmitting} + placeholder={`回复 @${comment.author.name}...`} + /> +
+ )} + + {comment.replies && comment.replies.length > 0 && ( +
+ {comment.replies.map((reply) => ( + + ))} +
+ )} +
+ ))} +
+ + {isLoading && ( +
+ 加载中... +
+ )} +
+ ) +} \ No newline at end of file diff --git a/moon/apps/web/pages/[org]/code/blob/[...path].tsx b/moon/apps/web/pages/[org]/code/blob/[...path].tsx index af3210e1d..bb04f396b 100644 --- a/moon/apps/web/pages/[org]/code/blob/[...path].tsx +++ b/moon/apps/web/pages/[org]/code/blob/[...path].tsx @@ -6,18 +6,10 @@ import { AppLayout } from '@/components/Layout/AppLayout' import AuthAppProviders from '@/components/Providers/AuthAppProviders' import { useGetBlob } from '@/hooks/useGetBlob' import { useRouter } from 'next/router' - -const treeStyle = { - borderRadius: 8, - overflow: 'hidden', - width: 'calc(15% - 8px)', - maxWidth: 'calc(15% - 8px)', - background: '#fff' -} +import { CommentSection } from '@/components/CodeView/BlobView/CommentSection' const codeStyle = { borderRadius: 8, - overflow: 'hidden', width: 'calc(85% - 8px)', background: '#fff' } @@ -30,22 +22,79 @@ const breadStyle = { background: '#fff' } +interface Comment { + id: string + content: string + author: { + id: string + name: string + avatar?: string + } + createdAt: Date + replies?: Comment[] +} + function BlobPage() { const { path = [] } = useRouter().query as { path?: string[] } const new_path = '/' + path.join('/') const fileContent = useGetBlob({ path: new_path }).data?.data?? "" + const mockComments: Comment[] = [ + { + id: '1', + content: '这段代码逻辑很清晰,建议可以添加一些错误处理。', + author: { + id: '1', + name: '张三', + avatar: '' + }, + createdAt: new Date('2024-12-01 10:30:00'), + replies: [] + }, + { + id: '2', + content: '同意。', + author: { + id: '2', + name: '李四', + avatar: '' + }, + createdAt: new Date('2024-12-01 10:30:00'), + replies: [ + { + id: '3', + content: '好的,收到。', + author: { + id: '3', + name: '王五', + avatar: '' + }, + createdAt: new Date('2024-12-01 10:30:00') + } + ] + } + ] + const handleAddComment = (__content: string, __lineNumber?: number) => { + //wait for complete + } + + const handleReplyComment = (__commentId: string, __content: string) => { + //wait for complete + } return ( -
+
- {/* */} + + {/* @ts-ignore */} + +
)