@@ -3,47 +3,36 @@ import { ImageWithFallback } from "./figma/ImageWithFallback";
|
||||
import { BrandedTag } from "./about/BrandedTag";
|
||||
import { StandardCTAButton } from "./StandardCTAButton";
|
||||
import { navigateTo } from "./Router";
|
||||
import { useGetFeaturedBlogsQuery } from "../redux/services/homepageApi";
|
||||
import { FullScreenLoader } from "./FullScreenLoader";
|
||||
import { getSlugWithId } from "../utils/urlHelpers";
|
||||
|
||||
interface InsightCard {
|
||||
id: number;
|
||||
// Interface for featured blog items from API
|
||||
interface FeaturedBlog {
|
||||
id: string;
|
||||
title: string;
|
||||
short_description: string | null;
|
||||
slug_name: string;
|
||||
banner_img: string | null;
|
||||
updated_at: string;
|
||||
content_category: string;
|
||||
content_type: string;
|
||||
tags: string[];
|
||||
featured: boolean;
|
||||
featured_order: number;
|
||||
}
|
||||
|
||||
// Insight Card Component Interface
|
||||
interface InsightCardData {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
date: string;
|
||||
tags: string[];
|
||||
image: string;
|
||||
slug?: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const insightCards: InsightCard[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "A New Lens on Leadership",
|
||||
description: "Your leadership calls, and how you interpret opportunities and threats, are influenced by your lenses, which are unique and personal to you.",
|
||||
date: "16-08-2016",
|
||||
tags: ["Leadership Lens", "Perspective"],
|
||||
image: "https://images.unsplash.com/photo-1560550900-5c10828c40aa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsZWFkZXJzaGlwJTIwbGVucyUyMHBlcnNwZWN0aXZlfGVufDF8fHx8MTc1OTk5NTg0N3ww&ixlib=rb-4.1.0&q=80&w=1080",
|
||||
slug: "new-lens-on-leadership"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Putting Psychometry in perspective",
|
||||
description: "An in-depth exploration of the limitations and appropriate use of psychometric tools in leadership assessment and talent selection.",
|
||||
date: "17-12-2016",
|
||||
tags: ["Psychometry", "Assessment"],
|
||||
image: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=600&h=400&fit=crop",
|
||||
slug: "putting-psychometry-in-perspective"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "The Busby Way to Talent Management: The Making of \"Busby's Babes\" – Part 1",
|
||||
description: "How Matt Busby transformed Manchester United through visionary talent management and youth development, creating a legacy that shaped modern football leadership.",
|
||||
date: "05-12-2013",
|
||||
tags: ["Talent Management", "Youth Development"],
|
||||
image: "https://images.unsplash.com/photo-1574629810360-7efbbe195018?w=600&h=400&fit=crop",
|
||||
slug: "busby-way-talent-management-part-1"
|
||||
}
|
||||
];
|
||||
|
||||
// Insight Tag Component
|
||||
function InsightTag({ text }: { text: string }) {
|
||||
return (
|
||||
@@ -78,7 +67,7 @@ function CalendarIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
// Explore All Button Component - Updated to redirect to articles page
|
||||
// Explore All Button Component
|
||||
function ExploreAllButton() {
|
||||
return (
|
||||
<StandardCTAButton
|
||||
@@ -90,14 +79,15 @@ function ExploreAllButton() {
|
||||
}
|
||||
|
||||
// Large Insight Card Component
|
||||
function LargeInsightCard({ card }: { card: InsightCard }) {
|
||||
function LargeInsightCard({ card }: { card: InsightCardData }) {
|
||||
const handleClick = () => {
|
||||
if (card.slug) {
|
||||
navigateTo(`/learning/articles/${card.slug}`);
|
||||
if (card.slug && card.id) {
|
||||
const url = getSlugWithId(card.slug, card.id);
|
||||
navigateTo(`/learning/articles/${url}`);
|
||||
}
|
||||
};
|
||||
|
||||
const hasLink = !!card.slug;
|
||||
const hasLink = !!(card.slug && card.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -125,8 +115,8 @@ function LargeInsightCard({ card }: { card: InsightCard }) {
|
||||
<div className="insight-card-white-box absolute bottom-6 left-6 right-6 bg-white rounded-xl p-6 shadow-lg transition-all duration-300 group-hover:shadow-xl">
|
||||
{/* Top section with tags and arrow */}
|
||||
<div className="insight-card-header flex items-start justify-between mb-4">
|
||||
<div className="insight-card-tags flex gap-2">
|
||||
{card.tags.map((tag, index) => (
|
||||
<div className="insight-card-tags flex gap-2 flex-wrap">
|
||||
{card.tags.slice(0, 2).map((tag, index) => (
|
||||
<InsightTag key={index} text={tag} />
|
||||
))}
|
||||
</div>
|
||||
@@ -158,14 +148,15 @@ function LargeInsightCard({ card }: { card: InsightCard }) {
|
||||
}
|
||||
|
||||
// Small Insight Card Component
|
||||
function SmallInsightCard({ card }: { card: InsightCard }) {
|
||||
function SmallInsightCard({ card }: { card: InsightCardData }) {
|
||||
const handleClick = () => {
|
||||
if (card.slug) {
|
||||
navigateTo(`/learning/articles/${card.slug}`);
|
||||
if (card.slug && card.id) {
|
||||
const url = getSlugWithId(card.slug, card.id);
|
||||
navigateTo(`/learning/articles/${url}`);
|
||||
}
|
||||
};
|
||||
|
||||
const hasLink = !!card.slug;
|
||||
const hasLink = !!(card.slug && card.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -193,8 +184,8 @@ function SmallInsightCard({ card }: { card: InsightCard }) {
|
||||
<div className="insight-card-white-box absolute bottom-4 left-4 right-4 bg-white rounded-xl p-4 shadow-lg transition-all duration-300 group-hover:shadow-xl">
|
||||
{/* Top section with tags and arrow */}
|
||||
<div className="insight-card-header flex items-start justify-between mb-3">
|
||||
<div className="insight-card-tags flex gap-2">
|
||||
{card.tags.map((tag, index) => (
|
||||
<div className="insight-card-tags flex gap-2 flex-wrap">
|
||||
{card.tags.slice(0, 2).map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2.5 py-1 text-xs font-medium rounded-full"
|
||||
@@ -232,7 +223,97 @@ function SmallInsightCard({ card }: { card: InsightCard }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Format date function
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
export function InsightsSection() {
|
||||
// Fetch featured blogs from API
|
||||
const { data: featuredBlogs, isLoading, isError } = useGetFeaturedBlogsQuery({ limit: 3 });
|
||||
|
||||
// Transform API data to match InsightCardData format
|
||||
const transformToInsightCard = (blog: FeaturedBlog): InsightCardData => ({
|
||||
id: blog.id,
|
||||
title: blog.title,
|
||||
description: blog.short_description || undefined,
|
||||
date: formatDate(blog.updated_at),
|
||||
tags: blog.tags || [],
|
||||
image: blog.banner_img || 'https://images.unsplash.com/photo-1481627834876-b7833e8f5570?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&w=1080',
|
||||
slug: blog.slug_name,
|
||||
});
|
||||
|
||||
// Handle loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<section
|
||||
className="py-24"
|
||||
style={{
|
||||
backgroundColor: '#F7F7FD',
|
||||
paddingTop: '8.75rem',
|
||||
paddingBottom: '8.75rem'
|
||||
}}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto section-margin-x">
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<FullScreenLoader text="Loading insights..." />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle error or no data state
|
||||
if (isError || !featuredBlogs || featuredBlogs.length === 0) {
|
||||
return (
|
||||
<section
|
||||
className="py-24"
|
||||
style={{
|
||||
backgroundColor: '#F7F7FD',
|
||||
paddingTop: '8.75rem',
|
||||
paddingBottom: '8.75rem'
|
||||
}}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto section-margin-x">
|
||||
<BrandedTag text="Leadership Insights" />
|
||||
|
||||
<div className="insights-container">
|
||||
{/* Header */}
|
||||
<div className="insights-header flex flex-col lg:flex-row lg:items-center lg:justify-between mb-16 gap-8 text-center lg:text-left">
|
||||
<h2
|
||||
className="insights-title text-3xl md:text-4xl lg:text-5xl font-bold leading-tight"
|
||||
style={{ color: 'var(--color-brand-black)' }}
|
||||
>
|
||||
Leadership Insights & Ideas
|
||||
</h2>
|
||||
<ExploreAllButton />
|
||||
</div>
|
||||
|
||||
{/* Show message when no featured blogs available */}
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-600">No featured insights available at the moment. Check back soon!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure we have at least 3 blogs for the layout (use placeholders if needed)
|
||||
const cards = featuredBlogs.map(transformToInsightCard);
|
||||
|
||||
// The layout expects: first card (large) + two small cards
|
||||
const largeCard = cards[0];
|
||||
const smallCards = cards.slice(1, 3);
|
||||
|
||||
// If we don't have enough cards, we can still render with what we have
|
||||
const hasLargeCard = !!largeCard;
|
||||
const hasSmallCards = smallCards.length > 0;
|
||||
|
||||
return (
|
||||
<section
|
||||
className="py-24"
|
||||
@@ -243,7 +324,6 @@ export function InsightsSection() {
|
||||
}}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto section-margin-x">
|
||||
{/* Branded Tag */}
|
||||
<BrandedTag text="Leadership Insights" />
|
||||
|
||||
<div className="insights-container">
|
||||
@@ -261,15 +341,20 @@ export function InsightsSection() {
|
||||
{/* Main Grid Layout */}
|
||||
<div className="insights-grid grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 lg:gap-8">
|
||||
{/* Large Card - Takes full height on left */}
|
||||
<div className="lg:row-span-2">
|
||||
<LargeInsightCard card={insightCards[0]} />
|
||||
</div>
|
||||
{hasLargeCard && (
|
||||
<div className="lg:row-span-2">
|
||||
<LargeInsightCard card={largeCard} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Small Cards - Stack on right */}
|
||||
<div className="flex flex-col gap-4 md:gap-6 lg:gap-8">
|
||||
<SmallInsightCard card={insightCards[1]} />
|
||||
<SmallInsightCard card={insightCards[2]} />
|
||||
</div>
|
||||
{hasSmallCards && (
|
||||
<div className="flex flex-col gap-4 md:gap-6 lg:gap-8">
|
||||
{smallCards.map((card:any, index:any) => (
|
||||
<SmallInsightCard key={card.id || index} card={card} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@ export function LearningOnline() {
|
||||
const [selectedPriceRange, setSelectedPriceRange] = useState('All Prices');
|
||||
const [selectedDuration, setSelectedDuration] = useState('All Durations');
|
||||
const [selectedRating, setSelectedRating] = useState('All Ratings');
|
||||
const [sortBy, setSortBy] = useState('Most Popular');
|
||||
const [sortBy, setSortBy] = useState('most_popular'); // ✅ Changed to match API value
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const coursesPerPage = 9;
|
||||
@@ -68,14 +68,15 @@ export function LearningOnline() {
|
||||
offset: 0
|
||||
});
|
||||
|
||||
// ✅ Updated sort options to match backend API values
|
||||
const sortOptions = [
|
||||
{ value: 'most_popular', label: 'Most Popular' },
|
||||
{ value: 'newest', label: 'Newest First' },
|
||||
{ value: 'title_asc', label: 'Title A-Z' },
|
||||
{ value: 'price_asc', label: 'Price: Low to High' },
|
||||
{ value: 'price_desc', label: 'Price: High to Low' },
|
||||
{ value: 'rating_desc', label: 'Highest Rated' },
|
||||
{ value: 'duration_asc', label: 'Duration' }
|
||||
{ value: 'price_low', label: 'Price: Low to High' },
|
||||
{ value: 'price_high', label: 'Price: High to Low' },
|
||||
{ value: 'highest_rated', label: 'Highest Rated' },
|
||||
{ value: 'duration', label: 'Duration' }
|
||||
];
|
||||
|
||||
const priceRanges = [
|
||||
@@ -112,33 +113,33 @@ export function LearningOnline() {
|
||||
return cats;
|
||||
}, [categoriesData]);
|
||||
|
||||
// Helper function to convert UI price range to API format
|
||||
// ✅ Helper function to convert UI price range to API format
|
||||
const getPriceRangeForApi = useCallback((priceRange: string): string | undefined => {
|
||||
switch (priceRange) {
|
||||
case 'Under ₹20,000':
|
||||
return '0-20000';
|
||||
return 'under_20000';
|
||||
case '₹20,000 - ₹35,000':
|
||||
return '20000-35000';
|
||||
return '20000_35000';
|
||||
case '₹35,000 - ₹50,000':
|
||||
return '35000-50000';
|
||||
return '35000_50000';
|
||||
case 'Over ₹50,000':
|
||||
return '50000-999999';
|
||||
return 'above_50000';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Helper function to convert UI duration to API format
|
||||
// ✅ Helper function to convert UI duration to API format
|
||||
const getDurationForApi = useCallback((duration: string): string | undefined => {
|
||||
switch (duration) {
|
||||
case 'Under 6 hours':
|
||||
return '0-6';
|
||||
return 'under_6';
|
||||
case '6-10 hours':
|
||||
return '6-10';
|
||||
return '6_10';
|
||||
case '10-15 hours':
|
||||
return '10-15';
|
||||
return '10_15';
|
||||
case 'Over 15 hours':
|
||||
return '15-999';
|
||||
return 'above_15';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
@@ -158,26 +159,11 @@ export function LearningOnline() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Helper function to convert sort option to API format
|
||||
// ✅ Helper function to convert sort option to API format
|
||||
const getSortByForApi = useCallback((sort: string): string | undefined => {
|
||||
switch (sort) {
|
||||
case 'Most Popular':
|
||||
return 'popular';
|
||||
case 'newest':
|
||||
return 'newest';
|
||||
case 'title':
|
||||
return 'title_asc';
|
||||
case 'price_low':
|
||||
return 'price_asc';
|
||||
case 'price_high':
|
||||
return 'price_desc';
|
||||
case 'rating':
|
||||
return 'rating_desc';
|
||||
case 'duration':
|
||||
return 'duration_asc';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
// sort is already in API format (most_popular, newest, title_asc, etc.)
|
||||
// Just return it as is
|
||||
return sort;
|
||||
}, []);
|
||||
|
||||
// Build API filters based on current UI state
|
||||
@@ -304,7 +290,7 @@ export function LearningOnline() {
|
||||
setSelectedPriceRange('All Prices');
|
||||
setSelectedDuration('All Durations');
|
||||
setSelectedRating('All Ratings');
|
||||
setSortBy('Most Popular');
|
||||
setSortBy('most_popular'); // ✅ Updated to match API value
|
||||
};
|
||||
|
||||
const hasActiveFilters = searchTerm ||
|
||||
@@ -361,7 +347,7 @@ export function LearningOnline() {
|
||||
|
||||
return (
|
||||
<div style={{ backgroundColor: '#FFFFFF' }}>
|
||||
{/* Hero Banner (keep as is) */}
|
||||
{/* Hero Banner */}
|
||||
<section className="relative py-16 overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
@@ -398,7 +384,7 @@ export function LearningOnline() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Search and Controls Section (keep as is) */}
|
||||
{/* 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">
|
||||
@@ -453,7 +439,7 @@ export function LearningOnline() {
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sortOptions.map((option: any) => (
|
||||
{sortOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
@@ -597,6 +583,15 @@ export function LearningOnline() {
|
||||
<p className="text-body-lg text-muted">
|
||||
No courses found matching your criteria.
|
||||
</p>
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={clearAllFilters}
|
||||
className="mt-4"
|
||||
>
|
||||
Clear Filters
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -693,18 +688,17 @@ export function LearningOnline() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(e: any) => {
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
handleAddToCart({
|
||||
id: course.id,
|
||||
title: course.title,
|
||||
price: course.price,
|
||||
originalPrice: course.originalPrice,
|
||||
thumbnail: course.thumbnail,
|
||||
category: course.category, // ✅ FIX
|
||||
level: course.level, // ✅ FIX
|
||||
type: 'course' // optional (if you added in interface)
|
||||
category: course.category,
|
||||
level: course.level,
|
||||
type: 'course'
|
||||
});
|
||||
}}
|
||||
className="flex items-center gap-2 hover:bg-blue-50 hover:border-blue-300"
|
||||
|
||||
@@ -321,8 +321,8 @@ export const courseApi = createApi({
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return queryString
|
||||
? `admin/course/list?${queryString}`
|
||||
: `admin/course/list`;
|
||||
? `admin/course/public/list?${queryString}`
|
||||
: `admin/course/public/list`;
|
||||
},
|
||||
|
||||
providesTags: (result) =>
|
||||
|
||||
@@ -112,9 +112,22 @@ export const homepageApi = createApi({
|
||||
providesTags: [{ type: "Homepage", id: "LIST" }],
|
||||
}),
|
||||
|
||||
getFeaturedBlogs: builder.query({
|
||||
query: ({ limit = 3 }) => ({
|
||||
url: `/admin/blogs/featured?limit=${limit}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
transformResponse: (response: any) => {
|
||||
if (response?.success && response?.data) {
|
||||
return response.data;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
|
||||
}),
|
||||
});
|
||||
|
||||
/* ================= HOOKS ================= */
|
||||
|
||||
export const { useGetHomepageQuery } = homepageApi;
|
||||
export const { useGetHomepageQuery, useGetFeaturedBlogsQuery } = homepageApi;
|
||||
Reference in New Issue
Block a user