2026-03-18 14:25:08 +05:30
|
|
|
import {useEffect, useRef, useState} from "react";
|
|
|
|
|
import {useNavigate, useParams} from "react-router-dom";
|
2026-04-16 16:38:16 +05:30
|
|
|
import {BytescaleUploader} from "@/components/BytescaleUploader/BytescaleUploader";
|
2026-03-18 14:25:08 +05:30
|
|
|
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";
|
2026-05-13 17:16:25 +05:30
|
|
|
import axios from "axios";
|
2026-03-18 14:25:08 +05:30
|
|
|
|
|
|
|
|
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() {
|
2026-05-13 17:16:25 +05:30
|
|
|
const baseURL = import.meta.env.VITE_API_URL;
|
2026-03-18 14:25:08 +05:30
|
|
|
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) => {
|
2026-05-13 17:16:25 +05:30
|
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
|
|
|
alert("File is too large (Max 5MB)");
|
|
|
|
|
return {success: 0, file: {url: ""}};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append("file", file);
|
|
|
|
|
formData.append("folderPath", "/blog");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await axios.post(
|
|
|
|
|
`${baseURL}/upload`,
|
|
|
|
|
formData,
|
|
|
|
|
{
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "multipart/form-data",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: 1,
|
|
|
|
|
file: {url: response.data.fileUrl},
|
|
|
|
|
};
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
console.error("EditorJS Image Upload Error:", e);
|
|
|
|
|
const errorMessage =
|
|
|
|
|
e.response?.data?.error || e.message || "Upload failed";
|
|
|
|
|
alert(`Upload Error: ${errorMessage}`);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: 0,
|
|
|
|
|
file: {url: ""},
|
|
|
|
|
};
|
|
|
|
|
}
|
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);
|
|
|
|
|
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>
|
|
|
|
|
|
2026-04-16 16:38:16 +05:30
|
|
|
<BytescaleUploader
|
|
|
|
|
value={coverImage}
|
|
|
|
|
folderPath="/blog"
|
|
|
|
|
onChange={(url) => setCoverImage(url)}
|
|
|
|
|
/>
|
2026-03-18 14:25:08 +05:30
|
|
|
|
|
|
|
|
{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>
|
|
|
|
|
);
|
|
|
|
|
}
|