Files
gg-backend/frontend/src/pages/BlogEditor.tsx
T

203 lines
5.0 KiB
TypeScript
Raw Normal View History

2026-05-26 15:48:01 +05:30
import { useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader';
import EditorJS, { OutputData } from '@editorjs/editorjs';
import Header from '@editorjs/header';
import List from '@editorjs/list';
import ImageTool from '@editorjs/image';
import Quote from '@editorjs/quote';
import Table from '@editorjs/table';
import CodeTool from '@editorjs/code';
import Embed from '@editorjs/embed';
import Delimiter from '@editorjs/delimiter';
import axios from 'axios';
import { createBlogApi, updateBlogApi, getBlogByIdApi, uploadImageApi } from '@/api/blog';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
2026-03-18 14:25:08 +05:30
export default function BlogEditorPage() {
2026-05-13 17:16:25 +05:30
const baseURL = import.meta.env.VITE_API_URL;
2026-05-26 15:48:01 +05:30
const { id } = useParams();
2026-03-18 14:25:08 +05:30
const navigate = useNavigate();
const editorRef = useRef<EditorJS | null>(null);
const hasInitialized = useRef(false);
const hasRenderedContent = useRef(false);
2026-05-26 15:48:01 +05:30
const [title, setTitle] = useState('');
const [writer, setWriter] = useState('');
const [coverImage, setCoverImage] = useState('');
2026-03-18 14:25:08 +05:30
const [loading, setLoading] = useState(false);
const isEdit = Boolean(id);
useEffect(() => {
if (hasInitialized.current) return;
hasInitialized.current = true;
let editor: EditorJS;
const initEditor = async () => {
editor = new EditorJS({
2026-05-26 15:48:01 +05:30
holder: 'editorjs',
2026-03-18 14:25:08 +05:30
2026-05-26 15:48:01 +05:30
placeholder: 'Write blog content...',
2026-03-18 14:25:08 +05:30
tools: {
header: {
class: Header,
inlineToolbar: true,
config: {
2026-05-26 15:48:01 +05:30
placeholder: 'Enter heading',
2026-03-18 14:25:08 +05:30
levels: [1, 2, 3, 4],
defaultLevel: 2,
},
},
list: {
class: List,
inlineToolbar: true,
config: {
2026-05-26 15:48:01 +05:30
defaultStyle: 'unordered',
2026-03-18 14:25:08 +05:30
},
},
quote: Quote,
table: Table,
code: CodeTool,
embed: Embed,
delimiter: Delimiter,
image: {
class: ImageTool,
config: {
uploader: {
uploadByFile: async (file: File) => {
2026-05-13 17:16:25 +05:30
if (file.size > 5 * 1024 * 1024) {
2026-05-26 15:48:01 +05:30
alert('File is too large (Max 5MB)');
return { success: 0, file: { url: '' } };
2026-05-13 17:16:25 +05:30
}
const formData = new FormData();
2026-05-26 15:48:01 +05:30
formData.append('file', file);
formData.append('folderPath', '/blog');
2026-05-13 17:16:25 +05:30
try {
2026-05-26 15:48:01 +05:30
const response = await axios.post(`${baseURL}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
2026-05-13 17:16:25 +05:30
},
2026-05-26 15:48:01 +05:30
});
2026-05-13 17:16:25 +05:30
return {
success: 1,
2026-05-26 15:48:01 +05:30
file: { url: response.data.fileUrl },
2026-05-13 17:16:25 +05:30
};
} catch (e: any) {
2026-05-26 15:48:01 +05:30
console.error('EditorJS Image Upload Error:', e);
const errorMessage = e.response?.data?.error || e.message || 'Upload failed';
2026-05-13 17:16:25 +05:30
alert(`Upload Error: ${errorMessage}`);
return {
success: 0,
2026-05-26 15:48:01 +05:30
file: { url: '' },
2026-05-13 17:16:25 +05:30
};
}
2026-03-18 14:25:08 +05:30
},
},
},
},
},
});
await editor.isReady;
editorRef.current = editor;
if (isEdit && id && !hasRenderedContent.current) {
try {
const res = await getBlogByIdApi(Number(id));
setTitle(res.title);
setWriter(res.writer);
2026-05-26 15:48:01 +05:30
setCoverImage(res.image || '');
2026-03-18 14:25:08 +05:30
if (res.content) {
await editor.blocks.clear();
await editor.render(res.content);
hasRenderedContent.current = true;
}
} catch (err) {
console.error(err);
}
}
};
initEditor();
}, [id, isEdit]);
const handleSave = async () => {
if (!editorRef.current) return;
setLoading(true);
try {
const content: OutputData = await editorRef.current.save();
const payload = {
title,
writer,
image: coverImage,
content,
isActive: true,
};
if (isEdit) {
await updateBlogApi(Number(id), payload);
} else {
await createBlogApi(payload);
}
2026-05-26 15:48:01 +05:30
navigate('/blog');
2026-03-18 14:25:08 +05:30
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
return (
<div className="p-6 max-w-5xl mx-auto space-y-6">
<Card>
<CardHeader>
2026-05-26 15:48:01 +05:30
<CardTitle>{isEdit ? 'Edit Blog' : 'Create Blog'}</CardTitle>
2026-03-18 14:25:08 +05:30
</CardHeader>
<CardContent className="space-y-4">
2026-05-26 15:48:01 +05:30
<Input placeholder="Blog Title" value={title} onChange={(e) => setTitle(e.target.value)} />
<Input placeholder="Writer Name" value={writer} onChange={(e) => setWriter(e.target.value)} />
2026-03-18 14:25:08 +05:30
<div className="space-y-2">
<label className="text-sm font-medium">Cover Image</label>
2026-05-26 15:48:01 +05:30
<BytescaleUploader value={coverImage} folderPath="/blog" onChange={(url) => setCoverImage(url)} />
2026-03-18 14:25:08 +05:30
{coverImage && (
2026-05-26 15:48:01 +05:30
<img src={coverImage} alt="cover" className="w-full max-h-[250px] object-cover rounded-md" />
2026-03-18 14:25:08 +05:30
)}
</div>
2026-05-26 15:48:01 +05:30
<div id="editorjs" className="border rounded-md p-4 bg-white min-h-[300px]" />
2026-03-18 14:25:08 +05:30
<Button onClick={handleSave} disabled={loading}>
2026-05-26 15:48:01 +05:30
{loading ? 'Saving...' : 'Save Blog'}
2026-03-18 14:25:08 +05:30
</Button>
</CardContent>
</Card>
</div>
);
}