blog-editor-cms
Version:
A comprehensive blog editor CMS package with JSON-server backend
193 lines (192 loc) • 8.46 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useState, useEffect, useRef, useCallback } from 'react';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
import { apiClient } from '../../../shared/api/client';
import { validateImageFile, fileToBase64 } from '../../../shared/utils';
import './styles.css';
// Custom image upload handler module
class ImageUploadHandler {
static register() {
const BaseImageFormat = Quill.import('formats/image');
class CustomImageFormat extends BaseImageFormat {
static create(value) {
const node = super.create(value);
node.setAttribute('data-custom', 'true');
return node;
}
}
Quill.register(CustomImageFormat, true);
}
}
const BlogEditor = ({ config = {}, clientIP = '', onSave, onError, className = '', initialContent, onContentChange, }) => {
const [title, setTitle] = useState(initialContent?.title || '');
const [error, setError] = useState(null);
const [isSaving, setIsSaving] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const quillRef = useRef(null);
const quillInstance = useRef(null);
// Initialize Quill editor
useEffect(() => {
if (!quillRef.current || quillInstance.current)
return;
// Check IP access if IP restrictions are configured
if (config.allowedIPs && config.allowedIPs?.length > 0 && clientIP) {
// const hasAccess = checkIPAccess(clientIP, config.allowedIPs);
// if (!hasAccess) {
// setError('Access denied: Your IP is not authorized');
// setIsLoading(false);
// return;
// }
}
try {
// Register custom modules
ImageUploadHandler.register();
quillInstance.current = new Quill(quillRef.current, {
theme: 'snow',
modules: {
toolbar: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ color: [] }, { background: [] }],
[{ align: [] }],
[{ list: 'ordered' }, { list: 'bullet' }],
['blockquote', 'code-block'],
['link', 'image', 'video'],
['clean'],
],
history: {
delay: 2000,
maxStack: 100,
userOnly: true,
},
clipboard: {
matchVisual: false,
},
},
placeholder: 'Start writing your blog post...',
formats: ['header', 'bold', 'italic', 'underline', 'strike', 'blockquote', 'code-block', 'link', 'image', 'video'],
});
// Set initial content if provided
if (initialContent?.content) {
quillInstance.current.clipboard.dangerouslyPasteHTML(initialContent.content);
}
// Add content change handler
quillInstance.current.on('text-change', () => {
if (onContentChange && quillInstance.current) {
onContentChange({
title,
content: quillInstance.current.root.innerHTML,
});
}
});
// Custom image handler
const toolbar = quillInstance.current.getModule('toolbar');
toolbar.addHandler('image', () => handleImageButtonClick());
setIsLoading(false);
}
catch (err) {
handleError(err, 'Failed to initialize editor');
setIsLoading(false);
}
return () => {
quillInstance.current = null;
};
}, [clientIP, config.allowedIPs]);
const handleImageButtonClick = useCallback(async () => {
if (!quillInstance.current)
return;
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 {
// Validate image
const validation = validateImageFile(file, config);
if (!validation.valid) {
throw new Error(validation.error);
}
// Convert to base64
const base64Image = await fileToBase64(file);
const base64Data = base64Image.split(',')[1];
// Upload image
const savedImage = await apiClient.uploadImage(file.name, base64Data);
const imageUrl = `data:${savedImage.type};base64,${savedImage.base64Data}`;
// Insert into editor
const range = quillInstance.current?.getSelection();
if (range) {
quillInstance.current?.insertEmbed(range.index, 'image', imageUrl);
quillInstance.current?.setSelection(range.index + 1, 0);
}
}
catch (err) {
handleError(err, 'Failed to upload image');
}
};
}, [config]);
const handleSave = useCallback(async () => {
if (!quillInstance.current)
return;
setIsSaving(true);
setError(null);
try {
if (!title.trim()) {
throw new Error('Title is required');
}
const content = {
title: title.trim(),
content: quillInstance.current.root.innerHTML,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
published: initialContent?.published || false,
tags: initialContent?.tags || [],
excerpt: initialContent?.excerpt || '',
};
let savedPost;
if (initialContent?.id) {
// Update existing post
savedPost = await apiClient.updatePost(initialContent.id, content);
}
else {
// Create new post
savedPost = await apiClient.addPost(content);
}
// Callbacks
onSave?.(savedPost);
}
catch (err) {
handleError(err, 'Failed to save post');
}
finally {
setIsSaving(false);
}
}, [title, initialContent, onSave]);
const handleError = useCallback((error, defaultMessage) => {
const message = error.message || defaultMessage;
setError(message);
onError?.({
message,
details: error
});
}, [onError]);
if (!isLoading && error) {
return (_jsx("div", { className: `blog-editor-error ${className}`, role: "alert", children: error }));
}
// if (isLoading) {
// return <div className={`blog-editor-loading ${className}`}>Loading editor...</div>;
// }
return (_jsxs("div", { className: `blog-editor-container ${className}`, children: [error && (_jsx("div", { className: "blog-editor-error", role: "alert", children: error })), _jsx("input", { type: "text", value: title, onChange: (e) => {
setTitle(e.target.value);
if (onContentChange && quillInstance.current) {
onContentChange({
title: e.target.value,
content: quillInstance.current.root.innerHTML,
});
}
}, placeholder: "Blog post title", className: "blog-editor-title", "aria-label": "Blog post title" }), _jsx("div", { className: "blog-editor-wrapper", children: _jsx("div", { ref: quillRef, className: "blog-editor", "aria-label": "Blog post content editor" }) }), _jsx("div", { className: "blog-editor-actions", children: _jsx("button", { onClick: handleSave, disabled: isSaving, className: "blog-editor-save-button", "aria-label": "Save blog post", children: isSaving ? 'Saving...' : initialContent?.id ? 'Update Post' : 'Save Post' }) })] }));
};
export default BlogEditor;