feat(blog): Implement blog module with post management, image upload workflow, and localization
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 18s
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 18s
- Added S3 image upload workflow documentation. - Created custom hooks for managing blog posts, post details, and tags. - Developed BlogListPage and BlogPostPage components for displaying posts. - Integrated blog components and hooks into the main application. - Updated localization files to include blog-related strings. - Removed mock blog data and replaced it with dynamic data fetching.
This commit is contained in:
152
src/blog/editor/BlogImagePlugin.tsx
Normal file
152
src/blog/editor/BlogImagePlugin.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Blog-specific Image Plugin
|
||||
* Handles image upload to S3 when images are dragged/pasted into the editor
|
||||
*/
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
|
||||
import type { LexicalCommand } from 'lexical'
|
||||
import { $createImageNode, ImageNode } from '../../editor/nodes/ImageNode'
|
||||
import { uploadFile, getDownloadPresignedURL } from '../api'
|
||||
|
||||
export const INSERT_BLOG_IMAGE_COMMAND: LexicalCommand<File> = createCommand('INSERT_BLOG_IMAGE_COMMAND')
|
||||
|
||||
interface UploadingImage {
|
||||
id: string
|
||||
file: File
|
||||
localUrl: string
|
||||
}
|
||||
|
||||
export function BlogImagePlugin() {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [, setUploadingImages] = useState<Map<string, UploadingImage>>(new Map())
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ImageNode])) {
|
||||
throw new Error('BlogImagePlugin: ImageNode not registered on editor')
|
||||
}
|
||||
|
||||
return editor.registerCommand(
|
||||
INSERT_BLOG_IMAGE_COMMAND,
|
||||
(file: File) => {
|
||||
const id = `uploading-${Date.now()}-${Math.random()}`
|
||||
const localUrl = URL.createObjectURL(file)
|
||||
|
||||
// Add to uploading state
|
||||
setUploadingImages(prev => new Map(prev.set(id, { id, file, localUrl })))
|
||||
|
||||
// Insert placeholder image node with loading state
|
||||
editor.update(() => {
|
||||
const imageNode = $createImageNode({
|
||||
src: localUrl,
|
||||
altText: file.name,
|
||||
maxWidth: 800,
|
||||
})
|
||||
$insertNodes([imageNode])
|
||||
})
|
||||
|
||||
// Start upload
|
||||
handleImageUpload(file, id, localUrl)
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR
|
||||
)
|
||||
}, [editor])
|
||||
|
||||
const handleImageUpload = async (file: File, uploadId: string, localUrl: string) => {
|
||||
try {
|
||||
// Upload file and get fileKey
|
||||
const fileKey = await uploadFile(file)
|
||||
|
||||
// Get download URL
|
||||
const { url: downloadUrl } = await getDownloadPresignedURL({ fileKey })
|
||||
|
||||
// Update the image node with the actual S3 URL
|
||||
editor.update(() => {
|
||||
const nodes = editor.getEditorState().read(() => {
|
||||
const allNodes: any[] = []
|
||||
editor.getEditorState()._nodeMap.forEach((node) => {
|
||||
if (node instanceof ImageNode && node.getSrc() === localUrl) {
|
||||
allNodes.push(node)
|
||||
}
|
||||
})
|
||||
return allNodes
|
||||
})
|
||||
|
||||
nodes.forEach(node => {
|
||||
if (node instanceof ImageNode) {
|
||||
const writable = node.getWritable()
|
||||
writable.__src = downloadUrl
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Clean up
|
||||
URL.revokeObjectURL(localUrl)
|
||||
setUploadingImages(prev => {
|
||||
const next = new Map(prev)
|
||||
next.delete(uploadId)
|
||||
return next
|
||||
})
|
||||
|
||||
// Show success notification
|
||||
showNotification('Image uploaded successfully', 'success')
|
||||
} catch (error) {
|
||||
console.error('Failed to upload image:', error)
|
||||
|
||||
// Show error notification
|
||||
showNotification('Failed to upload image', 'error')
|
||||
|
||||
// Clean up
|
||||
URL.revokeObjectURL(localUrl)
|
||||
setUploadingImages(prev => {
|
||||
const next = new Map(prev)
|
||||
next.delete(uploadId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Handle paste events
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
// @ts-ignore - PASTE_COMMAND exists
|
||||
editor.PASTE_COMMAND || 'PASTE_COMMAND',
|
||||
(event: ClipboardEvent) => {
|
||||
const items = event.clipboardData?.items
|
||||
if (!items) return false
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
if (item.type.indexOf('image') !== -1) {
|
||||
event.preventDefault()
|
||||
const file = item.getAsFile()
|
||||
if (file) {
|
||||
editor.dispatchCommand(INSERT_BLOG_IMAGE_COMMAND, file)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR
|
||||
)
|
||||
}, [editor])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Simple notification helper
|
||||
function showNotification(message: string, type: 'success' | 'error') {
|
||||
// You can replace this with your preferred notification library
|
||||
// For now, just using a simple alert-like approach
|
||||
const event = new CustomEvent('app:notification', {
|
||||
detail: { message, type }
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
|
||||
console.log(`[${type.toUpperCase()}] ${message}`)
|
||||
}
|
||||
Reference in New Issue
Block a user