Files
www.cialloo.com/src/pages/CreatePost.tsx
cialloo 9c263d0f51
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 17s
refactor: remove authentication check and unused imports from CreatePost and EditPost pages
2025-10-26 18:33:31 +08:00

367 lines
11 KiB
TypeScript

import { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import Layout from '../components/Layout';
import BlogEditor, { type BlogEditorRef } from '../blog/BlogEditor';
import { uploadImage, createBlogPost } from '../blog/api';
import { Toast } from '../components/Toast';
import { useToast } from '../hooks/useToast';
import '../App.css';
function CreatePost() {
const navigate = useNavigate();
const editorRef = useRef<BlogEditorRef>(null);
const { toasts, removeToast, success, error } = useToast();
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);
success('Cover image uploaded successfully!');
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Upload failed';
setUploadError(errorMessage);
error(`Cover image upload failed: ${errorMessage}`);
} finally {
setIsUploading(false);
}
};
const handleSubmit = async () => {
if (!title.trim()) {
alert('Please enter a title');
return;
}
if (!coverImageKey) {
alert('Please upload a cover image');
return;
}
// Get editor content
if (!editorRef.current) {
alert('Editor not initialized');
return;
}
setIsSubmitting(true);
try {
// Get the actual editor state as JSON
const content = editorRef.current.getEditorState();
await createBlogPost(title, content, coverImageKey);
success('Blog post published successfully!', 2000);
// Navigate to the blog page after a short delay
setTimeout(() => {
navigate('/blog');
}, 1500);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to create blog post';
error(`Failed to publish post: ${errorMessage}`);
} finally {
setIsSubmitting(false);
}
};
return (
<Layout currentPage="blog">
<Toast toasts={toasts} onRemove={removeToast} />
<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 ref={editorRef} />
</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;