feat: add toolbar styles and functionality for blog editor
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 18s

feat: implement EditorTheme for consistent styling across editor components

feat: define types for blog-related operations including image uploads and post creation

feat: create DropdownColorPicker component for color selection in blog editor

feat: implement ImageResizer component for resizing images in the blog editor

feat: add export and import functionality for blog posts in JSON format

feat: update main application routes to include CreatePost page

feat: enhance Blog page with a button to navigate to CreatePost

feat: implement CreatePost page with title, cover image upload, and content editor
This commit is contained in:
2025-10-25 21:03:58 +08:00
parent 4829c53355
commit e299694f22
26 changed files with 4095 additions and 1 deletions

355
src/pages/CreatePost.tsx Normal file
View File

@@ -0,0 +1,355 @@
import { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import Layout from '../components/Layout';
import BlogEditor from '../blog/BlogEditor';
import { uploadImage, createBlogPost } from '../blog/api';
import '../App.css';
function CreatePost() {
const navigate = useNavigate();
const [title, setTitle] = useState('');
const [coverImage, setCoverImage] = useState<string | null>(null);
const [coverImageKey, setCoverImageKey] = useState<string>('');
const [uploadProgress, setUploadProgress] = useState<number>(0);
const [isUploading, setIsUploading] = useState(false);
const [uploadError, setUploadError] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleCoverImageChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
// Validate file type
if (!file.type.startsWith('image/')) {
setUploadError('Please select an image file');
return;
}
// Show preview immediately
const reader = new FileReader();
reader.onload = (e) => {
setCoverImage(e.target?.result as string);
};
reader.readAsDataURL(file);
// Upload to S3
setIsUploading(true);
setUploadError('');
setUploadProgress(0);
try {
const { fileKey, url } = await uploadImage(file, (progress) => {
setUploadProgress(progress);
});
setCoverImageKey(fileKey);
setCoverImage(url);
setUploadProgress(100);
console.log('✅ Cover image uploaded successfully');
} catch (error) {
setUploadError(error instanceof Error ? error.message : 'Upload failed');
console.error('❌ Cover image upload failed:', error);
} finally {
setIsUploading(false);
}
};
const handleSubmit = async () => {
if (!title.trim()) {
alert('Please enter a title');
return;
}
if (!coverImageKey) {
alert('Please upload a cover image');
return;
}
setIsSubmitting(true);
try {
// Get editor content
// Note: You'll need to export the editor content from the BlogEditor component
// For now, we'll use a placeholder
const content = JSON.stringify({ editorState: 'placeholder' });
const response = await createBlogPost(title, content, coverImageKey);
console.log('✅ Blog post created successfully:', response.postId);
alert('Blog post created successfully!');
// Navigate to the blog page or the created post
navigate('/blog');
} catch (error) {
console.error('❌ Failed to create blog post:', error);
alert(error instanceof Error ? error.message : 'Failed to create blog post');
} finally {
setIsSubmitting(false);
}
};
return (
<Layout currentPage="blog">
<div style={{
maxWidth: '900px',
margin: '0 auto',
padding: '120px 2rem 80px'
}}>
{/* Header */}
<div style={{ marginBottom: '3rem' }}>
<h1 style={{
fontSize: '2.5rem',
fontWeight: 'bold',
color: 'var(--text-primary)',
marginBottom: '1rem'
}}>
Create New Post
</h1>
<p style={{
fontSize: '1.1rem',
color: 'var(--text-secondary)'
}}>
Share your thoughts with the community
</p>
</div>
{/* Title Input */}
<div style={{ marginBottom: '2rem' }}>
<label style={{
display: 'block',
fontSize: '1rem',
fontWeight: '600',
color: 'var(--text-primary)',
marginBottom: '0.5rem'
}}>
Title *
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter your blog post title..."
style={{
width: '100%',
padding: '1rem',
fontSize: '1.2rem',
border: '2px solid var(--border-color)',
borderRadius: '8px',
background: 'var(--bg-card)',
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)';
}}
/>
</div>
{/* Cover Image Upload */}
<div style={{ marginBottom: '2rem' }}>
<label style={{
display: 'block',
fontSize: '1rem',
fontWeight: '600',
color: 'var(--text-primary)',
marginBottom: '0.5rem'
}}>
Cover Image *
</label>
<div style={{
border: '2px dashed var(--border-color)',
borderRadius: '8px',
padding: '2rem',
textAlign: 'center',
background: 'var(--bg-card)',
cursor: 'pointer',
transition: 'border-color 0.3s ease'
}}
onClick={() => fileInputRef.current?.click()}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'var(--accent-color)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--border-color)';
}}
>
{coverImage ? (
<div style={{ position: 'relative' }}>
<img
src={coverImage}
alt="Cover"
style={{
maxWidth: '100%',
maxHeight: '400px',
borderRadius: '8px'
}}
/>
{isUploading && (
<div style={{
position: 'absolute',
bottom: '10px',
left: '50%',
transform: 'translateX(-50%)',
background: 'rgba(0, 0, 0, 0.8)',
padding: '10px 20px',
borderRadius: '8px',
color: 'white',
minWidth: '200px'
}}>
<div style={{
background: 'rgba(255, 255, 255, 0.2)',
borderRadius: '4px',
overflow: 'hidden',
height: '6px',
marginBottom: '5px'
}}>
<div style={{
background: 'var(--accent-color, #4CAF50)',
height: '100%',
width: `${uploadProgress}%`,
transition: 'width 0.3s ease'
}} />
</div>
<div style={{ fontSize: '14px' }}>
Uploading... {Math.round(uploadProgress)}%
</div>
</div>
)}
</div>
) : (
<div>
<div style={{
fontSize: '3rem',
marginBottom: '1rem'
}}>
📷
</div>
<p style={{
color: 'var(--text-secondary)',
fontSize: '1rem'
}}>
Click to upload cover image
</p>
<p style={{
color: 'var(--text-secondary)',
fontSize: '0.9rem',
marginTop: '0.5rem'
}}>
PNG, JPG, GIF up to 10MB
</p>
</div>
)}
</div>
{uploadError && (
<div style={{
marginTop: '1rem',
padding: '1rem',
background: 'rgba(244, 67, 54, 0.1)',
border: '1px solid rgba(244, 67, 54, 0.3)',
borderRadius: '8px',
color: '#F44336'
}}>
{uploadError}
</div>
)}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleCoverImageChange}
style={{ display: 'none' }}
/>
</div>
{/* Content Editor */}
<div style={{ marginBottom: '2rem' }}>
<label style={{
display: 'block',
fontSize: '1rem',
fontWeight: '600',
color: 'var(--text-primary)',
marginBottom: '0.5rem'
}}>
Content *
</label>
<div style={{
border: '2px solid var(--border-color)',
borderRadius: '8px',
overflow: 'hidden',
background: 'var(--bg-card)'
}}>
<BlogEditor />
</div>
</div>
{/* Action Buttons */}
<div style={{
display: 'flex',
gap: '1rem',
justifyContent: 'flex-end'
}}>
<button
onClick={() => navigate('/blog')}
style={{
padding: '1rem 2rem',
fontSize: '1rem',
fontWeight: '600',
border: '2px solid var(--border-color)',
borderRadius: '8px',
background: 'transparent',
color: 'var(--text-primary)',
cursor: 'pointer',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--bg-secondary)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={isSubmitting || isUploading}
style={{
padding: '1rem 2rem',
fontSize: '1rem',
fontWeight: '600',
border: 'none',
borderRadius: '8px',
background: isSubmitting || isUploading ? 'var(--bg-secondary)' : 'var(--accent-color)',
color: 'white',
cursor: isSubmitting || isUploading ? 'not-allowed' : 'pointer',
transition: 'all 0.3s ease',
opacity: isSubmitting || isUploading ? 0.6 : 1
}}
onMouseEnter={(e) => {
if (!isSubmitting && !isUploading) {
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';
}}
>
{isSubmitting ? 'Publishing...' : 'Publish Post'}
</button>
</div>
</div>
</Layout>
);
}
export default CreatePost;