feat(blog): Implement blog module with post management, image upload workflow, and localization
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 18s

- Added S3 image upload workflow documentation.
- Created custom hooks for managing blog posts, post details, and tags.
- Developed BlogListPage and BlogPostPage components for displaying posts.
- Integrated blog components and hooks into the main application.
- Updated localization files to include blog-related strings.
- Removed mock blog data and replaced it with dynamic data fetching.
This commit is contained in:
2025-10-25 13:58:15 +08:00
parent 4829c53355
commit 4417423612
19 changed files with 1782 additions and 471 deletions

View File

@@ -0,0 +1,129 @@
/**
* Blog Editor Component
* Extends the base RichTextEditor with blog-specific functionality
*/
import { LexicalComposer } from '@lexical/react/LexicalComposer'
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
import { ContentEditable } from '@lexical/react/LexicalContentEditable'
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'
import { HeadingNode, QuoteNode } from '@lexical/rich-text'
import { CodeNode, CodeHighlightNode } from '@lexical/code'
import { ListItemNode, ListNode } from '@lexical/list'
import { ListPlugin } from '@lexical/react/LexicalListPlugin'
import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin'
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode'
import { TableCellNode, TableNode, TableRowNode } from '@lexical/table'
import { TablePlugin } from '@lexical/react/LexicalTablePlugin'
import { LinkNode, AutoLinkNode } from '@lexical/link'
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin'
import { AutoLinkPlugin } from '@lexical/react/LexicalAutoLinkPlugin'
import { HashtagNode } from '@lexical/hashtag'
import { ImageNode } from '../../editor/nodes/ImageNode'
import { MentionNode } from '../../editor/nodes/MentionNode'
import ToolbarPlugin from '../../editor/plugins/ToolbarPlugin'
import MarkdownPlugin from '../../editor/plugins/MarkdownShortcutPlugin'
import DragDropPastePlugin from '../../editor/plugins/DragDropPastePlugin'
import HashtagPlugin from '../../editor/plugins/HashtagPlugin'
import MentionsPlugin from '../../editor/plugins/MentionsPlugin'
import editorTheme from '../../editor/themes/EditorTheme'
import { BlogImagePlugin } from './BlogImagePlugin'
import '../../editor/styles/editor.css'
const URL_MATCHER =
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/
const EMAIL_MATCHER =
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/
const MATCHERS = [
(text: string) => {
const match = URL_MATCHER.exec(text)
if (match === null) {
return null
}
const fullMatch = match[0]
return {
index: match.index,
length: fullMatch.length,
text: fullMatch,
url: fullMatch.startsWith('http') ? fullMatch : `https://${fullMatch}`,
}
},
(text: string) => {
const match = EMAIL_MATCHER.exec(text)
if (match === null) {
return null
}
const fullMatch = match[0]
return {
index: match.index,
length: fullMatch.length,
text: fullMatch,
url: `mailto:${fullMatch}`,
}
},
]
interface BlogEditorProps {
placeholder?: string
}
export default function BlogEditor({ placeholder }: BlogEditorProps) {
const editorConfig = {
namespace: 'BlogEditor',
theme: editorTheme,
onError(error: Error) {
console.error(error)
},
nodes: [
HeadingNode,
QuoteNode,
CodeNode,
CodeHighlightNode,
ListNode,
ListItemNode,
HorizontalRuleNode,
TableNode,
TableRowNode,
TableCellNode,
LinkNode,
AutoLinkNode,
ImageNode,
HashtagNode,
MentionNode,
],
}
return (
<LexicalComposer initialConfig={editorConfig}>
<div className="editor-container">
<ToolbarPlugin />
<div className="editor-inner">
<RichTextPlugin
contentEditable={<ContentEditable className="editor-input" />}
placeholder={
<div className="editor-placeholder">
{placeholder || 'Start writing your blog post...'}
</div>
}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
<ListPlugin />
<CheckListPlugin />
<LinkPlugin />
<AutoLinkPlugin matchers={MATCHERS} />
<TablePlugin />
<BlogImagePlugin />
<DragDropPastePlugin />
<HashtagPlugin />
<MentionsPlugin />
<MarkdownPlugin />
</div>
</div>
</LexicalComposer>
)
}