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 */} +
+ {/* Title Input */} +
+ + setTitle(e.target.value)} + placeholder="Enter your blog post title..." + required + style={{ + width: '100%', + padding: '1rem', + fontSize: '1.2rem', + border: '2px solid var(--border-color)', + borderRadius: '8px', + background: 'var(--bg-secondary)', + color: 'var(--text-primary)', + outline: 'none', + transition: 'border-color 0.3s ease', + }} + onFocus={(e) => { + e.currentTarget.style.borderColor = 'var(--accent-color)' + }} + onBlur={(e) => { + e.currentTarget.style.borderColor = 'var(--border-color)' + }} + /> +
+ + {/* Cover Image Upload */} +
+ + + {!coverImagePreview ? ( +
fileInputRef.current?.click()} + style={{ + border: '2px dashed var(--border-color)', + borderRadius: '8px', + padding: '3rem', + textAlign: 'center', + cursor: 'pointer', + background: 'var(--bg-secondary)', + transition: 'border-color 0.3s ease', + }} + onMouseEnter={(e) => { + e.currentTarget.style.borderColor = 'var(--accent-color)' + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = 'var(--border-color)' + }} + > +
📷
+

+ Click to upload cover image +

+

+ PNG, JPG, GIF up to 10MB +

+
+ ) : ( +
+ Cover preview + +
+ )} + + +
+ + {/* Content Editor */} +
+ +
+ +
+

+ 💡 Tip: You can drag and drop or paste images directly into the editor. They will be automatically uploaded to S3. +

+
+ + {/* Action Buttons */} +
+ + + +
+
+ + {/* 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( } /> } /> } /> + } /> } /> } /> } />