All checks were successful
CodeAnt AI Review - Stage 1 / codeant-review (pull_request) Successful in 1m4s
768 lines
33 KiB
TypeScript
768 lines
33 KiB
TypeScript
import {
|
|
BookOpen,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
Filter,
|
|
Grid,
|
|
List,
|
|
Search,
|
|
X
|
|
} from 'lucide-react';
|
|
import { useRef, useState, useEffect } from 'react';
|
|
import { BlogItem, useGetBlogsQuery } from '../redux/services/blogApi';
|
|
import { useGetFaqCategoriesQuery } from '../redux/services/faqApi';
|
|
import { ImageWithFallback } from './figma/ImageWithFallback';
|
|
import { PrimaryCTAButton } from './PrimaryCTAButton';
|
|
import { navigateTo } from './Router';
|
|
import { Badge } from './ui/badge';
|
|
import { Button } from './ui/button';
|
|
import { Card, CardContent } from './ui/card';
|
|
import { Input } from './ui/input';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
|
import { FullScreenLoader } from './FullScreenLoader';
|
|
import { getSlugWithId } from '../utils/urlHelpers';
|
|
|
|
// Define category type with ID and name
|
|
interface CategoryOption {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
type DateRange =
|
|
| 'all_time'
|
|
| 'last_7_days'
|
|
| 'last_30_days'
|
|
| 'last_3_months'
|
|
| 'last_6_months';
|
|
|
|
type SortBy =
|
|
| 'most_recent'
|
|
| 'oldest_first'
|
|
| 'title_az';
|
|
|
|
export function Articles() {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [selectedCategory, setSelectedCategory] = useState<CategoryOption>({ id: 'all', name: 'All Categories' });
|
|
const [selectedReadTime, setSelectedReadTime] = useState('All Read Times');
|
|
const [selectedDateRange, setSelectedDateRange] = useState<DateRange>('all_time');
|
|
const [selectedTopic, setSelectedTopic] = useState<{
|
|
id: string;
|
|
name: string;
|
|
}>({
|
|
id: 'all',
|
|
name: 'All Topics'
|
|
});
|
|
const [sortBy, setSortBy] = useState<SortBy>('most_recent');
|
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
const articlesPerPage = 6;
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const [allTags, setAllTags] = useState<{ id: string; name: string }[]>([]);
|
|
|
|
// Fetch categories for filter dropdown
|
|
const {
|
|
data: categoriesData,
|
|
isLoading: isLoadingCategories
|
|
} = useGetFaqCategoriesQuery({
|
|
limit: 100,
|
|
offset: 0
|
|
});
|
|
|
|
// Filter categories to only those for blog and create options with id and name
|
|
const categories: CategoryOption[] = [
|
|
{ id: 'all', name: 'All Categories' },
|
|
...(categoriesData?.data?.items
|
|
?.filter((category: any) => category.for_blog)
|
|
.map((category: any) => ({
|
|
id: category.id,
|
|
name: category.category_name
|
|
})) || [])
|
|
];
|
|
|
|
// Debounce search term
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
setDebouncedSearch(searchTerm);
|
|
}, 500);
|
|
return () => clearTimeout(timer);
|
|
}, [searchTerm]);
|
|
|
|
// Fetch blogs from API with all parameters
|
|
const {
|
|
data: blogsData,
|
|
isLoading: isLoadingBlogs,
|
|
isError: isBlogsError,
|
|
refetch: refetchBlogs
|
|
} = useGetBlogsQuery({
|
|
limit: articlesPerPage,
|
|
offset: (currentPage - 1) * articlesPerPage,
|
|
search: debouncedSearch || undefined,
|
|
content_status: 'publish',
|
|
content_type: 'BLOG',
|
|
date_range: selectedDateRange !== 'all_time' ? selectedDateRange : undefined,
|
|
sort_by: sortBy,
|
|
content_category_id: selectedCategory.id !== 'all' ? selectedCategory.id : undefined, // Send UUID
|
|
tag_id: selectedTopic.id !== 'all' ? selectedTopic.id : undefined,
|
|
});
|
|
|
|
|
|
const sortOptions = [
|
|
{ value: 'most_recent', label: 'Most Recent' },
|
|
{ value: 'oldest_first', label: 'Oldest First' },
|
|
{ value: 'title_az', label: 'Title A-Z' }
|
|
];
|
|
|
|
const readTimes = ['All Read Times', 'Under 5 min', '5-10 min', 'Over 10 min'];
|
|
const dateRanges = [
|
|
{ value: 'all_time', label: 'All Time' },
|
|
{ value: 'last_7_days', label: 'Last 7 days' },
|
|
{ value: 'last_30_days', label: 'Last 30 days' },
|
|
{ value: 'last_3_months', label: 'Last 3 months' }
|
|
];
|
|
|
|
// Format date function
|
|
const formatDate = (dateString: string) => {
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
};
|
|
|
|
// 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 articles by read time (client-side only - API doesn't support this)
|
|
const getReadTimeFilteredArticles = () => {
|
|
if (selectedReadTime === 'All Read Times' || !blogsData?.data?.items) {
|
|
return blogsData?.data?.items || [];
|
|
}
|
|
|
|
return (blogsData?.data?.items || []).filter((blog: BlogItem) => {
|
|
const readTimeMinutes = parseInt(calculateReadTime(blog.content).replace(' min read', '')) || 0;
|
|
return (selectedReadTime === 'Under 5 min' && readTimeMinutes < 5) ||
|
|
(selectedReadTime === '5-10 min' && readTimeMinutes >= 5 && readTimeMinutes <= 10) ||
|
|
(selectedReadTime === 'Over 10 min' && readTimeMinutes > 10);
|
|
});
|
|
};
|
|
|
|
const finalFilteredArticles = getReadTimeFilteredArticles();
|
|
|
|
const totalPages = Math.ceil((blogsData?.data?.pagination?.total || 0) / articlesPerPage);
|
|
|
|
const clearAllFilters = () => {
|
|
setSearchTerm('');
|
|
setDebouncedSearch('');
|
|
setSelectedCategory({ id: 'all', name: 'All Categories' });
|
|
setSelectedReadTime('All Read Times');
|
|
setSelectedDateRange('all_time');
|
|
setSelectedTopic({ id: 'all', name: 'All Topics' });
|
|
setSortBy('most_recent');
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
const hasActiveFilters = Boolean(
|
|
searchTerm ||
|
|
selectedCategory.id !== 'all' ||
|
|
selectedReadTime !== 'All Read Times' ||
|
|
selectedDateRange !== 'all_time' ||
|
|
selectedTopic.id !== 'all'
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!blogsData?.data?.items || allTags.length > 0) return;
|
|
|
|
const tags = Array.from(
|
|
new Map(
|
|
blogsData.data.items
|
|
.flatMap((blog: BlogItem) => blog.blog_tags || [])
|
|
.map(tag => [tag.id, { id: tag.id, name: tag.tag_name }])
|
|
).values()
|
|
);
|
|
|
|
setAllTags(tags);
|
|
}, [blogsData]);
|
|
|
|
// Handle loading state
|
|
if (isLoadingBlogs || isLoadingCategories) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-white">
|
|
<FullScreenLoader text="Loading articles..." />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Handle error state
|
|
if (isBlogsError) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-white">
|
|
<div className="text-center">
|
|
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<X className="w-8 h-8 text-red-600" />
|
|
</div>
|
|
<h2 className="text-h3 mb-2">Failed to load articles</h2>
|
|
<p className="text-gray-600 mb-4">Please try again later</p>
|
|
<Button onClick={() => refetchBlogs()}>
|
|
Retry
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={{ backgroundColor: '#FFFFFF' }} ref={containerRef}>
|
|
{/* Hero Section */}
|
|
<section className="relative py-28 overflow-hidden">
|
|
<div className="absolute inset-0">
|
|
<ImageWithFallback
|
|
src="https://images.unsplash.com/photo-1481627834876-b7833e8f5570?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcnRpY2xlJTIwYmxvZyUyMGNvbnRlbnQlMjBrbm93bGVkZ2V8ZW58MXx8fHwxNzU1ODU0Mjg2fDA&ixlib=rb-4.1.0&q=80&w=1080"
|
|
alt="Knowledge and insights through articles and research"
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
<div className="absolute inset-0 bg-black/60" />
|
|
</div>
|
|
|
|
<div className="relative section-margin-x">
|
|
<div className="max-w-4xl mx-auto text-center">
|
|
<div className="branded-tag-system-white mb-6 justify-center">
|
|
<div className="dot"></div>
|
|
<span className="text">INSIGHTS & KNOWLEDGE</span>
|
|
</div>
|
|
|
|
<h1 className="text-h1-white mb-6">
|
|
Articles & Research
|
|
</h1>
|
|
|
|
<p className="text-body-lg-white mb-8 max-w-2xl mx-auto">
|
|
Discover cutting-edge insights, research findings, and expert perspectives on leadership development, management strategies, and organizational excellence.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Statistics Strip */}
|
|
<div className="absolute bottom-0 left-0 right-0">
|
|
<div className="bg-black/80 backdrop-blur-sm px-8 py-6">
|
|
<div className="section-margin-x">
|
|
<div className="grid grid-cols-3 gap-8 text-center">
|
|
<div>
|
|
<div className="text-h2-white mb-2">{blogsData?.data?.pagination?.total || 0}+</div>
|
|
<div className="text-small-white">Expert Articles</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-h2-white mb-2">{categories.length - 1}</div>
|
|
<div className="text-small-white">Categories</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-h2-white mb-2">{allTags.length}+</div>
|
|
<div className="text-small-white">Topics</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Search and Controls Section */}
|
|
<section className="py-8" style={{ backgroundColor: '#FFFFFF' }}>
|
|
<div className="section-margin-x">
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
|
{/* Search Bar */}
|
|
<div className="relative max-w-md flex-1">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
|
<Input
|
|
type="text"
|
|
placeholder="Search articles..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10 pr-4 py-3 text-body rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all duration-200 w-full bg-gray-50"
|
|
style={{
|
|
fontSize: 'var(--font-body)',
|
|
fontFamily: 'var(--font-family-base)',
|
|
height: '48px'
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* View Toggle and Sort */}
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center border border-gray-300 rounded-lg overflow-hidden">
|
|
<button
|
|
onClick={() => setViewMode('grid')}
|
|
className={`p-2 transition-colors ${viewMode === 'grid'
|
|
? 'text-white'
|
|
: 'bg-white text-gray-600 hover:bg-gray-50'
|
|
}`}
|
|
style={{
|
|
backgroundColor: viewMode === 'grid' ? 'var(--color-primary)' : undefined
|
|
}}
|
|
aria-label="Grid view"
|
|
>
|
|
<Grid className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('list')}
|
|
className={`p-2 transition-colors ${viewMode === 'list'
|
|
? 'text-white'
|
|
: 'bg-white text-gray-600 hover:bg-gray-50'
|
|
}`}
|
|
style={{
|
|
backgroundColor: viewMode === 'list' ? 'var(--color-primary)' : undefined
|
|
}}
|
|
aria-label="List view"
|
|
>
|
|
<List className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<Select value={sortBy} onValueChange={setSortBy}>
|
|
<SelectTrigger className="w-40 text-body">
|
|
<SelectValue placeholder="Sort by" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sortOptions.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Main Content Section with Sidebar */}
|
|
<section className="pb-16" style={{ backgroundColor: '#FFFFFF' }}>
|
|
<div className="section-margin-x">
|
|
<div className="grid grid-cols-12 gap-8">
|
|
{/* Left Sidebar - Sticky Filters */}
|
|
<div className="col-span-12 lg:col-span-3">
|
|
<div className="sticky top-4">
|
|
<Card className="bg-white border border-gray-200 rounded-lg shadow-md overflow-hidden">
|
|
{/* Filter Header */}
|
|
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<div className="p-1.5 rounded-md" style={{ backgroundColor: 'rgba(4, 4, 91, 0.1)' }}>
|
|
<Filter className="w-3.5 h-3.5" style={{ color: 'var(--color-primary)' }} />
|
|
</div>
|
|
<h3 className="text-body font-semibold text-gray-800">
|
|
Filters
|
|
</h3>
|
|
</div>
|
|
{hasActiveFilters && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={clearAllFilters}
|
|
className="text-xs px-2 py-1 rounded-md transition-colors filter-clear-btn"
|
|
>
|
|
<X className="w-3 h-3 mr-1" />
|
|
Clear
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter Content */}
|
|
<div className="p-4">
|
|
<div className="space-y-4">
|
|
{/* Category Filter */}
|
|
<div className="filter-section">
|
|
<label className="block text-small mb-2 font-medium text-gray-700">
|
|
Category
|
|
</label>
|
|
<Select
|
|
value={selectedCategory.id}
|
|
onValueChange={(value: string) => {
|
|
const category = categories.find(c => c.id === value);
|
|
if (category) {
|
|
setSelectedCategory(category);
|
|
}
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
|
|
<SelectValue placeholder="All Categories">
|
|
{selectedCategory.name}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{categories.map((category) => (
|
|
<SelectItem key={category.id} value={category.id} className="text-small">
|
|
{category.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Read Time Filter - Client-side only */}
|
|
<div className="filter-section">
|
|
<label className="block text-small mb-2 font-medium text-gray-700">
|
|
Read Time
|
|
</label>
|
|
<Select value={selectedReadTime} onValueChange={setSelectedReadTime}>
|
|
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
|
|
<SelectValue placeholder="All Read Times" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{readTimes.map((readTime) => (
|
|
<SelectItem key={readTime} value={readTime} className="text-small">
|
|
{readTime}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Date Range Filter */}
|
|
<div className="filter-section">
|
|
<label className="block text-small mb-2 font-medium text-gray-700">
|
|
Date Range
|
|
</label>
|
|
<Select value={selectedDateRange} onValueChange={setSelectedDateRange}>
|
|
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
|
|
<SelectValue placeholder="All Time" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{dateRanges.map((range) => (
|
|
<SelectItem key={range.value} value={range.value}>
|
|
{range.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Topics Filter */}
|
|
<div className="filter-section">
|
|
<label className="block text-small mb-2 font-medium text-gray-700">
|
|
Topic
|
|
</label>
|
|
<Select
|
|
value={selectedTopic.id}
|
|
onValueChange={(value: string) => {
|
|
if (value === 'all') {
|
|
setSelectedTopic({ id: 'all', name: 'All Topics' });
|
|
return;
|
|
}
|
|
const topic = allTags.find(t => t.id === value);
|
|
if (topic) setSelectedTopic(topic);
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
|
|
<SelectValue placeholder="All Topics" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">
|
|
All Topics
|
|
</SelectItem>
|
|
|
|
{allTags.map((tag) => (
|
|
<SelectItem key={tag.id} value={tag.id}>
|
|
{tag.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Content Area - Scrollable Articles */}
|
|
<div className="col-span-12 lg:col-span-9">
|
|
<div className="mb-4 text-small text-muted">
|
|
Showing {finalFilteredArticles.length} of {blogsData?.data?.pagination?.total || 0} articles
|
|
</div>
|
|
|
|
{/* Articles Results */}
|
|
{finalFilteredArticles.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<BookOpen className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
|
<p className="text-body-lg text-muted">
|
|
No articles found matching your criteria.
|
|
</p>
|
|
{hasActiveFilters && (
|
|
<Button
|
|
variant="outline"
|
|
onClick={clearAllFilters}
|
|
className="mt-4"
|
|
>
|
|
Clear Filters
|
|
</Button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Grid View */}
|
|
{viewMode === 'grid' && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{finalFilteredArticles.map((article: BlogItem) => (
|
|
<Card
|
|
key={article.id}
|
|
className="overflow-hidden hover:shadow-lg transition-all duration-300 cursor-pointer group"
|
|
onClick={() => {
|
|
// Use slug_name to create the URL with full UUID
|
|
const url = getSlugWithId(article.slug_name, article.id);
|
|
navigateTo(`/learning/articles/${url}`);
|
|
}}
|
|
>
|
|
<div className="aspect-video w-full bg-gray-100 overflow-hidden relative">
|
|
<ImageWithFallback
|
|
src={article.banner_img || 'https://images.unsplash.com/photo-1481627834876-b7833e8f5570?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcnRpY2xlJTIwYmxvZyUyMGNvbnRlbnQlMjBrbm93bGVkZ2V8ZW58MXx8fHwxNzU1ODU0Mjg2fDA&ixlib=rb-4.1.0&q=80&w=1080'}
|
|
alt={article.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 gap-2 mb-3">
|
|
<Badge variant="outline" className="text-small">
|
|
{article.content_category}
|
|
</Badge>
|
|
<span className="text-small text-muted">
|
|
{calculateReadTime(article.content)}
|
|
</span>
|
|
</div>
|
|
|
|
<h3 className="text-h4 mb-3 group-hover:text-[#04045B] transition-colors line-clamp-2">
|
|
{article.title}
|
|
</h3>
|
|
|
|
<p className="text-small text-muted mb-4 line-clamp-3">
|
|
{article.short_description || article.content.substring(0, 150) + '...'}
|
|
</p>
|
|
|
|
<div className="flex items-center justify-end gap-2">
|
|
<div className="text-small text-muted">
|
|
{formatDate(article.updated_at)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Display tags if available */}
|
|
{article.blog_tags && article.blog_tags.length > 0 && (
|
|
<div className="flex flex-wrap gap-1 mt-3 pt-3 border-t border-gray-100">
|
|
{article.blog_tags.slice(0, 3).map((tag, idx) => (
|
|
<Badge key={idx} variant="secondary" className="text-xs">
|
|
{tag.tag_name}
|
|
</Badge>
|
|
))}
|
|
{article.blog_tags.length > 3 && (
|
|
<span className="text-xs text-gray-500">+{article.blog_tags.length - 3}</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* List View */}
|
|
{viewMode === 'list' && (
|
|
<div className="space-y-6">
|
|
{finalFilteredArticles.map((article: BlogItem) => (
|
|
<Card
|
|
key={article.id}
|
|
className="overflow-hidden hover:shadow-lg transition-all duration-300 cursor-pointer group"
|
|
onClick={() => {
|
|
// Use slug_name to create the URL with full UUID
|
|
const url = getSlugWithId(article.slug_name, article.id);
|
|
navigateTo(`/learning/articles/${url}`);
|
|
}}
|
|
>
|
|
<div className="flex flex-col md:flex-row">
|
|
<div className="md:w-80 h-48 md:h-auto bg-gray-100 overflow-hidden relative flex-shrink-0">
|
|
<ImageWithFallback
|
|
src={article.banner_img || 'https://images.unsplash.com/photo-1481627834876-b7833e8f5570?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcnRpY2xlJTIwYmxvZyUyMGNvbnRlbnQlMjBrbm93bGVkZ2V8ZW58MXx8fHwxNzU1ODU0Mjg2fDA&ixlib=rb-4.1.0&q=80&w=1080'}
|
|
alt={article.title}
|
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex-1 p-6">
|
|
<div className="flex justify-between items-start">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Badge variant="outline" className="text-small">
|
|
{article.content_category}
|
|
</Badge>
|
|
<span className="text-small text-muted">
|
|
{calculateReadTime(article.content)}
|
|
</span>
|
|
</div>
|
|
|
|
<h3 className="text-h4 mb-2 group-hover:text-[#04045B] transition-colors">
|
|
{article.title}
|
|
</h3>
|
|
|
|
<p className="text-body text-muted mb-3">
|
|
{article.short_description || article.content.substring(0, 200) + '...'}
|
|
</p>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="text-small text-muted">
|
|
{formatDate(article.updated_at)}
|
|
</div>
|
|
|
|
{/* Display tags if available */}
|
|
{article.blog_tags && article.blog_tags.length > 0 && (
|
|
<div className="flex flex-wrap gap-1">
|
|
{article.blog_tags.slice(0, 2).map((tag, idx) => (
|
|
<Badge key={idx} variant="secondary" className="text-xs">
|
|
{tag.tag_name}
|
|
</Badge>
|
|
))}
|
|
{article.blog_tags.length > 2 && (
|
|
<span className="text-xs text-gray-500">+{article.blog_tags.length - 2}</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Pagination */}
|
|
{totalPages > 1 && (
|
|
<div className="flex items-center justify-center gap-2 mt-8">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
setCurrentPage(prev => Math.max(1, prev - 1));
|
|
containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}}
|
|
disabled={currentPage === 1}
|
|
className="flex items-center gap-1 border-gray-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<ChevronLeft className="w-4 h-4" />
|
|
Previous
|
|
</Button>
|
|
|
|
<div className="flex items-center gap-1">
|
|
{Array.from({ length: totalPages }, (_, i) => {
|
|
const page = i + 1;
|
|
// Show limited pages for better UX
|
|
if (totalPages > 7) {
|
|
const showPage =
|
|
page === 1 ||
|
|
page === totalPages ||
|
|
(page >= currentPage - 1 && page <= currentPage + 1);
|
|
|
|
if (!showPage) {
|
|
if (page === currentPage - 2 || page === currentPage + 2) {
|
|
return <span key={page} className="px-2">...</span>;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
key={page}
|
|
variant={currentPage === page ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => {
|
|
setCurrentPage(page);
|
|
containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}}
|
|
className={`min-w-10 ${currentPage === page
|
|
? 'bg-[#04045B] text-white hover:bg-[#04045B]'
|
|
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
{page}
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
setCurrentPage(prev => Math.min(totalPages, prev + 1));
|
|
containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}}
|
|
disabled={currentPage === totalPages}
|
|
className="flex items-center gap-1 border-gray-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Next
|
|
<ChevronRight className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* CTA Banner Section */}
|
|
<section className="relative h-[700px] overflow-hidden">
|
|
<div className="absolute inset-0">
|
|
<ImageWithFallback
|
|
src="https://images.unsplash.com/photo-1753613648191-4771cf76f034?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxvbmxpbmUlMjBsZWFybmluZyUyMGRpZ2l0YWwlMjBlZHVjYXRpb258ZW58MXx8fHwxNzU1ODU0Mjc1fDA&ixlib=rb-4.1.0&q=80&w=1080"
|
|
alt="Online learning and digital education environment"
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
<div className="absolute inset-0 bg-black/30" />
|
|
<div className="absolute inset-0 bg-gradient-to-r from-black/20 via-transparent to-black/60" />
|
|
</div>
|
|
|
|
<div className="relative h-full flex items-center justify-end section-margin-x">
|
|
<div
|
|
className="bg-opacity-95 backdrop-blur-sm rounded-lg p-16 max-w-2xl"
|
|
style={{
|
|
backgroundColor: 'var(--color-brand-primary)'
|
|
}}
|
|
>
|
|
<div className="branded-tag-system-next-steps mb-6 justify-start">
|
|
<div className="dot"></div>
|
|
<span className="text">NEXT STEPS</span>
|
|
</div>
|
|
|
|
<h2 className="text-h2-white mb-8">
|
|
Ready to explore more insights?
|
|
<span
|
|
className="italic"
|
|
style={{ color: 'var(--color-brand-accent)' }}
|
|
>
|
|
{" "}Discover{" "}
|
|
</span>
|
|
our complete library of leadership resources.
|
|
</h2>
|
|
|
|
<PrimaryCTAButton
|
|
text="Browse All Resources"
|
|
onClick={() => navigateTo('/learning/articles')}
|
|
ariaLabel="Browse all leadership articles and resources"
|
|
className="cta-banner-yellow mb-6"
|
|
/>
|
|
|
|
<p className="text-body-white opacity-90">
|
|
Access cutting-edge research, expert insights, and practical guidance to accelerate your leadership journey and organizational success.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|