Files
www.cialloo.com/src/blog/editor/BlogImagePlugin.tsx
cialloo 4417423612
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 18s
feat(blog): Implement blog module with post management, image upload workflow, and localization
- 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.
2025-10-25 13:58:15 +08:00

153 lines
4.4 KiB
TypeScript

/**
* 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}`)
}