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 ? ( +
+ Cover + {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 ( +
+

Edit Post

+

Update your article content and cover image

+
+ ); +} + 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 ? ( -
- Cover - {isUploading && ( -
-
-
-
-
- Uploading... {Math.round(uploadProgress)}% -
-
- )} -
- ) : ( -
-
📷
-

- Click to upload cover image -

-

- PNG, JPG, GIF up to 10MB -

-
- )} -
- - {uploadError && ( -
- ❌ {uploadError} -
- )} - - -
- -
- -
- -
-
- -
- - -
+ + + + + )}