Files
sonarscantest/src/components/BlogDetail.tsx
WDI-Ideas 58d3a923f2
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Failing after 1m52s
first commit
2026-03-30 08:39:00 +05:30

546 lines
22 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Button } from './ui/button';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { CTABannerSection } from './CTABannerSection';
import { useCart } from './CartContext';
import {
Calendar,
Clock,
ChevronUp,
Bookmark,
Twitter,
Facebook,
Linkedin,
Link,
Heart,
Eye,
BookOpen,
ArrowLeft
} from 'lucide-react';
import { useGetBlogByIDQuery, useGetBlogsQuery } from '../redux/services/blogApi';
import { FullScreenLoader } from './FullScreenLoader';
import { extractIdFromSlug, extractSlugFromSlugAndId, getSlugWithId } from '../utils/urlHelpers';
interface BlogDetailProps {
params?: {
slugAndId?: string;
};
}
export function BlogDetail({ params }: BlogDetailProps) {
const { slugAndId } = useParams<{ slugAndId: string }>();
const navigate = useNavigate();
const [scrollProgress, setScrollProgress] = useState(0);
const [showBackToTop, setShowBackToTop] = useState(false);
const [isLiked, setIsLiked] = useState(false);
const [isBookmarked, setIsBookmarked] = useState(false);
const { addToCart } = useCart();
// Extract full ID from URL using the new function
const fullId = slugAndId ? extractIdFromSlug(slugAndId) : null;
const urlSlug = slugAndId ? extractSlugFromSlugAndId(slugAndId) : '';
// Fetch blog details by ID directly - no dependency on list API
const {
data: blogPost,
isLoading: isLoadingBlog,
isError: isBlogError,
refetch: refetchBlog
} = useGetBlogByIDQuery(fullId as string, {
skip: !fullId,
refetchOnMountOrArgChange: true,
});
// Fetch related blogs (excluding current blog)
const {
data: relatedBlogsData,
isLoading: isLoadingRelated
} = useGetBlogsQuery({
limit: 3,
offset: 0,
content_status: 'publish',
content_type: 'BLOG',
}, {
skip: !fullId,
});
// SEO: Check if URL slug matches the actual slug_name and redirect if needed
useEffect(() => {
if (blogPost && fullId) {
// Get the expected slug from the blog post
const expectedSlug = blogPost.slug_name;
// Create the expected URL with proper formatting
const expectedUrl = getSlugWithId(expectedSlug, fullId);
// Get the current URL slug
const currentSlugAndId = slugAndId || '';
// Compare (case-insensitive)
if (currentSlugAndId.toLowerCase() !== expectedUrl.toLowerCase()) {
// Redirect to the correct URL
navigate(`/learning/articles/${expectedUrl}`, { replace: true });
}
}
}, [blogPost, fullId, slugAndId, navigate]);
useEffect(() => {
if (blogPost?.title) {
document.title = `${blogPost.title} | KLC Blog`;
}
window.scrollTo(0, 0);
const handleScroll = () => {
const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
const scrolled = (winScroll / height) * 100;
setScrollProgress(scrolled);
setShowBackToTop(winScroll > 300);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [blogPost?.title]);
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
const handleShare = (platform: string) => {
const url = window.location.href;
const title = blogPost?.title || '';
const shareUrls = {
twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(url)}`,
facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`,
copy: url
};
if (platform === 'copy') {
navigator.clipboard.writeText(url);
alert('Link copied to clipboard!');
} else {
window.open(shareUrls[platform as keyof typeof shareUrls], '_blank', 'width=600,height=400');
}
};
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
// Calculate read time based on content length (approx)
const calculateReadTime = (content: string): string => {
const wordsPerMinute = 200;
const wordCount = content.split(/\s+/).length;
const minutes = Math.ceil(wordCount / wordsPerMinute);
return `${minutes} min read`;
};
// Filter related blogs (exclude current blog)
const relatedPosts = relatedBlogsData?.data?.items
?.filter((blog: any) => blog.id !== fullId)
.map((blog: any) => ({
id: blog.id,
title: blog.title,
slug: blog.slug_name,
excerpt: blog.short_description || blog.content.substring(0, 150) + '...',
author: blog.author_name || 'KLC Team',
publishedDate: blog.updated_at,
readTime: calculateReadTime(blog.content),
category: blog.content_category,
image: blog.banner_img || 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=400&h=300&fit=crop'
})) || [];
// Handle related post click - use full UUID
const handleRelatedPostClick = (postSlug: string, postId: string) => {
const url = getSlugWithId(postSlug, postId);
navigate(`/learning/articles/${url}`);
};
// Handle loading state
if (isLoadingBlog) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<FullScreenLoader text="Loading article..." />
</div>
);
}
// Handle error state
if (isBlogError || !blogPost) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<div className="text-center max-w-md px-4">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<BookOpen className="w-8 h-8 text-red-600" />
</div>
<h2 className="text-h3 mb-2">Article Not Found</h2>
<p className="text-gray-600 mb-6">
The article you're looking for could not be found. It may have been moved or removed.
</p>
<Button onClick={() => navigate('/learning/articles')}>
Back to Articles
</Button>
</div>
</div>
);
}
return (
<div className="min-h-screen" style={{ backgroundColor: '#FFFFFF' }}>
{/* Scroll Progress Bar */}
<div className="fixed top-0 left-0 w-full h-1 z-50" style={{ backgroundColor: 'rgba(0, 0, 0, 0.1)' }}>
<div
className="h-full transition-all duration-150"
style={{
width: `${scrollProgress}%`,
backgroundColor: '#04045B'
}}
/>
</div>
{/* Back to Top Button */}
{showBackToTop && (
<Button
onClick={scrollToTop}
size="icon"
className="fixed bottom-8 right-8 z-40 rounded-full shadow-lg"
style={{
backgroundColor: '#04045B',
color: 'white'
}}
>
<ChevronUp className="w-4 h-4" />
</Button>
)}
<main className="pt-20">
{/* Consistent Side Gutters - Breadcrumb Navigation */}
<div className="section-margin-x mb-8">
<div className="flex items-center gap-3 text-small" style={{ color: '#6F6F6F' }}>
<Button
variant="ghost"
size="sm"
onClick={() => navigate('/learning/articles')}
className="p-0 h-auto font-medium hover:bg-transparent transition-colors"
style={{ color: '#6F6F6F' }}
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Articles
</Button>
<span className="text-[#E5E7EB]">•</span>
<span>{blogPost.content_category || 'Article'}</span>
</div>
</div>
{/* Main Content Container with Consistent Gutters */}
<div className="section-margin-x">
{/* Reading Width Constraint for Better Readability */}
<div className="max-w-4xl mx-auto">
{/* Hero Header - Improved Spacing */}
<header className="mb-16">
{/* Category Badge */}
<div className="mb-8">
<Badge
className="mb-6 text-small px-4 py-2 font-medium border-none"
style={{
backgroundColor: 'rgba(4, 4, 91, 0.1)',
color: '#04045B'
}}
>
{blogPost.content_category || 'Article'}
</Badge>
{/* Improved Typography Hierarchy */}
<h1 className="text-h1 mb-6 leading-tight" style={{ color: '#26231A' }}>
{blogPost.title}
</h1>
{/* Constrained Width Excerpt for Better Readability */}
<div className="max-w-3xl">
<p className="text-body-lg leading-relaxed" style={{ color: '#6F6F6F' }}>
{blogPost.short_description || blogPost.content.substring(0, 200) + '...'}
</p>
</div>
</div>
{/* Enhanced Meta Bar with Cleaner Spacing */}
<div
className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6 p-6 rounded-xl border"
style={{ backgroundColor: 'rgba(0, 0, 0, 0.02)', borderColor: 'rgba(0, 0, 0, 0.08)' }}
>
{/* Author Info with Improved Layout */}
<div className="flex items-center gap-4">
<Avatar className="w-14 h-14 ring-2 ring-white shadow-md">
<AvatarImage src="https://images.unsplash.com/photo-1494790108755-2616b612b47c?w=150&h=150&fit=crop" alt="Author" />
<AvatarFallback className="text-subhead font-medium">KLC</AvatarFallback>
</Avatar>
<div>
<div className="text-subhead font-medium mb-1" style={{ color: '#26231A' }}>
KLC Team
</div>
{/* Cleaner Meta Information with Subtle Dividers */}
<div className="flex items-center gap-4 text-small" style={{ color: '#6F6F6F' }}>
<span className="flex items-center gap-1.5">
<Calendar className="w-4 h-4" />
{formatDate(blogPost.updated_at || new Date().toISOString())}
</span>
<div className="w-1 h-1 rounded-full" style={{ backgroundColor: '#E5E7EB' }}></div>
<span className="flex items-center gap-1.5">
<Clock className="w-4 h-4" />
{calculateReadTime(blogPost.content)}
</span>
<div className="w-1 h-1 rounded-full" style={{ backgroundColor: '#E5E7EB' }}></div>
<span className="flex items-center gap-1.5">
<Eye className="w-4 h-4" />
0
</span>
</div>
</div>
</div>
{/* Action Buttons with Better Spacing */}
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => setIsLiked(!isLiked)}
className={`transition-colors ${isLiked ? 'text-red-500' : 'text-[#6F6F6F]'}`}
>
<Heart className={`w-4 h-4 mr-2 ${isLiked ? 'fill-current' : ''}`} />
0
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setIsBookmarked(!isBookmarked)}
className={`transition-colors ${isBookmarked ? 'text-[#04045B]' : 'text-[#6F6F6F]'}`}
>
<Bookmark className={`w-4 h-4 ${isBookmarked ? 'fill-current' : ''}`} />
</Button>
{/* Share Options */}
<div className="flex items-center gap-1 ml-2 pl-2 border-l border-[#E5E7EB]">
<Button variant="ghost" size="sm" onClick={() => handleShare('twitter')} className="text-[#6F6F6F] hover:text-[#04045B]">
<Twitter className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => handleShare('linkedin')} className="text-[#6F6F6F] hover:text-[#04045B]">
<Linkedin className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => handleShare('copy')} className="text-[#6F6F6F] hover:text-[#04045B]">
<Link className="w-4 h-4" />
</Button>
</div>
</div>
</div>
{/* Featured Image with Better Aspect Ratio */}
<div className="aspect-[16/9] rounded-xl overflow-hidden mt-8 shadow-lg">
<ImageWithFallback
src={blogPost.banner_img || 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=1200&h=600&fit=crop'}
alt={blogPost.title}
className="w-full h-full object-cover"
/>
</div>
</header>
{/* Article Body with Enhanced Typography - Full Width */}
<article className="mb-20">
{/* Full Width Container - Uses complete available width */}
<div className="w-full">
<div
className="prose prose-xl max-w-none blog-article-content w-full"
style={{
/* Enhanced Typography Hierarchy using Design System */
'--tw-prose-body': '#26231A',
'--tw-prose-headings': '#26231A',
'--tw-prose-lead': '#26231A',
'--tw-prose-links': '#04045B',
'--tw-prose-bold': '#26231A',
'--tw-prose-counters': '#6F6F6F',
'--tw-prose-bullets': '#6F6F6F',
'--tw-prose-hr': 'rgba(0, 0, 0, 0.1)',
'--tw-prose-quotes': '#04045B',
'--tw-prose-quote-borders': '#04045B',
'--tw-prose-captions': '#6F6F6F',
'--tw-prose-code': '#04045B',
'--tw-prose-pre-code': '#26231A',
'--tw-prose-pre-bg': 'rgba(0, 0, 0, 0.05)',
'--tw-prose-th-borders': 'rgba(0, 0, 0, 0.15)',
'--tw-prose-td-borders': 'rgba(0, 0, 0, 0.1)',
/* Typography Scale using Design System Tokens */
fontSize: 'var(--font-body-lg)',
lineHeight: '1.75',
fontFamily: 'var(--font-family-base)',
color: '#26231A',
width: '100%'
} as React.CSSProperties}
dangerouslySetInnerHTML={{ __html: blogPost.content }}
/>
</div>
</article>
{/* Enhanced Tag Pills with Hover States */}
{blogPost.blog_tags && blogPost.blog_tags.length > 0 && (
<div className="mb-16">
<h3 className="text-subhead mb-6 font-medium" style={{ color: '#26231A' }}>
Topics covered in this article
</h3>
<div className="flex flex-wrap gap-3">
{blogPost.blog_tags.map((tag: any) => (
<Badge
key={tag.tag_name}
className="transition-all duration-200 text-body px-4 py-2 font-medium"
style={{
backgroundColor: 'rgba(4, 4, 91, 0.08)',
color: '#04045B',
border: '1px solid rgba(4, 4, 91, 0.15)'
}}
>
{tag.tag_name}
</Badge>
))}
</div>
</div>
)}
{/* Enhanced Author Bio Card */}
<Card className="mb-16 shadow-md border-0" style={{ backgroundColor: '#FFFFFF' }}>
<CardContent className="p-8">
<div className="flex items-start gap-6">
<Avatar className="w-20 h-20 ring-4 ring-white shadow-lg">
<AvatarImage src="https://images.unsplash.com/photo-1494790108755-2616b612b47c?w=150&h=150&fit=crop" alt="Author" />
<AvatarFallback className="text-lg font-medium">KLC</AvatarFallback>
</Avatar>
<div className="flex-1">
<h4 className="text-h4 mb-3 font-semibold" style={{ color: '#26231A' }}>
About KLC Team
</h4>
<p className="text-body leading-relaxed mb-6" style={{ color: '#6F6F6F' }}>
The Kautilya Leadership Center team is dedicated to providing cutting-edge insights and research on leadership development, management strategies, and organizational excellence.
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
{/* Related Articles Section with Balanced Grid Layout */}
{relatedPosts.length > 0 && (
<section className="py-20" style={{ backgroundColor: 'rgba(0, 0, 0, 0.02)' }}>
<div className="section-margin-x">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<div className="branded-tag-system mb-6">
<div className="dot"></div>
<span className="text">Continue Learning</span>
</div>
<h2 className="text-h2 mb-6 font-bold" style={{ color: '#26231A' }}>
Explore More Leadership Insights
</h2>
<p className="text-body-lg max-w-2xl mx-auto" style={{ color: '#6F6F6F' }}>
Discover additional strategies and frameworks to enhance your leadership effectiveness
</p>
</div>
{/* Balanced Card Grid with Equal Spacing */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{relatedPosts.map((post: any) => (
<Card
key={post.id}
className="overflow-hidden hover:shadow-xl transition-all duration-300 cursor-pointer group border-0"
onClick={() => {
// Use the same pattern as the main articles with full UUID
const url = getSlugWithId(post.slug, post.id);
navigate(`/learning/articles/${url}`);
}}
style={{ backgroundColor: '#FFFFFF' }}
>
<div className="aspect-[16/10] w-full bg-gray-100 overflow-hidden relative">
<ImageWithFallback
src={post.image}
alt={post.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-3">
<Badge
variant="outline"
className="text-small border-none"
style={{
backgroundColor: 'rgba(4, 4, 91, 0.1)',
color: '#04045B'
}}
>
{post.category}
</Badge>
<span className="text-small" style={{ color: '#6F6F6F' }}>
{post.readTime}
</span>
</div>
<h3
className="mb-3 group-hover:text-blue-600 transition-colors line-clamp-2"
style={{
fontSize: 'var(--font-h4)',
fontWeight: 'var(--font-weight-h4)',
lineHeight: '1.3',
color: '#26231A',
fontFamily: 'var(--font-family-base)'
}}
>
{post.title}
</h3>
<p
className="mb-4 line-clamp-3"
style={{
fontSize: 'var(--font-body)',
lineHeight: '1.5',
color: '#6F6F6F',
fontFamily: 'var(--font-family-base)'
}}
>
{post.excerpt}
</p>
<div className="flex items-center justify-between pt-4 border-t" style={{ borderColor: 'rgba(0, 0, 0, 0.05)' }}>
<span className="text-small" style={{ color: '#6F6F6F' }}>
{post.author}
</span>
<span className="text-small" style={{ color: '#6F6F6F' }}>
{formatDate(post.publishedDate)}
</span>
</div>
</CardContent>
</Card>
))}
</div>
</div>
</div>
</section>
)}
{/* CTA Section */}
<CTABannerSection />
</main>
</div>
);
}