diff --git a/src/pages/EditPost.tsx b/src/pages/EditPost.tsx
index 9375f7a..7160cc9 100644
--- a/src/pages/EditPost.tsx
+++ b/src/pages/EditPost.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import Layout from '../components/Layout';
import BlogEditor, { type BlogEditorRef } from '../blog/BlogEditor';
@@ -7,6 +7,307 @@ import { Toast } from '../components/Toast';
import { useToast } from '../hooks/useToast';
import '../App.css';
+const styles = {
+ page: {
+ maxWidth: '900px',
+ margin: '0 auto',
+ padding: '120px 2rem 80px',
+ },
+ header: {
+ marginBottom: '3rem',
+ },
+ heading: {
+ fontSize: '2.5rem',
+ fontWeight: 'bold',
+ color: 'var(--text-primary)',
+ marginBottom: '1rem',
+ },
+ subHeading: {
+ fontSize: '1.1rem',
+ color: 'var(--text-secondary)',
+ },
+ fieldWrapper: {
+ marginBottom: '2rem',
+ },
+ label: {
+ display: 'block',
+ fontSize: '1rem',
+ fontWeight: 600,
+ color: 'var(--text-primary)',
+ marginBottom: '0.5rem',
+ },
+ textInput: {
+ 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',
+ },
+ coverDropZone: {
+ border: '2px dashed var(--border-color)',
+ borderRadius: '8px',
+ padding: '2rem',
+ textAlign: 'center' as const,
+ background: 'var(--bg-card)',
+ cursor: 'pointer',
+ },
+ coverPreview: {
+ position: 'relative' as const,
+ },
+ coverImage: {
+ maxWidth: '100%',
+ maxHeight: '400px',
+ borderRadius: '8px',
+ },
+ uploadOverlay: {
+ position: 'absolute' as const,
+ bottom: '10px',
+ left: '50%',
+ transform: 'translateX(-50%)',
+ background: 'rgba(0, 0, 0, 0.8)',
+ padding: '10px 20px',
+ borderRadius: '8px',
+ color: 'white',
+ minWidth: '200px',
+ },
+ progressTrack: {
+ background: 'rgba(255, 255, 255, 0.2)',
+ borderRadius: '4px',
+ overflow: 'hidden',
+ height: '6px',
+ marginBottom: '5px',
+ },
+ progressBar: {
+ background: 'var(--accent-color, #4CAF50)',
+ height: '100%',
+ transition: 'width 0.3s ease',
+ },
+ helperText: {
+ color: 'var(--text-secondary)',
+ fontSize: '1rem',
+ },
+ helperCaption: {
+ color: 'var(--text-secondary)',
+ fontSize: '0.9rem',
+ marginTop: '0.5rem',
+ },
+ errorBanner: {
+ marginTop: '1rem',
+ padding: '1rem',
+ background: 'rgba(244, 67, 54, 0.1)',
+ border: '1px solid rgba(244, 67, 54, 0.3)',
+ borderRadius: '8px',
+ color: '#F44336',
+ },
+ editorWrapper: {
+ border: '2px solid var(--border-color)',
+ borderRadius: '8px',
+ overflow: 'hidden',
+ background: 'var(--bg-card)',
+ },
+ actions: {
+ display: 'flex',
+ gap: '1rem',
+ justifyContent: 'flex-end',
+ },
+ secondaryButton: {
+ padding: '1rem 2rem',
+ fontSize: '1rem',
+ fontWeight: 600,
+ border: '2px solid var(--border-color)',
+ borderRadius: '8px',
+ background: 'transparent',
+ color: 'var(--text-primary)',
+ cursor: 'pointer',
+ },
+ primaryButton: {
+ padding: '1rem 2rem',
+ fontSize: '1rem',
+ fontWeight: 600,
+ border: 'none',
+ borderRadius: '8px',
+ background: 'var(--accent-color)',
+ color: 'white',
+ cursor: 'pointer',
+ transition: 'transform 0.2s ease',
+ },
+ disabledButton: {
+ background: 'var(--bg-secondary)',
+ cursor: 'not-allowed',
+ opacity: 0.6,
+ boxShadow: 'none',
+ transform: 'none',
+ },
+ loading: {
+ color: 'var(--text-secondary)',
+ },
+ errorContainer: {
+ padding: '2rem',
+ borderRadius: '12px',
+ border: '1px solid rgba(244, 67, 54, 0.3)',
+ background: 'rgba(244, 67, 54, 0.1)',
+ color: '#F44336',
+ },
+};
+
+interface TitleFieldProps {
+ title: string;
+ onChange: (value: string) => void;
+}
+
+function TitleField({ title, onChange }: TitleFieldProps) {
+ return (
+
+
+ onChange(event.target.value)}
+ placeholder="Enter your blog post title..."
+ style={styles.textInput}
+ />
+
+ );
+}
+
+interface CoverImageSectionProps {
+ coverImage: string | null;
+ uploadError: string;
+ isUploading: boolean;
+ uploadProgress: number;
+ fileInputRef: React.RefObject;
+ onFileSelected: (event: ChangeEvent) => void;
+}
+
+function CoverImageSection({
+ coverImage,
+ uploadError,
+ isUploading,
+ uploadProgress,
+ fileInputRef,
+ onFileSelected,
+}: CoverImageSectionProps) {
+ return (
+
+
+
fileInputRef.current?.click()}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ fileInputRef.current?.click();
+ }
+ }}
+ >
+ {coverImage ? (
+
+

+ {isUploading && (
+
+
+
Uploading... {Math.round(uploadProgress)}%
+
+ )}
+
+ ) : (
+
+
📷
+
Click or press Enter to upload a cover image
+
PNG, JPG, GIF up to 10MB
+
+ )}
+
+ {uploadError &&
❌ {uploadError}
}
+
+
+ );
+}
+
+interface EditorSectionProps {
+ editorRef: React.RefObject;
+ initialContent?: string;
+}
+
+function EditorSection({ editorRef, initialContent }: EditorSectionProps) {
+ return (
+
+ );
+}
+
+interface ActionButtonsProps {
+ onCancel: () => void;
+ onSubmit: () => void;
+ disabled: boolean;
+ submitLabel: string;
+}
+
+function ActionButtons({ onCancel, onSubmit, disabled, submitLabel }: ActionButtonsProps) {
+ return (
+
+
+
+
+ );
+}
+
+interface ErrorStateProps {
+ message: string;
+}
+
+function ErrorState({ message }: ErrorStateProps) {
+ return {message}
;
+}
+
+function LoadingState() {
+ return Loading post...
;
+}
+
+function PageHeader() {
+ return (
+
+ );
+}
+
function EditPost() {
const { postId } = useParams<{ postId: string }>();
const navigate = useNavigate();
@@ -14,19 +315,23 @@ function EditPost() {
const fileInputRef = useRef(null);
const { toasts, removeToast, success, error } = useToast();
- const [isLoading, setIsLoading] = useState(true);
- const [loadError, setLoadError] = useState(null);
-
const [title, setTitle] = useState('');
- const [initialContent, setInitialContent] = useState(undefined);
+ const [initialContent, setInitialContent] = useState();
const [coverImage, setCoverImage] = useState(null);
- const [originalCoverImageKey, setOriginalCoverImageKey] = useState('');
+ const [originalCoverImageKey, setOriginalCoverImageKey] = useState('');
const [newCoverImageKey, setNewCoverImageKey] = useState(null);
- const [uploadProgress, setUploadProgress] = useState(0);
- const [isUploading, setIsUploading] = useState(false);
- const [uploadError, setUploadError] = useState('');
+ const [isLoading, setIsLoading] = useState(true);
+ const [loadError, setLoadError] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ const [isUploading, setIsUploading] = useState(false);
+ const [uploadProgress, setUploadProgress] = useState(0);
+ const [uploadError, setUploadError] = useState('');
+
+ const effectiveCoverImageKey = useMemo(
+ () => newCoverImageKey ?? originalCoverImageKey,
+ [newCoverImageKey, originalCoverImageKey],
+ );
const loadPost = useCallback(async () => {
if (!postId) {
@@ -43,9 +348,7 @@ function EditPost() {
setTitle(fetched.title);
setInitialContent(fetched.content);
setCoverImage(fetched.coverImageUrl ?? null);
-
- const fetchedCoverKey = fetched.coverImageKey ?? '';
- setOriginalCoverImageKey(fetchedCoverKey);
+ setOriginalCoverImageKey(fetched.coverImageKey ?? '');
setNewCoverImageKey(null);
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to load post.';
@@ -60,46 +363,49 @@ function EditPost() {
void loadPost();
}, [loadPost]);
- const handleCoverImageChange = async (event: React.ChangeEvent) => {
- const file = event.target.files?.[0];
- if (!file) {
- return;
- }
+ const handleCoverImageChange = useCallback(
+ async (event: ChangeEvent) => {
+ const file = event.target.files?.[0];
+ if (!file) {
+ return;
+ }
- if (!file.type.startsWith('image/')) {
- setUploadError('Please select an image 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);
+ const reader = new FileReader();
+ reader.onload = (readerEvent) => {
+ setCoverImage(readerEvent.target?.result as string);
+ };
+ reader.readAsDataURL(file);
- setIsUploading(true);
- setUploadError('');
- setUploadProgress(0);
+ setIsUploading(true);
+ setUploadError('');
+ setUploadProgress(0);
- try {
- const { fileKey, url } = await uploadImage(file, (progress) => {
- setUploadProgress(progress);
- });
+ try {
+ const { fileKey, url } = await uploadImage(file, (progress) => {
+ setUploadProgress(progress);
+ });
- setNewCoverImageKey(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);
- }
- };
+ setNewCoverImageKey(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);
+ }
+ },
+ [error, success],
+ );
- const handleSubmit = async () => {
+ const handleSubmit = useCallback(async () => {
if (!postId) {
error('Invalid post identifier.');
return;
@@ -116,24 +422,20 @@ function EditPost() {
}
const content = editorRef.current.getEditorState();
-
setIsSubmitting(true);
try {
- const coverKeyToSend = newCoverImageKey ?? originalCoverImageKey;
-
const payload: Parameters[0] = {
postId,
title: title.trim(),
content,
};
- if (coverKeyToSend) {
- payload.coverImageKey = coverKeyToSend;
+ if (effectiveCoverImageKey) {
+ payload.coverImageKey = effectiveCoverImageKey;
}
await updateBlogPost(payload);
-
success('Blog post updated successfully!', 2000);
setTimeout(() => {
@@ -145,292 +447,37 @@ function EditPost() {
} finally {
setIsSubmitting(false);
}
- };
+ }, [effectiveCoverImageKey, error, navigate, postId, success, title]);
+
+ const handleCancel = useCallback(() => {
+ navigate(postId ? `/blog/${postId}` : '/blog');
+ }, [navigate, postId]);
return (
-
- {isLoading ? (
-
Loading post...
- ) : loadError ? (
-
- {loadError}
-
- ) : (
+
+ {isLoading &&
}
+ {!isLoading && loadError &&
}
+ {!isLoading && !loadError && (
<>
-
-
- Edit Post
-
-
- Update your article content and cover image
-
-
-
-
-
- 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)';
- }}
- />
-
-
-
-
-
-
fileInputRef.current?.click()}
- onMouseEnter={(e) => {
- e.currentTarget.style.borderColor = 'var(--accent-color)';
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.borderColor = 'var(--border-color)';
- }}
- >
- {coverImage ? (
-
-

- {isUploading && (
-
-
-
- Uploading... {Math.round(uploadProgress)}%
-
-
- )}
-
- ) : (
-
-
📷
-
- Click to upload cover image
-
-
- PNG, JPG, GIF up to 10MB
-
-
- )}
-
-
- {uploadError && (
-
- ❌ {uploadError}
-
- )}
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
>
)}