fix:fix in the blog editor #12

Merged
kailasdevdas merged 2 commits from fix/blog-view into dev 2026-04-16 11:17:23 +00:00
5 changed files with 182 additions and 60 deletions
Showing only changes of commit 5cf73a6351 - Show all commits
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

@@ -6,7 +6,7 @@ import axios from "axios";
interface BytescaleUploaderProps { interface BytescaleUploaderProps {
value: string; value: string;
onChange: (url: string) => void; onChange: (url: string) => void;
folderPath: "/doctors" | "/departments" | "/news"; folderPath: "/doctors" | "/departments" | "/news" | "/blog";
} }
export function BytescaleUploader({ export function BytescaleUploader({
+159 -29
View File
@@ -1,72 +1,202 @@
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import {useParams, useNavigate} from "react-router-dom"; import {useParams, useNavigate} from "react-router-dom";
import axios from "axios";
import {Button} from "@/components/ui/button"; import {Button} from "@/components/ui/button";
import {Card, CardContent} from "@/components/ui/card";
import {getBlogByIdApi} from "@/api/blog"; import {getBlogByIdApi} from "@/api/blog";
/* ---------------- LIST RENDERER ---------------- */
const renderList = (items, style) => {
// ✅ Checklist
if (style === "checklist") {
return (
<div className="mb-4 space-y-2">
{items.map((item, i) => (
<div key={i} className="flex items-center gap-2">
<input
type="checkbox"
checked={item.meta?.checked || false}
readOnly
/>
<span
className={item.meta?.checked ? "line-through opacity-60" : ""}
dangerouslySetInnerHTML={{
__html: item.content || "",
}}
/>
</div>
))}
</div>
);
}
// ✅ Ordered / Unordered
const ListTag = style === "ordered" ? "ol" : "ul";
return (
<ListTag
className={`pl-6 mb-4 ${
style === "ordered" ? "list-decimal" : "list-disc"
}`}
>
{items
.filter((item) => item.content)
.map((item, i) => (
<li key={i}>
<span
dangerouslySetInnerHTML={{
__html: item.content,
}}
/>
{item.items?.length > 0 && renderList(item.items, style)}
</li>
))}
</ListTag>
);
};
/* ---------------- BLOCK RENDERER ---------------- */
const renderBlock = (block, index) => {
switch (block.type) {
case "paragraph":
return (
<p
key={index}
className="mb-4 leading-7 text-gray-800"
dangerouslySetInnerHTML={{__html: block.data.text}}
/>
);
case "header":
return (
<h2
key={index}
className="text-2xl font-semibold mb-4"
dangerouslySetInnerHTML={{__html: block.data.text}}
/>
);
case "image":
return (
<img
key={index}
src={block.data.file?.url}
alt={block.data.caption || "blog-image"}
className="w-full rounded-lg mb-4"
/>
);
case "list":
return (
<div key={index}>{renderList(block.data.items, block.data.style)}</div>
);
case "quote":
return (
<blockquote
key={index}
className="border-l-4 border-gray-300 pl-4 italic my-4 text-gray-600"
>
{block.data.text}
</blockquote>
);
case "code":
return (
<pre
key={index}
className="bg-gray-100 p-4 rounded-md overflow-x-auto text-sm mb-4"
>
<code>{block.data.code}</code>
</pre>
);
case "table":
return (
<div key={index} className="overflow-x-auto mb-6">
<table className="w-full border border-gray-200">
<tbody>
{block.data.content.map((row, i) => (
<tr key={i}>
{row.map((cell, j) => (
<td
key={j}
className="border px-3 py-2"
dangerouslySetInnerHTML={{
__html: cell,
}}
/>
))}
</tr>
))}
</tbody>
</table>
</div>
);
case "delimiter":
return <hr key={index} className="my-6 border-gray-300" />;
default:
return null;
}
};
/* ---------------- MAIN COMPONENT ---------------- */
const BlogDetail = () => { const BlogDetail = () => {
const {id} = useParams(); const {id} = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const [blog, setBlog] = useState<any>(null); const [blog, setBlog] = useState(null);
const fetchBlogById = async () => { const fetchBlog = async () => {
try { try {
const response = await getBlogByIdApi(Number(id)); const res = await getBlogByIdApi(Number(id));
console.log({res});
setBlog(response); setBlog(res);
} catch (error) { } catch (err) {
console.error("Error fetching blog", error); console.error("Error fetching blog", err);
} }
}; };
useEffect(() => { useEffect(() => {
fetchBlogById(); fetchBlog();
}, [id]); // ✅ FIXED dependency }, [id]);
if (!blog) { if (!blog) {
return <div className="mt-40 text-center text-gray-500">Loading...</div>; return <p className="mt-40 text-center">Loading...</p>;
} }
return ( return (
<div className="mx-auto flex flex-col "> <div className="mx-auto flex flex-col ">
<Card> {/* Back Button */}
<CardContent className="p-6 space-y-4">
{/* Back */}
<Button <Button
variant="ghost" variant="ghost"
className="text-black-600 p-0" className="mb-4 w-fit text-black px-0"
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
> >
Back Back
</Button> </Button>
{/* Title */} {/* Title */}
<h1 className="text-2xl md:text-4xl lg:text-5xl font-bold"> <h1 className="text-3xl md:text-5xl font-bold mb-2">{blog.title}</h1>
{blog.title}
</h1>
{/* Meta */} {/* Meta */}
<p className="text-gray-500 text-sm"> <p className="text-gray-500 mb-4">
{blog.writer} {new Date(blog.createdAt).toLocaleDateString()} {blog.writer} {new Date(blog.createdAt).toLocaleDateString()}
</p> </p>
{/* Image */} {/* Image (only if exists) */}
{blog.image?.trim() && (
<img <img
src={blog.image} src={blog.image}
alt={blog.title} alt="blog"
className="w-full h-[220px] md:h-[400px] object-cover rounded-md" className="w-full h-[220px] md:h-[400px] object-cover rounded-lg mb-6"
/> />
)}
{/* Content */} {/* Content */}
<div className="space-y-3 text-gray-800 leading-relaxed"> <div>
{blog.content?.blocks?.map((block: any, index: number) => ( {blog.content?.blocks?.map((block, index) => renderBlock(block, index))}
<p key={index}>{block.data?.text}</p>
))}
</div> </div>
</CardContent>
</Card>
</div> </div>
); );
}; };
+6 -14
View File
@@ -1,6 +1,6 @@
import {useEffect, useRef, useState} from "react"; import {useEffect, useRef, useState} from "react";
import {useNavigate, useParams} from "react-router-dom"; import {useNavigate, useParams} from "react-router-dom";
import {BytescaleUploader} from "@/components/BytescaleUploader/BytescaleUploader";
import EditorJS, {OutputData} from "@editorjs/editorjs"; import EditorJS, {OutputData} from "@editorjs/editorjs";
import Header from "@editorjs/header"; import Header from "@editorjs/header";
import List from "@editorjs/list"; import List from "@editorjs/list";
@@ -117,18 +117,6 @@ export default function BlogEditorPage() {
initEditor(); initEditor();
}, [id, isEdit]); }, [id, isEdit]);
const handleCoverUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
try {
const res = await uploadImageApi(file);
setCoverImage(res.file.url);
} catch (err) {
console.error(err);
}
};
const handleSave = async () => { const handleSave = async () => {
if (!editorRef.current) return; if (!editorRef.current) return;
@@ -182,7 +170,11 @@ export default function BlogEditorPage() {
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Cover Image</label> <label className="text-sm font-medium">Cover Image</label>
<Input type="file" onChange={handleCoverUpload} /> <BytescaleUploader
value={coverImage}
folderPath="/blog"
onChange={(url) => setCoverImage(url)}
/>
{coverImage && ( {coverImage && (
<img <img