546 lines
22 KiB
TypeScript
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>
|
|
);
|
|
} |