From 1f2cb3268f5ab876a83474a130539f03520c52b0 Mon Sep 17 00:00:00 2001
From: cialloo
Date: Sat, 25 Oct 2025 14:31:26 +0800
Subject: [PATCH] feat(blog): add blog post creation page with image upload and
content editor
---
src/blog/index.ts | 1 +
src/blog/pages/BlogCreatePage.tsx | 416 ++++++++++++++++++++++++++++++
src/blog/pages/BlogListPage.tsx | 28 ++
src/main.tsx | 3 +-
4 files changed, 447 insertions(+), 1 deletion(-)
create mode 100644 src/blog/pages/BlogCreatePage.tsx
diff --git a/src/blog/index.ts b/src/blog/index.ts
index 4f1f5dc..a906dcf 100644
--- a/src/blog/index.ts
+++ b/src/blog/index.ts
@@ -5,6 +5,7 @@
// Pages
export { default as BlogListPage } from './pages/BlogListPage'
export { default as BlogPostPage } from './pages/BlogPostPage'
+export { default as BlogCreatePage } from './pages/BlogCreatePage'
// Components
export { default as BlogPostCard } from './components/BlogPostCard'
diff --git a/src/blog/pages/BlogCreatePage.tsx b/src/blog/pages/BlogCreatePage.tsx
new file mode 100644
index 0000000..9b81191
--- /dev/null
+++ b/src/blog/pages/BlogCreatePage.tsx
@@ -0,0 +1,416 @@
+/**
+ * Blog Post Creation Page
+ * Admin interface for creating new blog posts
+ */
+
+import { useState, useRef } from 'react'
+import { useNavigate } from 'react-router-dom'
+import Layout from '../../components/Layout'
+import BlogEditor from '../editor/BlogEditor'
+import { createPost, uploadFile } from '../api'
+import '../../App.css'
+
+export default function BlogCreatePage() {
+ const navigate = useNavigate()
+ const [title, setTitle] = useState('')
+ const [coverImage, setCoverImage] = useState(null)
+ const [coverImagePreview, setCoverImagePreview] = useState('')
+ const [isUploading, setIsUploading] = useState(false)
+ const [error, setError] = useState(null)
+ const fileInputRef = useRef(null)
+
+ // Get editor content - we'll use a simple approach since BlogEditor doesn't expose ref
+ const [editorContent] = useState('')
+
+ const handleCoverImageSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0]
+ if (file) {
+ setCoverImage(file)
+ const reader = new FileReader()
+ reader.onloadend = () => {
+ setCoverImagePreview(reader.result as string)
+ }
+ reader.readAsDataURL(file)
+ }
+ }
+
+ const handleRemoveCoverImage = () => {
+ setCoverImage(null)
+ setCoverImagePreview('')
+ if (fileInputRef.current) {
+ fileInputRef.current.value = ''
+ }
+ }
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+
+ if (!title.trim()) {
+ setError('Please enter a title')
+ return
+ }
+
+ if (!editorContent.trim()) {
+ setError('Please enter some content')
+ return
+ }
+
+ setIsUploading(true)
+ setError(null)
+
+ try {
+ // Upload cover image if selected
+ let coverImageKey = ''
+ if (coverImage) {
+ coverImageKey = await uploadFile(coverImage)
+ }
+
+ // Create the blog post
+ const { postId } = await createPost({
+ title: title.trim(),
+ content: editorContent,
+ coverImageKey,
+ })
+
+ // Show success message
+ alert('Blog post created successfully!')
+
+ // Navigate to the created post
+ navigate(`/blog/${postId}`)
+ } catch (err) {
+ console.error('Failed to create blog post:', err)
+ setError(err instanceof Error ? err.message : 'Failed to create blog post')
+ } finally {
+ setIsUploading(false)
+ }
+ }
+
+ const handleCancel = () => {
+ if (confirm('Are you sure you want to cancel? Your changes will be lost.')) {
+ navigate('/blog')
+ }
+ }
+
+ return (
+
+
+
+ {/* Header */}
+
+
+ Create New Blog Post
+
+
+ Write and publish a new article
+
+
+
+ {/* Error Message */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Form */}
+
+
+ {/* Help Text */}
+
+
+ âšī¸ Editor Tips
+
+
+ - Use Markdown shortcuts for quick formatting
+ - Drag and drop images directly into the editor
+ - Paste images from clipboard (Ctrl+V or Cmd+V)
+ - Use # for headings, * for lists, ``` for code blocks
+ - Add @mentions and #hashtags for better engagement
+ - All images are automatically uploaded to S3
+
+
+
+
+
+ )
+}
diff --git a/src/blog/pages/BlogListPage.tsx b/src/blog/pages/BlogListPage.tsx
index a9bd5a6..0f68bc5 100644
--- a/src/blog/pages/BlogListPage.tsx
+++ b/src/blog/pages/BlogListPage.tsx
@@ -4,6 +4,7 @@
*/
import { useTranslation } from 'react-i18next'
+import { Link } from 'react-router-dom'
import Layout from '../../components/Layout'
import BlogPostCard from '../components/BlogPostCard'
import BlogSidebar from '../components/BlogSidebar'
@@ -43,10 +44,37 @@ export default function BlogListPage() {
fontSize: '1.3rem',
color: 'var(--text-secondary)',
lineHeight: '1.6',
+ marginBottom: '2rem',
}}
>
{t('blog.subtitle')}
+
+ {/* Create Post Button */}
+ {
+ e.currentTarget.style.transform = 'translateY(-2px)'
+ e.currentTarget.style.boxShadow = '0 4px 12px var(--accent-shadow)'
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.transform = 'translateY(0)'
+ e.currentTarget.style.boxShadow = 'none'
+ }}
+ >
+ âī¸ Create New Post
+
diff --git a/src/main.tsx b/src/main.tsx
index 1de7ded..3bfab19 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -15,7 +15,7 @@ import Servers from './pages/Servers.tsx'
import Forum from './pages/Forum.tsx'
import AuthCallback from './pages/AuthCallback.tsx'
import EditorDemo from './pages/EditorDemo.tsx'
-import { BlogPostPage } from './blog'
+import { BlogPostPage, BlogCreatePage } from './blog'
createRoot(document.getElementById('root')!).render(
@@ -29,6 +29,7 @@ createRoot(document.getElementById('root')!).render(
} />
} />
} />
+ } />
} />
} />
} />