Files
gg-backend/frontend/src/pages/BlogEditor.tsx
T
2026-04-16 16:38:16 +05:30

201 lines
4.3 KiB
TypeScript

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 {
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";
export default function BlogEditorPage() {
const {id} = useParams();
const navigate = useNavigate();
const editorRef = useRef<EditorJS | null>(null);
const hasInitialized = useRef(false);
const hasRenderedContent = useRef(false);
const [title, setTitle] = useState("");
const [writer, setWriter] = useState("");
const [coverImage, setCoverImage] = useState("");
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({
holder: "editorjs",
placeholder: "Write blog content...",
tools: {
header: {
class: Header,
inlineToolbar: true,
config: {
placeholder: "Enter heading",
levels: [1, 2, 3, 4],
defaultLevel: 2,
},
},
list: {
class: List,
inlineToolbar: true,
config: {
defaultStyle: "unordered",
},
},
quote: Quote,
table: Table,
code: CodeTool,
embed: Embed,
delimiter: Delimiter,
image: {
class: ImageTool,
config: {
uploader: {
uploadByFile: async (file: File) => {
const res = await uploadImageApi(file);
return {
success: 1,
file: {url: res.file.url},
};
},
},
},
},
},
});
await editor.isReady;
editorRef.current = editor;
if (isEdit && id && !hasRenderedContent.current) {
try {
const res = await getBlogByIdApi(Number(id));
setTitle(res.title);
setWriter(res.writer);
setCoverImage(res.image || "");
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);
}
navigate("/blog");
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
return (
<div className="p-6 max-w-5xl mx-auto space-y-6">
<Card>
<CardHeader>
<CardTitle>{isEdit ? "Edit Blog" : "Create Blog"}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Input
placeholder="Blog Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<Input
placeholder="Writer Name"
value={writer}
onChange={(e) => setWriter(e.target.value)}
/>
<div className="space-y-2">
<label className="text-sm font-medium">Cover Image</label>
<BytescaleUploader
value={coverImage}
folderPath="/blog"
onChange={(url) => setCoverImage(url)}
/>
{coverImage && (
<img
src={coverImage}
alt="cover"
className="w-full max-h-[250px] object-cover rounded-md"
/>
)}
</div>
<div
id="editorjs"
className="border rounded-md p-4 bg-white min-h-[300px]"
/>
<Button onClick={handleSave} disabled={loading}>
{loading ? "Saving..." : "Save Blog"}
</Button>
</CardContent>
</Card>
</div>
);
}