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.
153 lines
4.4 KiB
TypeScript
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}`)
|
|
}
|