2026-04-14 16:04:44 +05:30
|
|
|
import React, {useEffect, useState} from "react";
|
|
|
|
|
import {useParams, useNavigate} from "react-router-dom";
|
|
|
|
|
|
|
|
|
|
import {Button} from "@/components/ui/button";
|
|
|
|
|
import {getBlogByIdApi} from "@/api/blog";
|
|
|
|
|
|
2026-04-16 16:38:16 +05:30
|
|
|
/* ---------------- 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 ---------------- */
|
2026-04-14 16:04:44 +05:30
|
|
|
const BlogDetail = () => {
|
|
|
|
|
const {id} = useParams();
|
|
|
|
|
const navigate = useNavigate();
|
2026-04-16 16:38:16 +05:30
|
|
|
const [blog, setBlog] = useState(null);
|
2026-04-14 16:04:44 +05:30
|
|
|
|
2026-04-16 16:38:16 +05:30
|
|
|
const fetchBlog = async () => {
|
2026-04-14 16:04:44 +05:30
|
|
|
try {
|
2026-04-16 16:38:16 +05:30
|
|
|
const res = await getBlogByIdApi(Number(id));
|
|
|
|
|
console.log({res});
|
|
|
|
|
setBlog(res);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Error fetching blog", err);
|
2026-04-14 16:04:44 +05:30
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-04-16 16:38:16 +05:30
|
|
|
fetchBlog();
|
|
|
|
|
}, [id]);
|
2026-04-14 16:04:44 +05:30
|
|
|
|
|
|
|
|
if (!blog) {
|
2026-04-16 16:38:16 +05:30
|
|
|
return <p className="mt-40 text-center">Loading...</p>;
|
2026-04-14 16:04:44 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-04-16 16:38:16 +05:30
|
|
|
<div className="mx-auto flex flex-col ">
|
|
|
|
|
{/* Back Button */}
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className="mb-4 w-fit text-black px-0"
|
|
|
|
|
onClick={() => navigate(-1)}
|
|
|
|
|
>
|
|
|
|
|
← Back
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{/* Title */}
|
|
|
|
|
<h1 className="text-3xl md:text-5xl font-bold mb-2">{blog.title}</h1>
|
|
|
|
|
|
|
|
|
|
{/* Meta */}
|
|
|
|
|
<p className="text-gray-500 mb-4">
|
|
|
|
|
{blog.writer} • {new Date(blog.createdAt).toLocaleDateString()}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
{/* Image (only if exists) */}
|
|
|
|
|
{blog.image?.trim() && (
|
|
|
|
|
<img
|
|
|
|
|
src={blog.image}
|
|
|
|
|
alt="blog"
|
|
|
|
|
className="w-full h-[220px] md:h-[400px] object-cover rounded-lg mb-6"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
<div>
|
|
|
|
|
{blog.content?.blocks?.map((block, index) => renderBlock(block, index))}
|
|
|
|
|
</div>
|
2026-04-14 16:04:44 +05:30
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default BlogDetail;
|