fix:fix in the blog editor
This commit is contained in:
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 {
|
||||
value: string;
|
||||
onChange: (url: string) => void;
|
||||
folderPath: "/doctors" | "/departments" | "/news";
|
||||
folderPath: "/doctors" | "/departments" | "/news" | "/blog";
|
||||
}
|
||||
|
||||
export function BytescaleUploader({
|
||||
|
||||
@@ -1,72 +1,202 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useParams, useNavigate} from "react-router-dom";
|
||||
import axios from "axios";
|
||||
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Card, CardContent} from "@/components/ui/card";
|
||||
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 {id} = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [blog, setBlog] = useState<any>(null);
|
||||
const [blog, setBlog] = useState(null);
|
||||
|
||||
const fetchBlogById = async () => {
|
||||
const fetchBlog = async () => {
|
||||
try {
|
||||
const response = await getBlogByIdApi(Number(id));
|
||||
|
||||
setBlog(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching blog", error);
|
||||
const res = await getBlogByIdApi(Number(id));
|
||||
console.log({res});
|
||||
setBlog(res);
|
||||
} catch (err) {
|
||||
console.error("Error fetching blog", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchBlogById();
|
||||
}, [id]); // ✅ FIXED dependency
|
||||
fetchBlog();
|
||||
}, [id]);
|
||||
|
||||
if (!blog) {
|
||||
return <div className="mt-40 text-center text-gray-500">Loading...</div>;
|
||||
return <p className="mt-40 text-center">Loading...</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex flex-col ">
|
||||
<Card>
|
||||
<CardContent className="p-6 space-y-4">
|
||||
{/* Back */}
|
||||
{/* Back Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-black-600 p-0"
|
||||
className="mb-4 w-fit text-black px-0"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
← Back
|
||||
</Button>
|
||||
|
||||
{/* Title */}
|
||||
<h1 className="text-2xl md:text-4xl lg:text-5xl font-bold">
|
||||
{blog.title}
|
||||
</h1>
|
||||
<h1 className="text-3xl md:text-5xl font-bold mb-2">{blog.title}</h1>
|
||||
|
||||
{/* Meta */}
|
||||
<p className="text-gray-500 text-sm">
|
||||
<p className="text-gray-500 mb-4">
|
||||
{blog.writer} • {new Date(blog.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
|
||||
{/* Image */}
|
||||
{/* Image (only if exists) */}
|
||||
{blog.image?.trim() && (
|
||||
<img
|
||||
src={blog.image}
|
||||
alt={blog.title}
|
||||
className="w-full h-[220px] md:h-[400px] object-cover rounded-md"
|
||||
alt="blog"
|
||||
className="w-full h-[220px] md:h-[400px] object-cover rounded-lg mb-6"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="space-y-3 text-gray-800 leading-relaxed">
|
||||
{blog.content?.blocks?.map((block: any, index: number) => (
|
||||
<p key={index}>{block.data?.text}</p>
|
||||
))}
|
||||
<div>
|
||||
{blog.content?.blocks?.map((block, index) => renderBlock(block, index))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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";
|
||||
@@ -117,18 +117,6 @@ export default function BlogEditorPage() {
|
||||
initEditor();
|
||||
}, [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 () => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
@@ -182,7 +170,11 @@ export default function BlogEditorPage() {
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Cover Image</label>
|
||||
|
||||
<Input type="file" onChange={handleCoverUpload} />
|
||||
<BytescaleUploader
|
||||
value={coverImage}
|
||||
folderPath="/blog"
|
||||
onChange={(url) => setCoverImage(url)}
|
||||
/>
|
||||
|
||||
{coverImage && (
|
||||
<img
|
||||
|
||||
Reference in New Issue
Block a user