274 lines
11 KiB
TypeScript
274 lines
11 KiB
TypeScript
"use client";
|
|
import { useParams } from "next/navigation";
|
|
import Image from "next/legacy/image";
|
|
import Link from "next/link";
|
|
import { useBlogPost } from "@/lib/hooks/useBlog";
|
|
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
|
import { sanitizeHTML } from "@/lib/security/sanitize";
|
|
|
|
const BlogSingle = () => {
|
|
const params = useParams();
|
|
const slug = params?.slug as string;
|
|
const { post, loading, error } = useBlogPost(slug);
|
|
|
|
if (loading) {
|
|
return (
|
|
<section className="blog-single-section fix-top pb-120">
|
|
<div className="container">
|
|
<div className="row justify-content-center">
|
|
<div className="col-12 col-lg-10 col-xl-8">
|
|
<div className="loading-state text-center py-5">
|
|
<div className="spinner-border text-primary mb-3" role="status">
|
|
<span className="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p className="text-tertiary">Loading insight...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
if (error || !post) {
|
|
return (
|
|
<section className="blog-single-section fix-top pb-120">
|
|
<div className="container">
|
|
<div className="row justify-content-center">
|
|
<div className="col-12 col-lg-10 col-xl-8">
|
|
<div className="error-state text-center py-5">
|
|
<div className="error-icon mb-4">
|
|
<i className="fa-solid fa-exclamation-circle fa-4x text-tertiary"></i>
|
|
</div>
|
|
<h2 className="text-secondary mb-3">Insight Not Found</h2>
|
|
<p className="text-tertiary mb-4">
|
|
The insight you're looking for doesn't exist or has been removed.
|
|
</p>
|
|
<Link href="/insights" className="btn btn-primary">
|
|
<i className="fa-solid fa-arrow-left me-2"></i>
|
|
Back to Insights
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section className="blog-single-section fix-top pb-120">
|
|
<div className="container">
|
|
{/* Article Content */}
|
|
<div className="row">
|
|
<div className="col-12">
|
|
<article className="blog-single-article">
|
|
{/* Article Header */}
|
|
<header className="article-header">
|
|
{/* Top Meta Bar */}
|
|
<div className="article-top-meta d-flex flex-wrap align-items-center justify-content-between mb-4">
|
|
<div className="left-meta d-flex align-items-center gap-3">
|
|
{/* Category Badge */}
|
|
{post.category && (
|
|
<Link href={`/insights?category=${post.category.slug}`} className="category-badge">
|
|
<i className="fa-solid fa-folder-open me-2"></i>
|
|
{post.category.title}
|
|
</Link>
|
|
)}
|
|
|
|
{/* Date */}
|
|
<div className="meta-item d-flex align-items-center">
|
|
<i className="fa-solid fa-calendar me-2"></i>
|
|
<span>
|
|
{new Date(post.published_at || post.created_at).toLocaleDateString('en-US', {
|
|
day: 'numeric',
|
|
month: 'short',
|
|
year: 'numeric'
|
|
})}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="right-meta d-flex align-items-center gap-3">
|
|
{/* Reading Time */}
|
|
<div className="meta-item d-flex align-items-center">
|
|
<i className="fa-solid fa-clock me-2"></i>
|
|
<span>{post.reading_time} min</span>
|
|
</div>
|
|
|
|
{/* Views */}
|
|
<div className="meta-item d-flex align-items-center">
|
|
<i className="fa-solid fa-eye me-2"></i>
|
|
<span>{post.views_count}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Title */}
|
|
<h1 className="article-title">
|
|
{post.title}
|
|
</h1>
|
|
|
|
{/* Author and Tags Bar */}
|
|
<div className="article-bottom-meta d-flex flex-wrap align-items-center justify-content-between mt-4 pt-4">
|
|
{/* Author */}
|
|
<div className="author-meta d-flex align-items-center">
|
|
<div className="author-avatar me-3">
|
|
{post.author?.avatar ? (
|
|
<Image
|
|
src={post.author.avatar}
|
|
alt={post.author.name}
|
|
width={48}
|
|
height={48}
|
|
className="rounded-circle"
|
|
/>
|
|
) : (
|
|
<div className="avatar-placeholder">
|
|
<i className="fa-solid fa-user"></i>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="author-info">
|
|
<div className="author-label">Written by</div>
|
|
<div className="author-name">
|
|
{post.author?.name || post.author_name || 'Admin'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tags */}
|
|
{post.tags && post.tags.length > 0 && (
|
|
<div className="article-tags d-flex flex-wrap align-items-center gap-2">
|
|
{post.tags.map((tag) => (
|
|
<Link
|
|
key={tag.id}
|
|
href={`/insights?tag=${tag.slug}`}
|
|
className="tag-badge"
|
|
>
|
|
#{tag.name}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
{/* Featured Image */}
|
|
{(post.featured_image || post.thumbnail) && (
|
|
<div className="article-featured-image">
|
|
<div className="image-wrapper">
|
|
<Image
|
|
src={getValidImageUrl(
|
|
post.featured_image || post.thumbnail,
|
|
FALLBACK_IMAGES.BLOG
|
|
)}
|
|
alt={getValidImageAlt(post.title)}
|
|
width={1200}
|
|
height={600}
|
|
layout="responsive"
|
|
className="featured-image"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Article Body */}
|
|
<div className="article-body">
|
|
{/* Excerpt */}
|
|
{post.excerpt && (
|
|
<div className="article-excerpt">
|
|
<p className="lead">{post.excerpt}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Content */}
|
|
{post.content && (
|
|
<div
|
|
className="article-content enterprise-content"
|
|
dangerouslySetInnerHTML={{ __html: sanitizeHTML(post.content) }}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer Section */}
|
|
<footer className="article-footer">
|
|
{/* Share Section */}
|
|
<div className="article-share">
|
|
<div className="share-container">
|
|
<h6 className="share-title">
|
|
Share this insight
|
|
</h6>
|
|
<div className="social-share">
|
|
<Link
|
|
href="https://linkedin.com"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="share-btn share-linkedin"
|
|
aria-label="Share on LinkedIn"
|
|
>
|
|
<i className="fa-brands fa-linkedin-in"></i>
|
|
<span>LinkedIn</span>
|
|
</Link>
|
|
<button
|
|
onClick={() => {
|
|
if (typeof window !== 'undefined' && navigator.clipboard) {
|
|
navigator.clipboard.writeText(window.location.href);
|
|
alert('Link copied to clipboard!');
|
|
}
|
|
}}
|
|
className="share-btn share-copy"
|
|
aria-label="Copy link"
|
|
>
|
|
<i className="fa-solid fa-link"></i>
|
|
<span>Copy</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Author Bio */}
|
|
{post.author && post.author.bio && (
|
|
<div className="author-bio-section">
|
|
<div className="author-bio-card">
|
|
<div className="author-avatar-container">
|
|
{post.author.avatar ? (
|
|
<Image
|
|
src={post.author.avatar}
|
|
alt={post.author.name}
|
|
width={90}
|
|
height={90}
|
|
className="rounded-circle"
|
|
/>
|
|
) : (
|
|
<div className="author-avatar-large">
|
|
<i className="fa-solid fa-user"></i>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="author-bio-content">
|
|
<div className="bio-label">About the Author</div>
|
|
<h6 className="author-name">{post.author.name}</h6>
|
|
<p className="author-bio-text">{post.author.bio}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Navigation */}
|
|
<div className="article-navigation">
|
|
<Link href="/insights" className="btn-back-insights">
|
|
<i className="fa-solid fa-arrow-left me-2"></i>
|
|
Back to All Insights
|
|
</Link>
|
|
</div>
|
|
</footer>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default BlogSingle;
|