187 lines
6.0 KiB
JavaScript
187 lines
6.0 KiB
JavaScript
import imageCompression from 'browser-image-compression'
|
|
import Quill from 'quill'
|
|
import 'quill/dist/quill.snow.css'
|
|
import QuillMarkdown from 'quilljs-markdown'
|
|
import { useCallback, useEffect, useRef } from 'react'
|
|
import { useError } from '../../service/ErrorProvider'
|
|
import { resolvePhotoURL } from '../../utils/Helpers'
|
|
import { UploadFile } from '../../utils/TokenManager'
|
|
import './RichTextEditor.css'
|
|
|
|
const RichTextEditor = ({
|
|
value = '',
|
|
onChange,
|
|
isEditable = true,
|
|
variant = 'outlined',
|
|
entityId,
|
|
entityType,
|
|
}) => {
|
|
const { showError } = useError()
|
|
const quillRef = useRef(null)
|
|
const editorRef = useRef(null)
|
|
|
|
// Image upload handler - wrapped in useCallback to avoid recreating on every render
|
|
const handleImageUpload = useCallback(() => {
|
|
const input = document.createElement('input')
|
|
input.setAttribute('type', 'file')
|
|
input.setAttribute('accept', 'image/*')
|
|
input.click()
|
|
input.onchange = async () => {
|
|
const file = input.files[0]
|
|
if (!file) return
|
|
|
|
try {
|
|
// Define compression options based on entity type ( this need a revist later)
|
|
const compressionOptions = {
|
|
maxSizeMB: entityType === 'profile' ? 0.5 : 1, // Smaller size for profile images
|
|
maxWidthOrHeight: entityType === 'profile' ? 320 : 1200, // Smaller dimensions for profile images
|
|
useWebWorker: true,
|
|
fileType: 'image/jpeg',
|
|
}
|
|
|
|
// Compress the image
|
|
const compressedFile = await imageCompression(file, compressionOptions)
|
|
|
|
// Create new file with .jpg extension to ensure it's treated as JPEG
|
|
const compressedJpegFile = new File(
|
|
[compressedFile],
|
|
`${file.name.split('.')[0]}.jpg`,
|
|
{ type: 'image/jpeg' },
|
|
)
|
|
|
|
console.log(`Original size: ${(file.size / 1024 / 1024).toFixed(2)} MB`)
|
|
console.log(
|
|
`Compressed size: ${(compressedJpegFile.size / 1024 / 1024).toFixed(2)} MB`,
|
|
)
|
|
|
|
// Upload compressed image to backend
|
|
const formData = new FormData()
|
|
formData.append('file', compressedJpegFile)
|
|
formData.append('entityId', entityId)
|
|
formData.append('entityType', entityType)
|
|
|
|
const response = await UploadFile('/assets/chore', {
|
|
method: 'POST',
|
|
body: formData,
|
|
})
|
|
|
|
if (response.status === 507) {
|
|
showError({
|
|
title: 'Storage Quota Exceeded',
|
|
message: 'You have exceeded your quota for uploading files.',
|
|
})
|
|
return
|
|
} else if (response.status === 413) {
|
|
showError({
|
|
title: 'File Too Large',
|
|
message: 'The file you are trying to upload is too large.',
|
|
})
|
|
return
|
|
} else if (!response.ok) {
|
|
showError({
|
|
title: 'Upload Failed',
|
|
message: 'Failed to upload image.',
|
|
})
|
|
return
|
|
}
|
|
const data = await response.json()
|
|
const url = resolvePhotoURL(data.url || data.sign)
|
|
// Insert image into Quill
|
|
const quill = editorRef.current
|
|
const range = quill.getSelection()
|
|
quill.insertEmbed(range ? range.index : 0, 'image', url)
|
|
} catch (error) {
|
|
console.error('Error during image processing or upload:', error)
|
|
showError({
|
|
title: 'Upload Failed',
|
|
message: 'An error occurred while processing the image.',
|
|
})
|
|
}
|
|
}
|
|
}, [entityId, entityType, showError]) // Dependencies for useCallback
|
|
|
|
useEffect(() => {
|
|
if (!quillRef.current) return
|
|
if (!editorRef.current && isEditable) {
|
|
editorRef.current = new Quill(quillRef.current, {
|
|
theme: variant === 'bubble' ? 'bubble' : 'snow',
|
|
modules: {
|
|
toolbar: {
|
|
container: [
|
|
[{ header: [1, 2, 3, 4, false] }],
|
|
['bold', 'italic', 'underline', 'strike'],
|
|
['blockquote', 'code-block'],
|
|
[{ list: 'ordered' }, { list: 'bullet' }],
|
|
['link', 'image'],
|
|
['clean'],
|
|
],
|
|
handlers: {
|
|
image: handleImageUpload,
|
|
},
|
|
},
|
|
},
|
|
placeholder: 'Enter description...',
|
|
})
|
|
new QuillMarkdown(editorRef.current, {})
|
|
editorRef.current.root.innerHTML = value
|
|
editorRef.current.on('text-change', () => {
|
|
if (onChange) {
|
|
onChange(editorRef.current.root.innerHTML)
|
|
}
|
|
})
|
|
}
|
|
// If switching to read-only mode, disable Quill instance
|
|
if (editorRef.current && !isEditable) {
|
|
// editorRef.current.disable()
|
|
editorRef.current.readOnly = true
|
|
|
|
// If switching back to editable, enable Quill
|
|
if (editorRef.current && isEditable) {
|
|
// editorRef.current.enable()
|
|
editorRef.current.readOnly = false
|
|
}
|
|
}
|
|
}, [onChange, value, isEditable, variant, handleImageUpload]) // Added handleImageUpload to dependency array
|
|
|
|
useEffect(() => {
|
|
if (editorRef.current && isEditable) {
|
|
if (editorRef.current.root.innerHTML !== value) {
|
|
editorRef.current.root.innerHTML = value || ''
|
|
}
|
|
}
|
|
}, [value, isEditable])
|
|
|
|
if (!isEditable) {
|
|
// Display-only mode: render HTML
|
|
return (
|
|
<div
|
|
className='editor-view-mode'
|
|
style={{
|
|
minHeight: 120,
|
|
overflow: 'scroll',
|
|
// border:
|
|
// '1px solid var(--joy-palette-neutral-outlinedBorder, #DDE7EE)',
|
|
borderRadius: 8,
|
|
padding: 16,
|
|
background: 'var(--joy-palette-background-surface, #fff)',
|
|
color: 'var(--joy-palette-text-primary, #1A2027)',
|
|
fontFamily:
|
|
'var(--joy-fontFamily-body, Inter, system-ui, Avenir, Helvetica, Arial, sans-serif)',
|
|
fontSize: 16,
|
|
boxShadow:
|
|
'var(--joy-shadow-xs, 0px 1px 2px 0px rgba(16, 24, 40, 0.05))',
|
|
}}
|
|
dangerouslySetInnerHTML={{ __html: value }}
|
|
/>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className={`quill-root quill-variant-${variant}`}>
|
|
<div ref={quillRef} style={{ minHeight: 120 }} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default RichTextEditor
|