feat: implement EditPost page with functionality to update existing blog posts
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 17s

This commit is contained in:
2025-10-26 18:21:45 +08:00
parent 085e48ff69
commit 1f6bb77691
8 changed files with 566 additions and 56 deletions

436
src/pages/EditPost.tsx Normal file
View File

@@ -0,0 +1,436 @@
import { useEffect, useRef, useState } from 'react';
import { Navigate, useNavigate, useParams } from 'react-router-dom';
import Layout from '../components/Layout';
import BlogEditor, { type BlogEditorRef } from '../blog/BlogEditor';
import { getBlogPost, updateBlogPost, uploadImage } from '../blog/api';
import { Toast } from '../components/Toast';
import { useToast } from '../hooks/useToast';
import { useAuth } from '../contexts/AuthContext';
import '../App.css';
function EditPost() {
const { postId } = useParams<{ postId: string }>();
const navigate = useNavigate();
const editorRef = useRef<BlogEditorRef>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const { toasts, removeToast, success, error } = useToast();
const { isAuthenticated } = useAuth();
const [isLoading, setIsLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
const [title, setTitle] = useState('');
const [initialContent, setInitialContent] = useState<string | undefined>(undefined);
const [coverImage, setCoverImage] = useState<string | null>(null);
const [coverImageKey, setCoverImageKey] = useState<string>('');
const [uploadProgress, setUploadProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);
const [uploadError, setUploadError] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (!postId) {
setLoadError('Post not found.');
setIsLoading(false);
return;
}
(async () => {
try {
const fetched = await getBlogPost(postId);
setTitle(fetched.title);
setInitialContent(fetched.content);
setCoverImage(fetched.coverImageUrl ?? null);
setCoverImageKey(fetched.coverImageKey ?? '');
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to load post.';
setLoadError(message);
console.error('Failed to load blog post for editing:', err);
} finally {
setIsLoading(false);
}
})();
}, [postId]);
const handleCoverImageChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}
if (!file.type.startsWith('image/')) {
setUploadError('Please select an image file');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
setCoverImage(e.target?.result as string);
};
reader.readAsDataURL(file);
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!', 2000);
} catch (err) {
const message = err instanceof Error ? err.message : 'Upload failed';
setUploadError(message);
error(`Cover image upload failed: ${message}`);
} finally {
setIsUploading(false);
}
};
const handleSubmit = async () => {
if (!postId) {
error('Invalid post identifier.');
return;
}
if (!title.trim()) {
error('Please enter a title');
return;
}
if (!editorRef.current) {
error('Editor not initialized');
return;
}
const content = editorRef.current.getEditorState();
if (!coverImageKey) {
error('Please upload a cover image');
return;
}
setIsSubmitting(true);
try {
await updateBlogPost({
postId,
title: title.trim(),
content,
coverImageKey,
});
success('Blog post updated successfully!', 2000);
setTimeout(() => {
navigate(`/blog/${postId}`);
}, 1500);
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to update blog post';
error(`Failed to update post: ${message}`);
} finally {
setIsSubmitting(false);
}
};
if (!isAuthenticated) {
return <Navigate to="/blog" replace />;
}
return (
<Layout currentPage="blog">
<Toast toasts={toasts} onRemove={removeToast} />
<div
style={{
maxWidth: '900px',
margin: '0 auto',
padding: '120px 2rem 80px',
}}
>
{isLoading ? (
<div style={{ color: 'var(--text-secondary)' }}>Loading post...</div>
) : loadError ? (
<div
style={{
padding: '2rem',
borderRadius: '12px',
border: '1px solid rgba(244, 67, 54, 0.3)',
background: 'rgba(244, 67, 54, 0.1)',
color: '#F44336',
}}
>
{loadError}
</div>
) : (
<>
<div style={{ marginBottom: '3rem' }}>
<h1
style={{
fontSize: '2.5rem',
fontWeight: 'bold',
color: 'var(--text-primary)',
marginBottom: '1rem',
}}
>
Edit Post
</h1>
<p style={{ fontSize: '1.1rem', color: 'var(--text-secondary)' }}>
Update your article content and cover image
</p>
</div>
<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>
<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>
<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} initialContent={initialContent} />
</div>
</div>
<div
style={{
display: 'flex',
gap: '1rem',
justifyContent: 'flex-end',
}}
>
<button
onClick={() => navigate(`/blog/${postId}`)}
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 ? 'Saving...' : 'Save Changes'}
</button>
</div>
</>
)}
</div>
</Layout>
);
}
export default EditPost;