221 lines
8.1 KiB
JavaScript
221 lines
8.1 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
|
import { useParams, Link, useLocation } from 'react-router-dom';
|
|
import ReactMarkdown from 'react-markdown';
|
|
import remarkGfm from 'remark-gfm';
|
|
import matter from 'gray-matter';
|
|
import { ArrowLeft, Calendar, User, Tag } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Buffer } from 'buffer'; // Explicitly import Buffer
|
|
|
|
const imageModules = import.meta.glob('../assets/*.jpg', { eager: true, import: 'default' });
|
|
|
|
const BlogPost = () => {
|
|
const { category, slug } = useParams(); // category will be undefined for /component/:slug
|
|
const location = useLocation();
|
|
const [post, setPost] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
|
|
useEffect(() => {
|
|
const loadPost = async () => {
|
|
try {
|
|
let contentModule;
|
|
let basePath;
|
|
let currentSlug = slug; // Use slug from useParams
|
|
|
|
// Determine content type based on pathname
|
|
const pathSegments = location.pathname.split('/').filter(Boolean);
|
|
let contentType = '';
|
|
|
|
if (pathSegments[0] === 'blog' && pathSegments.length >= 3) {
|
|
contentType = 'blog';
|
|
// For blog posts, category is the second segment, slug is the third
|
|
currentSlug = pathSegments[2];
|
|
basePath = `../content/blog/`; // blog/:category/:slug
|
|
} else if (pathSegments[0] === 'component' && pathSegments.length >= 2) {
|
|
contentType = 'component';
|
|
currentSlug = pathSegments[1]; // component/:slug
|
|
basePath = '../content/component/';
|
|
} else if (pathSegments[0] === 'technology' && pathSegments.length >= 2) {
|
|
contentType = 'tech';
|
|
currentSlug = pathSegments[1];
|
|
basePath = '../content/tech/';
|
|
} else if (pathSegments[0] === 'freezone' && pathSegments.length >= 2) {
|
|
contentType = 'freezone';
|
|
currentSlug = pathSegments[1];
|
|
basePath = '../content/freezone/';
|
|
} else if (pathSegments[0] === 'home' && pathSegments.length >= 2) {
|
|
contentType = 'home';
|
|
currentSlug = pathSegments[1];
|
|
basePath = '../content/home/';
|
|
}
|
|
else {
|
|
setError('Invalid URL path for content.');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const modules = import.meta.glob('../content/**/*.md', { as: 'raw', eager: true });
|
|
const fullPath = `${basePath}${currentSlug}.md`;
|
|
contentModule = modules[fullPath];
|
|
|
|
if (!contentModule) {
|
|
throw new Error(`Markdown file not found at ${fullPath}`);
|
|
}
|
|
|
|
const { data: frontmatter, content: markdownContent } = matter(contentModule);
|
|
|
|
// Resolve image path
|
|
const resolvedImage = frontmatter.image ? imageModules[`../assets/${frontmatter.image}`] : null;
|
|
|
|
setPost({ frontmatter: { ...frontmatter, image: resolvedImage }, content: markdownContent, contentType }); // Store contentType and resolved image with post
|
|
} catch (err) {
|
|
setError(`Failed to load content: ${err.message}. Please ensure the file exists and is correctly formatted.`);
|
|
}
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadPost();
|
|
}, [location.pathname, slug]); // Depend on pathname and slug
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
|
<div className="text-white text-xl">Loading...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
|
<div className="text-white text-xl">Error: {error}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const getBackLink = () => {
|
|
// Use post.contentType to determine the back link
|
|
if (!post || !post.contentType) {
|
|
return '/'; // Default to home if content type is unknown
|
|
}
|
|
switch (post.contentType) {
|
|
case 'component':
|
|
return '/how';
|
|
case 'blog':
|
|
return '/blog';
|
|
case 'tech':
|
|
return '/technology';
|
|
case 'freezone':
|
|
return '/freezone';
|
|
case 'home':
|
|
return '/'; // Or a more specific home content listing page if it exists
|
|
default:
|
|
return '/';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-900">
|
|
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
|
<div className="mb-8">
|
|
<Link to={getBackLink()}>
|
|
<Button className="bg-gray-800 hover:bg-gray-700 text-white">
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Back to {post?.contentType ? post.contentType.charAt(0).toUpperCase() + post.contentType.slice(1) : 'Previous Page'}
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
|
|
{post.frontmatter.image && (
|
|
<div className="mb-8">
|
|
<img
|
|
src={post.frontmatter.image}
|
|
alt={post.frontmatter.title}
|
|
className="w-full h-64 md:h-96 object-cover rounded-lg"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<article className="bg-gray-800 rounded-lg p-8">
|
|
<div className="mb-6">
|
|
<h1 className="text-4xl font-bold text-white mb-4">
|
|
{post.frontmatter.title}
|
|
</h1>
|
|
|
|
<div className="flex items-center space-x-4 text-gray-400 mb-4">
|
|
<div className="flex items-center space-x-2">
|
|
<User size={16} />
|
|
<span>{post.frontmatter.author}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Calendar size={16} />
|
|
<span>{post.frontmatter.date}</span>
|
|
</div>
|
|
<span>{post.frontmatter.readTime}</span>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
{post.frontmatter.tags?.map((tag, index) => (
|
|
<span
|
|
key={index}
|
|
className="px-3 py-1 bg-blue-600/20 text-blue-400 text-sm rounded-full"
|
|
>
|
|
<Tag className="inline-block mr-1 h-3 w-3" />
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="prose prose-invert prose-lg max-w-none">
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
components={{
|
|
h1: ({node, ...props}) => <h1 className="text-3xl font-bold text-white mb-4" {...props} />,
|
|
h2: ({node, ...props}) => <h2 className="text-2xl font-semibold text-white mb-3 mt-6" {...props} />,
|
|
h3: ({node, ...props}) => <h3 className="text-xl font-semibold text-white mb-2 mt-4" {...props} />,
|
|
p: ({node, ...props}) => <p className="text-gray-300 leading-relaxed mb-4" {...props} />,
|
|
ul: ({node, ...props}) => <ul className="text-gray-300 list-disc list-inside mb-4" {...props} />,
|
|
ol: ({node, ...props}) => <ol className="text-gray-300 list-decimal list-inside mb-4" {...props} />,
|
|
li: ({node, ...props}) => <li className="text-gray-300 mb-1" {...props} />,
|
|
blockquote: ({node, ...props}) => (
|
|
<blockquote
|
|
className="border-l-4 border-blue-500 pl-4 italic text-gray-400 mb-4"
|
|
{...props}
|
|
/>
|
|
),
|
|
code: ({node, inline, ...props}) => (
|
|
<code
|
|
className={`${
|
|
inline
|
|
? 'bg-gray-700 px-1 py-0.5 rounded text-sm'
|
|
: 'block bg-gray-700 p-4 rounded text-sm overflow-x-auto'
|
|
}`}
|
|
{...props}
|
|
/>
|
|
),
|
|
a: ({node, ...props}) => (
|
|
<a
|
|
className="text-blue-400 hover:text-blue-300 underline"
|
|
{...props}
|
|
/>
|
|
),
|
|
}}
|
|
>
|
|
{post.content}
|
|
</ReactMarkdown>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BlogPost;
|