main #1

Merged
PriyanshuVishwakarma merged 2 commits from main into testing 2026-03-27 07:14:29 +00:00
26 changed files with 5894 additions and 5276 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -5,21 +5,20 @@ import { PrimaryCTAButton } from "./PrimaryCTAButton";
import { navigateTo } from "./Router";
interface CTABannerSectionProps {
ctaBands?: Array<{
ctaSection?: {
id: string;
background_image_url: string;
background_image_alt_text: string;
text: string;
cta_text: string;
cta_destination: string;
}>;
description: string;
landing_page_type: string;
service_type: string | null;
};
isLoading?: boolean;
}
export function CTABannerSection({ ctaBands = [], isLoading }: CTABannerSectionProps) {
// Get the first CTA band or use default values
const ctaBand = ctaBands && ctaBands.length > 0 ? ctaBands[0] : null;
export function CTABannerSection({ ctaSection, isLoading }: CTABannerSectionProps) {
if (isLoading) {
return (
<section className="relative h-[700px] overflow-hidden bg-gray-100 animate-pulse">
@@ -31,8 +30,8 @@ export function CTABannerSection({ ctaBands = [], isLoading }: CTABannerSectionP
);
}
// If no CTA band is available, don't render anything
if (!ctaBand) {
// If no CTA section data is available, don't render anything
if (!ctaSection) {
return null;
}
@@ -41,8 +40,8 @@ export function CTABannerSection({ ctaBands = [], isLoading }: CTABannerSectionP
{/* Background Image */}
<div className="absolute inset-0">
<ImageWithFallback
src={ctaBand.background_image_url || "https://images.unsplash.com/photo-1552664730-d307ca884978?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2940&q=80"}
alt={ctaBand.background_image_alt_text || "Professional team collaborating in modern office"}
src={ctaSection.background_image_url || "https://images.unsplash.com/photo-1552664730-d307ca884978?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2940&q=80"}
alt="Background image for call to action section"
className="w-full h-full object-cover"
/>
@@ -65,11 +64,11 @@ export function CTABannerSection({ ctaBands = [], isLoading }: CTABannerSectionP
{/* Branded Tag */}
<BrandedTag text="Next Steps" variant="white" />
{/* Main Headline - Use API text or fallback */}
{/* Main Headline */}
<h2
className="text-h2-white mb-8"
className="text-h2-white mb-4"
>
{ctaBand.text || "Ready to transform your leadership?"}
{ctaSection.text || "Ready to transform your leadership?"}
<span
className="italic"
style={{ color: 'var(--color-brand-accent)' }}
@@ -79,20 +78,22 @@ export function CTABannerSection({ ctaBands = [], isLoading }: CTABannerSectionP
to start your development journey now.
</h2>
{/* Description */}
{ctaSection.description && (
<p
className="text-body-white mb-6 opacity-90"
>
{ctaSection.description}
</p>
)}
{/* CTA Button */}
<PrimaryCTAButton
text={ctaBand.cta_text || "Schedule a Consultation"}
onClick={() => navigateTo(ctaBand.cta_destination || '/contact?topic=consulting')}
text={ctaSection.cta_text || "Schedule a Consultation"}
onClick={() => navigateTo(ctaSection.cta_destination || '/contact?topic=consulting')}
ariaLabel="Schedule a consultation with our leadership experts"
className="cta-banner-yellow"
/>
{/* Supporting Text */}
<p
className="text-body-white mt-6 opacity-90"
>
Connect with our leadership experts to discuss your organization's specific development needs.
</p>
</div>
</div>
</section>

View File

@@ -14,21 +14,22 @@ export interface CartItem {
originalPrice?: string;
category: string;
level: string;
type?: string;
}
interface CartPopupProps {
isOpen: boolean;
onClose: () => void;
cartItems: CartItem[]; // Legacy prop - no longer used but kept for backward compatibility
onRemoveItem: (itemId: string) => void; // Legacy prop - no longer used but kept for backward compatibility
// cartItems: CartItem[]; // Legacy prop - no longer used but kept for backward compatibility
// onRemoveItem: (itemId: string) => void; // Legacy prop - no longer used but kept for backward compatibility
recentlyAddedItem?: CartItem | null;
}
export function CartPopup({
isOpen,
onClose,
cartItems: legacyCartItems, // Renamed to avoid confusion
onRemoveItem: legacyOnRemoveItem, // Renamed to avoid confusion
// cartItems: legacyCartItems, // Renamed to avoid confusion
// onRemoveItem: legacyOnRemoveItem, // Renamed to avoid confusion
recentlyAddedItem
}: CartPopupProps) {
const [showSuccess, setShowSuccess] = useState(false);

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import {
Users,
Clock,
import {
Users,
Clock,
Star,
ArrowRight,
ShoppingCart
@@ -22,7 +22,7 @@ export interface Course {
level: string;
format: string;
rating: number;
participants: string;
reviews: string;
category: string;
description: string;
price: string;
@@ -47,7 +47,7 @@ export function CourseCard({ course, onClick, className, onAddToCart }: CourseCa
const handleAddToCart = (e: React.MouseEvent) => {
e.stopPropagation(); // Prevent card click when clicking Add to Cart
if (onAddToCart) {
const cartItem: CartItem = {
id: course.id,
@@ -75,7 +75,7 @@ export function CourseCard({ course, onClick, className, onAddToCart }: CourseCa
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
<div className="absolute top-4 left-4">
<Badge
<Badge
variant="secondary"
className="px-3 py-1 font-medium"
style={{
@@ -89,26 +89,12 @@ export function CourseCard({ course, onClick, className, onAddToCart }: CourseCa
{course.category}
</Badge>
</div>
<div className="absolute top-4 right-4">
<Badge
variant="outline"
className="px-3 py-1 font-medium bg-white/90 backdrop-blur-sm"
style={{
fontSize: 'var(--font-small)',
fontFamily: 'var(--font-family-base)',
borderColor: 'var(--color-primary)',
color: 'var(--color-primary)'
}}
>
{course.level}
</Badge>
</div>
</div>
{/* Card Content - Reduced horizontal padding */}
<div className="p-5 flex flex-col flex-1">
{/* Course Title */}
<h3
<h3
className="mb-3 group-hover:text-blue-600 transition-colors leading-snug"
style={{
fontSize: 'var(--font-h4)',
@@ -120,9 +106,9 @@ export function CourseCard({ course, onClick, className, onAddToCart }: CourseCa
>
{course.title}
</h3>
{/* Course Description - Limited to 2 lines with ellipsis */}
<p
<p
className="mb-5 line-clamp-2 leading-relaxed"
style={{
fontSize: 'var(--font-body)',
@@ -138,14 +124,14 @@ export function CourseCard({ course, onClick, className, onAddToCart }: CourseCa
>
{course.description}
</p>
{/* Course Meta Information - Reduced bottom margin */}
<div className="flex items-center justify-between mb-5 pt-3 border-t border-gray-100">
<div className="flex items-center gap-5">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-gray-400" />
<span style={{
fontSize: 'var(--font-small)',
<span style={{
fontSize: 'var(--font-small)',
fontFamily: 'var(--font-family-base)',
color: 'var(--color-gray-muted)',
fontWeight: '500'
@@ -155,20 +141,20 @@ export function CourseCard({ course, onClick, className, onAddToCart }: CourseCa
</div>
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-gray-400" />
<span style={{
fontSize: 'var(--font-small)',
<span style={{
fontSize: 'var(--font-small)',
fontFamily: 'var(--font-family-base)',
color: 'var(--color-gray-muted)',
fontWeight: '500'
}}>
{course.participants}
{course.reviews}
</span>
</div>
</div>
<div className="flex items-center gap-1">
<Star className="w-4 h-4 fill-current text-yellow-400" />
<span
<span
className="font-semibold"
style={{
fontSize: 'var(--font-small)',
@@ -185,7 +171,7 @@ export function CourseCard({ course, onClick, className, onAddToCart }: CourseCa
<div className="mb-5">
<div className="flex items-center justify-between">
<div className="flex items-baseline gap-3">
<span
<span
className="font-bold"
style={{
fontSize: '1.75rem',
@@ -196,7 +182,7 @@ export function CourseCard({ course, onClick, className, onAddToCart }: CourseCa
{course.price}
</span>
{course.originalPrice && (
<span
<span
className="line-through"
style={{
fontSize: 'var(--font-body)',
@@ -208,7 +194,7 @@ export function CourseCard({ course, onClick, className, onAddToCart }: CourseCa
</span>
)}
</div>
{course.originalPrice && (
{/* {course.originalPrice && (
<div className="text-right">
<span
className="text-green-600 font-semibold text-sm"
@@ -219,62 +205,64 @@ export function CourseCard({ course, onClick, className, onAddToCart }: CourseCa
Save {Math.round(((parseFloat(course.originalPrice.replace('$', '')) - parseFloat(course.price.replace('$', ''))) / parseFloat(course.originalPrice.replace('$', ''))) * 100)}%
</span>
</div>
)}
)} */}
</div>
</div>
{/* Action Buttons - Horizontal Layout with reduced gap */}
<div className="flex flex-row gap-2 mt-auto">
{/* Add to Cart Button - Outline Blue */}
{/* Add to Cart */}
<Button
variant="outline"
onClick={handleAddToCart}
className="flex-1 flex items-center justify-center gap-2 h-11 rounded-lg transition-all duration-200 font-medium"
className="flex-1 flex items-center justify-center gap-1.5 h-9 rounded-md transition-all duration-200 font-medium px-2"
style={{
borderColor: '#04045B',
color: '#04045B',
backgroundColor: 'transparent',
fontSize: 'var(--font-body)',
fontSize: '12px', // ⬅️ reduced
fontFamily: 'var(--font-family-base)',
fontWeight: '500',
borderWidth: '2px'
borderWidth: '1px',
padding: '8px'
}}
onMouseEnter={(e) => {
onMouseEnter={(e: any) => {
e.currentTarget.style.backgroundColor = '#04045B';
e.currentTarget.style.color = 'white';
}}
onMouseLeave={(e) => {
onMouseLeave={(e: any) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = '#04045B';
}}
>
<ShoppingCart className="w-4 h-4" />
<ShoppingCart className="w-3.5 h-3.5" /> {/* ⬅️ smaller icon */}
Add to Cart
</Button>
{/* Learn More Button - Solid Blue */}
{/* Learn More */}
<Button
className="flex-1 flex items-center justify-center gap-2 h-11 rounded-lg transition-all duration-200 font-medium"
className="flex-1 flex items-center justify-center gap-1.5 h-9 rounded-md transition-all duration-200 font-medium px-2"
style={{
backgroundColor: '#04045B',
color: 'white',
fontSize: 'var(--font-body)',
fontSize: '12px', // ⬅️ reduced
fontFamily: 'var(--font-family-base)',
fontWeight: '500',
border: 'none'
border: 'none',
padding: '8px'
}}
onMouseEnter={(e) => {
onMouseEnter={(e: any) => {
e.currentTarget.style.backgroundColor = '#030359';
}}
onMouseLeave={(e) => {
onMouseLeave={(e: any) => {
e.currentTarget.style.backgroundColor = '#04045B';
}}
>
Learn More
<ArrowRight className="w-4 h-4" />
<ArrowRight className="w-3.5 h-3.5" /> {/* ⬅️ smaller icon */}
</Button>
</div>
</div>
</motion.div>
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,206 +1,55 @@
import React, { useState, useRef, useEffect } from 'react';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Input } from './ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import {
Play,
Users,
Clock,
ChevronRight,
ChevronLeft,
GraduationCap,
MessageCircle,
Zap,
Video,
Smartphone,
Award,
Building2,
BookOpen,
Star,
Globe,
Target,
TrendingUp,
Lightbulb,
CheckCircle,
ArrowRight,
Calendar,
Search,
ChevronRight,
Clock,
DollarSign,
Filter,
Grid,
List,
X,
DollarSign
Search,
Star,
Users,
X
} from 'lucide-react';
import { motion } from 'motion/react';
import { navigateTo } from './Router';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { BrandedTag } from './about/BrandedTag';
import { PrimaryCTAButton } from './PrimaryCTAButton';
import { CourseCard } from './CourseCard';
import { CartPopup, CartItem } from './CartPopup';
import { useState, useEffect, useMemo, useCallback } from 'react';
import { useCart } from './CartContext';
import { CartItem, CartPopup } from './CartPopup';
import { CourseCard } from './CourseCard';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { navigateTo } from './Router';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Card } from './ui/card';
import { Input } from './ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import {
courseApi,
useGetCoursesQuery,
Course,
GetCoursesParams,
useGetCourseCategoriesQuery,
CourseCategory
} from '../redux/services/courseApi';
import { useDebounce } from '../redux/hooks/useDebounce';
// Course Categories
const courseCategories = [
'Leadership Fundamentals',
'Decision Making & Strategy',
'Perspective & Risk',
'Communication & Influence',
'Change & Innovation'
];
// Helper function to parse rupee price from string (keep as is)
const parsePriceToNumber = (priceStr: string | number): number => {
if (typeof priceStr === 'number') return priceStr;
const numericStr = priceStr.toString().replace(/[^0-9.-]/g, '');
return parseFloat(numericStr) || 0;
};
// Featured Courses Data - Updated with Rupee pricing
const featuredCourses = [
{
id: '1',
title: 'Strategic Leadership Foundations',
thumbnail: 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=400&h=250&fit=crop',
duration: '12 hours',
level: 'Intermediate',
format: 'Self-paced',
rating: 4.8,
participants: '2,400+',
category: 'Leadership Fundamentals',
description: 'Master the core principles of strategic leadership and organizational vision.',
price: '₹24,817',
originalPrice: '₹33,117'
},
{
id: '2',
title: 'Data-Driven Decision Making',
thumbnail: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=400&h=250&fit=crop',
duration: '8 hours',
level: 'Advanced',
format: 'Cohort-based',
rating: 4.9,
participants: '1,800+',
category: 'Decision Making & Strategy',
description: 'Learn to make strategic decisions using data analytics and business intelligence.',
price: '₹37,267',
originalPrice: '₹45,567'
},
{
id: '3',
title: 'Risk Assessment & Management',
thumbnail: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=250&fit=crop',
duration: '10 hours',
level: 'Intermediate',
format: 'Self-paced',
rating: 4.7,
participants: '3,200+',
category: 'Perspective & Risk',
description: 'Develop expertise in identifying, analyzing, and mitigating organizational risks.',
price: '₹28,967',
originalPrice: '₹37,267'
},
{
id: '4',
title: 'Influential Communication',
thumbnail: 'https://images.unsplash.com/photo-1556761175-b413da4baf72?w=400&h=250&fit=crop',
duration: '6 hours',
level: 'Beginner',
format: 'Self-paced',
rating: 4.8,
participants: '5,100+',
category: 'Communication & Influence',
description: 'Master the art of persuasive communication and stakeholder engagement.',
price: '₹16,517',
originalPrice: '₹20,667'
},
{
id: '5',
title: 'Leading Innovation & Change',
thumbnail: 'https://images.unsplash.com/photo-1542744173-8e7e53415bb0?w=400&h=250&fit=crop',
duration: '14 hours',
level: 'Advanced',
format: 'Cohort-based',
rating: 4.9,
participants: '1,950+',
category: 'Change & Innovation',
description: 'Drive organizational transformation and foster a culture of innovation.',
price: '₹45,567',
originalPrice: '₹53,867'
},
{
id: '6',
title: 'Digital Leadership Essentials',
thumbnail: 'https://images.unsplash.com/photo-1551434678-e076c223a692?w=400&h=250&fit=crop',
duration: '9 hours',
level: 'Intermediate',
format: 'Self-paced',
rating: 4.6,
participants: '2,800+',
category: 'Leadership Fundamentals',
description: 'Navigate the digital transformation as a modern leader.',
price: '₹23,157',
originalPrice: '₹28,967'
},
{
id: '7',
title: 'Crisis Leadership Strategies',
thumbnail: 'https://images.unsplash.com/photo-1584697964358-3e14ca57658b?w=400&h=250&fit=crop',
duration: '7 hours',
level: 'Advanced',
format: 'Cohort-based',
rating: 4.7,
participants: '1,200+',
category: 'Leadership Fundamentals',
description: 'Navigate uncertainty and lead your team through challenging situations with confidence.',
price: '₹33,117',
originalPrice: '₹41,417'
},
{
id: '8',
title: 'Emotional Intelligence for Leaders',
thumbnail: 'https://images.unsplash.com/photo-1559027615-cd4628902d4a?w=400&h=250&fit=crop',
duration: '5 hours',
level: 'Beginner',
format: 'Self-paced',
rating: 4.9,
participants: '4,300+',
category: 'Communication & Influence',
description: 'Develop emotional intelligence to enhance your leadership effectiveness.',
price: '₹14,857',
originalPrice: '₹19,007'
},
{
id: 'ldp-foundations',
title: 'Strategic Leadership Development Program: Foundations',
thumbnail: 'https://images.unsplash.com/photo-1588912914078-2fe5224fd8b8?w=400&h=250&fit=crop',
duration: '40 hours',
level: 'Intermediate',
format: 'Self-paced',
rating: 4.8,
participants: '1,247+',
category: 'Leadership Development',
description: 'Master the fundamentals of effective leadership through evidence-based practices and real-world case studies.',
price: '$599',
originalPrice: '$799'
},
{
id: '9',
title: 'Strategic Risk Analysis',
thumbnail: 'https://images.unsplash.com/photo-1560472355-536de3962603?w=400&h=250&fit=crop',
duration: '11 hours',
level: 'Advanced',
format: 'Self-paced',
rating: 4.8,
participants: '1,500+',
category: 'Perspective & Risk',
description: 'Master advanced risk analysis techniques for strategic decision-making.',
price: '₹39,757',
originalPrice: '₹49,717'
}
];
// Format price with Rupee symbol (keep as is)
const formatPrice = (price: number): string => {
return `${price.toLocaleString('en-IN')}`;
};
export function LearningOnline() {
// UI state
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('All Categories');
const [selectedLevel, setSelectedLevel] = useState('All Levels');
const [selectedFormat, setSelectedFormat] = useState('All Formats');
const [selectedCategoryId, setSelectedCategoryId] = useState<string>('');
const [selectedCategoryName, setSelectedCategoryName] = useState('All Categories');
const [selectedPriceRange, setSelectedPriceRange] = useState('All Prices');
const [selectedDuration, setSelectedDuration] = useState('All Durations');
const [selectedRating, setSelectedRating] = useState('All Ratings');
@@ -208,99 +57,250 @@ export function LearningOnline() {
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [currentPage, setCurrentPage] = useState(1);
const coursesPerPage = 9;
const prefetchCourseById = courseApi.usePrefetch('getcoursebyid');
// Cart functionality - using global cart context
const { addToCart } = useCart();
const [isCartPopupOpen, setIsCartPopupOpen] = useState(false);
const [recentlyAddedItem, setRecentlyAddedItem] = useState<CartItem | null>(null);
// Debounced search term to avoid too many API calls
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// Get unique values for filters - Updated for Rupees
const categories = ['All Categories', ...courseCategories];
const levels = ['All Levels', ...Array.from(new Set(featuredCourses.map(course => course.level)))];
const formats = ['All Formats', ...Array.from(new Set(featuredCourses.map(course => course.format)))];
const priceRanges = ['All Prices', 'Under ₹20,000', '₹20,000 - ₹35,000', '₹35,000 - ₹50,000', 'Over ₹50,000'];
const durations = ['All Durations', 'Under 6 hours', '6-10 hours', '10-15 hours', 'Over 15 hours'];
const ratings = ['All Ratings', '4.5+ Stars', '4.0+ Stars', '3.5+ Stars'];
const sortOptions = [
{ value: 'Most Popular', label: 'Most Popular' },
{ value: 'newest', label: 'Newest First' },
{ value: 'title', label: 'Title A-Z' },
{ value: 'price_low', label: 'Price: Low to High' },
{ value: 'price_high', label: 'Price: High to Low' },
{ value: 'rating', label: 'Highest Rated' },
{ value: 'duration', label: 'Duration' }
];
// Helper function to parse rupee price
const parseRupeePrice = (priceStr: string) => {
return parseFloat(priceStr.replace('₹', '').replace(/,/g, ''));
};
// Filter and sort courses
const filteredCourses = featuredCourses.filter(course => {
const matchesSearch = course.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
course.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
course.category.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = selectedCategory === 'All Categories' || course.category === selectedCategory;
const matchesLevel = selectedLevel === 'All Levels' || course.level === selectedLevel;
const matchesFormat = selectedFormat === 'All Formats' || course.format === selectedFormat;
// Price filter - Updated for Rupees
const price = parseRupeePrice(course.price);
const matchesPrice = selectedPriceRange === 'All Prices' ||
(selectedPriceRange === 'Under ₹20,000' && price < 20000) ||
(selectedPriceRange === '₹20,000 - ₹35,000' && price >= 20000 && price <= 35000) ||
(selectedPriceRange === '₹35,000 - ₹50,000' && price >= 35000 && price <= 50000) ||
(selectedPriceRange === 'Over ₹50,000' && price > 50000);
// Duration filter
const durationHours = parseInt(course.duration);
const matchesDuration = selectedDuration === 'All Durations' ||
(selectedDuration === 'Under 6 hours' && durationHours < 6) ||
(selectedDuration === '6-10 hours' && durationHours >= 6 && durationHours <= 10) ||
(selectedDuration === '10-15 hours' && durationHours >= 10 && durationHours <= 15) ||
(selectedDuration === 'Over 15 hours' && durationHours > 15);
// Rating filter
const matchesRating = selectedRating === 'All Ratings' ||
(selectedRating === '4.5+ Stars' && course.rating >= 4.5) ||
(selectedRating === '4.0+ Stars' && course.rating >= 4.0) ||
(selectedRating === '3.5+ Stars' && course.rating >= 3.5);
return matchesSearch && matchesCategory && matchesLevel && matchesFormat && matchesPrice && matchesDuration && matchesRating;
}).sort((a, b) => {
switch (sortBy) {
case 'Most Popular':
return parseInt(b.participants.replace(/[^\d]/g, '')) - parseInt(a.participants.replace(/[^\d]/g, ''));
case 'newest':
return a.id.localeCompare(b.id); // Assuming newer courses have higher IDs
case 'title':
return a.title.localeCompare(b.title);
case 'price_low':
return parseRupeePrice(a.price) - parseRupeePrice(b.price);
case 'price_high':
return parseRupeePrice(b.price) - parseRupeePrice(a.price);
case 'rating':
return b.rating - a.rating;
case 'duration':
return parseInt(a.duration) - parseInt(b.duration);
default:
return 0;
}
// Fetch course categories
const { data: categoriesData, isLoading: categoriesLoading } = useGetCourseCategoriesQuery({
limit: 100,
offset: 0
});
// Paginate results
const totalPages = Math.ceil(filteredCourses.length / coursesPerPage);
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' }
];
const priceRanges = [
'All Prices',
'Under ₹20,000',
'₹20,000 - ₹35,000',
'₹35,000 - ₹50,000',
'Over ₹50,000'
];
const durations = [
'All Durations',
'Under 6 hours',
'6-10 hours',
'10-15 hours',
'Over 15 hours'
];
const ratings = [
'All Ratings',
'4.5+ Stars',
'4.0+ Stars',
'3.5+ Stars'
];
// Build categories list
const categories = useMemo(() => {
const cats = [{ id: '', name: 'All Categories' }];
if (categoriesData?.data?.items) {
categoriesData.data.items.forEach((cat: CourseCategory) => {
cats.push({ id: cat.id, name: cat.category_name });
});
}
return cats;
}, [categoriesData]);
// 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';
case '₹20,000 - ₹35,000':
return '20000-35000';
case '₹35,000 - ₹50,000':
return '35000-50000';
case 'Over ₹50,000':
return '50000-999999';
default:
return undefined;
}
}, []);
// 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';
case '6-10 hours':
return '6-10';
case '10-15 hours':
return '10-15';
case 'Over 15 hours':
return '15-999';
default:
return undefined;
}
}, []);
// Helper function to convert UI rating to API format
const getRatingForApi = useCallback((rating: string): number | undefined => {
switch (rating) {
case '4.5+ Stars':
return 4.5;
case '4.0+ Stars':
return 4.0;
case '3.5+ Stars':
return 3.5;
default:
return undefined;
}
}, []);
// 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;
}
}, []);
// Build API filters based on current UI state
const apiFilters: GetCoursesParams = useMemo(() => {
const filters: GetCoursesParams = {
limit: 100,
offset: 0,
status: 'publish'
};
// Category filter
if (selectedCategoryId) {
filters.course_category = [selectedCategoryId];
}
// Search query
if (debouncedSearchTerm) {
filters.search_query = debouncedSearchTerm;
}
// Price range
const apiPriceRange = getPriceRangeForApi(selectedPriceRange);
if (apiPriceRange) {
filters.price_range = apiPriceRange;
}
// Duration range
const apiDurationRange = getDurationForApi(selectedDuration);
if (apiDurationRange) {
filters.duration_range = apiDurationRange;
}
// Rating
const apiRating = getRatingForApi(selectedRating);
if (apiRating !== undefined) {
filters.min_rating = apiRating;
}
// Sort by
const apiSortBy = getSortByForApi(sortBy);
if (apiSortBy) {
filters.sort_by = apiSortBy;
}
return filters;
}, [
selectedCategoryId,
debouncedSearchTerm,
selectedPriceRange,
selectedDuration,
selectedRating,
sortBy,
getPriceRangeForApi,
getDurationForApi,
getRatingForApi,
getSortByForApi
]);
// Fetch courses with API filters
const {
data: coursesData,
isLoading: coursesLoading,
isError,
isFetching // To show loading indicator while fetching
} = useGetCoursesQuery(apiFilters);
// Reset to page 1 when filters change
useEffect(() => {
setCurrentPage(1);
}, [
selectedCategoryId,
debouncedSearchTerm,
selectedPriceRange,
selectedDuration,
selectedRating,
sortBy
]);
// Transform API response to course format
const courses = useMemo(() => {
if (!coursesData?.data?.items) return [];
return coursesData.data.items.map((course: Course) => ({
id: course.id,
title: course.course_name,
thumbnail: course.thumbnail_img || 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=400&h=250&fit=crop',
duration: `${course.total_duration || 0} hours`,
level: 'Intermediate',
format: course.retail_type === 'public' ? 'Cohort-based' : 'Self-paced',
rating: course.avg_rating || 4.5,
reviews: `${course.total_reviews || 0} review${(course.total_reviews || 0) === 1 ? '' : 's'}`,
category: course.course_category_name || 'General',
categoryId: course.course_category_xid || '',
description: course.course_desc || `Master ${course.course_name} with our comprehensive program.`,
price: formatPrice(course.best_value || 0),
originalPrice: formatPrice(course.price || 0),
course_status: course.course_status
}));
}, [coursesData]);
// Get total courses count from API response
const totalCoursesCount = coursesData?.data?.pagination_info?.total_count || 0;
// Paginate the courses (since API returns all courses based on filters, we paginate client-side)
const totalPages = Math.ceil(totalCoursesCount / coursesPerPage);
const startIndex = (currentPage - 1) * coursesPerPage;
const currentCourses = filteredCourses.slice(startIndex, startIndex + coursesPerPage);
const currentCourses = courses.slice(startIndex, startIndex + coursesPerPage);
// Handle category change
const handleCategoryChange = (value: string) => {
const selectedCat = categories.find(cat => cat.name === value);
if (selectedCat) {
setSelectedCategoryName(selectedCat.name);
setSelectedCategoryId(selectedCat.id);
} else {
setSelectedCategoryName('All Categories');
setSelectedCategoryId('');
}
};
const clearAllFilters = () => {
setSearchTerm('');
setSelectedCategory('All Categories');
setSelectedLevel('All Levels');
setSelectedFormat('All Formats');
handleCategoryChange('All Categories');
setSelectedPriceRange('All Prices');
setSelectedDuration('All Durations');
setSelectedRating('All Ratings');
@@ -308,14 +308,16 @@ export function LearningOnline() {
};
const hasActiveFilters = searchTerm ||
selectedCategory !== 'All Categories' ||
selectedLevel !== 'All Levels' ||
selectedFormat !== 'All Formats' ||
selectedCategoryName !== 'All Categories' ||
selectedPriceRange !== 'All Prices' ||
selectedDuration !== 'All Durations' ||
selectedRating !== 'All Ratings';
// Cart functions - using global cart context
// Cart functionality
const { addToCart } = useCart();
const [isCartPopupOpen, setIsCartPopupOpen] = useState(false);
const [recentlyAddedItem, setRecentlyAddedItem] = useState<CartItem | null>(null);
const handleAddToCart = (item: CartItem) => {
addToCart(item);
setRecentlyAddedItem(item);
@@ -327,9 +329,39 @@ export function LearningOnline() {
setRecentlyAddedItem(null);
};
const handleCourseClick = useCallback((courseId: string) => {
prefetchCourseById(courseId, { force: true });
navigateTo(`/course/${courseId}`);
}, [prefetchCourseById]);
// Show loading state
if (coursesLoading || categoriesLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-gray-600">Loading courses...</p>
</div>
</div>
);
}
// Show error state
if (isError) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold text-red-600 mb-4">Error Loading Courses</h2>
<p className="text-gray-600 mb-4">Failed to load courses. Please try again later.</p>
<Button onClick={() => window.location.reload()}>Retry</Button>
</div>
</div>
);
}
return (
<div style={{ backgroundColor: '#FFFFFF' }}>
{/* Hero Banner Digital Learning - Blog Style */}
{/* Hero Banner (keep as is) */}
<section className="relative py-16 overflow-hidden">
<div
className="absolute inset-0"
@@ -349,18 +381,13 @@ export function LearningOnline() {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
{/* Eyebrow Text */}
<div className="branded-tag-system-white mb-6 justify-start">
<div className="dot"></div>
<span className="text">DIGITAL LEARNING PLATFORM</span>
</div>
{/* Main Header */}
<h1 className="text-h1-white mb-8" style={{ lineHeight: 'var(--line-height-h1)' }}>
Discover Your Leadership<br />Potential Online
</h1>
{/* Sub Text */}
<div className="max-w-5xl mb-8">
<p className="text-body-lg-white" style={{ lineHeight: '1.7' }}>
Our Leadership Courses are structured packages which are targeted towards building your leadership abilities. Each course is a wholesome package which not only helps you gain awareness about your leadership style but also gives insights to build your leadership abilities. Every course contains curated content targeted towards a specific leadership ability. Each course consists of our proprietary profiling instruments Leadership Profilers, conceptual videos and experiences of leaders Leadership Webcasts, as well as additional content to supplement learning.
@@ -371,11 +398,10 @@ export function LearningOnline() {
</div>
</section>
{/* Search and Controls Section */}
{/* Search and Controls Section (keep as is) */}
<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
@@ -392,14 +418,13 @@ export function LearningOnline() {
/>
</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'
? 'text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
style={{
backgroundColor: viewMode === 'grid' ? 'var(--color-primary)' : undefined
@@ -411,8 +436,8 @@ export function LearningOnline() {
<button
onClick={() => setViewMode('list')}
className={`p-2 transition-colors ${viewMode === 'list'
? 'text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
? 'text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
style={{
backgroundColor: viewMode === 'list' ? 'var(--color-primary)' : undefined
@@ -428,7 +453,7 @@ export function LearningOnline() {
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
{sortOptions.map((option) => (
{sortOptions.map((option: any) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -481,52 +506,14 @@ export function LearningOnline() {
<label className="block text-small mb-2 font-medium text-gray-700">
Category
</label>
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
<Select value={selectedCategoryName} onValueChange={handleCategoryChange}>
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
<SelectValue placeholder="All Categories" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem key={category} value={category} className="text-small">
{category}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Level Filter */}
<div className="filter-section">
<label className="block text-small mb-2 font-medium text-gray-700">
Level
</label>
<Select value={selectedLevel} onValueChange={setSelectedLevel}>
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
<SelectValue placeholder="All Levels" />
</SelectTrigger>
<SelectContent>
{levels.map((level) => (
<SelectItem key={level} value={level} className="text-small">
{level}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Format Filter */}
<div className="filter-section">
<label className="block text-small mb-2 font-medium text-gray-700">
Format
</label>
<Select value={selectedFormat} onValueChange={setSelectedFormat}>
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
<SelectValue placeholder="All Formats" />
</SelectTrigger>
<SelectContent>
{formats.map((format) => (
<SelectItem key={format} value={format} className="text-small">
{format}
<SelectItem key={category.name} value={category.name} className="text-small">
{category.name}
</SelectItem>
))}
</SelectContent>
@@ -595,10 +582,13 @@ export function LearningOnline() {
</div>
</div>
{/* Right Content Area - Scrollable Courses */}
{/* Right Content Area */}
<div className="col-span-12 lg:col-span-9">
<div className="mb-4 text-small text-muted">
Showing {currentCourses.length} of {filteredCourses.length} courses
<div className="mb-4 text-small text-muted flex justify-between items-center">
<span>Showing {currentCourses.length} of {totalCoursesCount} courses</span>
{isFetching && (
<span className="text-xs text-blue-600 animate-pulse">Updating results...</span>
)}
</div>
{/* Courses Results */}
@@ -618,6 +608,7 @@ export function LearningOnline() {
<CourseCard
course={course}
className="h-[560px] flex flex-col w-full"
onClick={() => handleCourseClick(course.id)}
onAddToCart={handleAddToCart}
/>
</div>
@@ -632,7 +623,7 @@ export function LearningOnline() {
<Card
key={course.id}
className="overflow-hidden hover:shadow-lg transition-all duration-300 cursor-pointer group"
onClick={() => navigateTo(`/course/${course.id}`)}
onClick={() => handleCourseClick(course.id)}
>
<div className="flex flex-col md:flex-row">
<div className="md:w-80 flex-shrink-0">
@@ -688,7 +679,7 @@ export function LearningOnline() {
</div>
<div className="flex items-center gap-1">
<Users className="w-4 h-4 text-gray-400" />
<span className="text-small text-gray-600">{course.participants}</span>
<span className="text-small text-gray-600">{course.reviews}</span>
</div>
</div>
</div>
@@ -702,15 +693,18 @@ export function LearningOnline() {
<Button
variant="outline"
size="sm"
onClick={(e) => {
onClick={(e: any) => {
e.stopPropagation();
handleAddToCart({
id: course.id,
title: course.title,
price: course.price,
originalPrice: course.originalPrice,
thumbnail: course.thumbnail,
type: 'course'
category: course.category, // ✅ FIX
level: course.level, // ✅ FIX
type: 'course' // optional (if you added in interface)
});
}}
className="flex items-center gap-2 hover:bg-blue-50 hover:border-blue-300"
@@ -781,4 +775,4 @@ export function LearningOnline() {
/>
</div>
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ import { useState, useRef, useEffect } from "react";
import { BrandedTag } from "./about/BrandedTag";
interface Testimonial {
id?: number;
id?: number | string;
name: string;
role: string;
company?: string;
@@ -16,8 +16,13 @@ interface Testimonial {
isVideo?: boolean;
videoThumbnail?: string;
videoUrl?: string;
designation?: string;
content?: string;
video_url?: string;
profile_xid?: string;
}
// Default testimonials as fallback
const defaultTestimonialsData: Testimonial[] = [
{
id: 1,
@@ -52,38 +57,6 @@ const defaultTestimonialsData: Testimonial[] = [
isVideo: true,
videoThumbnail: "https://images.unsplash.com/photo-1560472355-109703aa3edc?w=600&h=300&fit=crop",
videoUrl: "https://example.com/testimonial-video-2.mp4"
},
{
id: 4,
name: "David Thompson",
role: "Senior Manager",
company: "Enterprise Solutions",
avatar: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop&crop=face",
quote: "The personalized coaching and development programs have been game-changing for our organization's leadership pipeline and succession planning initiatives.",
rating: 5,
isVideo: false
},
{
id: 5,
name: "Lisa Wang",
role: "Product Manager",
company: "Digital Ventures",
avatar: "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&h=400&fit=crop&crop=face",
quote: "KLC has transformed how we think about leadership in the digital age. The insights and strategies have been invaluable for our team's growth and innovation culture.",
rating: 5,
isVideo: true,
videoThumbnail: "https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=600&h=300&fit=crop",
videoUrl: "https://example.com/testimonial-video-3.mp4"
},
{
id: 6,
name: "Robert Kim",
role: "Regional Director",
company: "Global Corp",
avatar: "https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?w=400&h=400&fit=crop&crop=face",
quote: "The leadership development framework provided by KLC has been instrumental in building a more cohesive and effective leadership team across our regions.",
rating: 4,
isVideo: false
}
];
@@ -138,12 +111,18 @@ function VideoModal({ isOpen, onClose, videoUrl }: {
);
}
// Individual Testimonial Card - Updated with Landing Page Design Standards
// Individual Testimonial Card
function TestimonialCard({ testimonial, onPlayVideo }: {
testimonial: Testimonial;
onPlayVideo: (videoUrl: string) => void;
}) {
const avatarSrc = testimonial.avatar || testimonial.image;
const isVideo = testimonial.isVideo || !!testimonial.video_url;
const videoUrl = testimonial.videoUrl || testimonial.video_url || "";
const role = testimonial.role || testimonial.designation || "";
const quote = testimonial.quote || testimonial.content || "";
const name = testimonial.name || "";
const rating = testimonial.rating || 5;
return (
<motion.div
@@ -162,14 +141,14 @@ function TestimonialCard({ testimonial, onPlayVideo }: {
}}
>
{/* Video Testimonials */}
{testimonial.isVideo ? (
{isVideo ? (
<div
className="relative h-full cursor-pointer overflow-hidden group rounded-xl"
onClick={() => onPlayVideo(testimonial.videoUrl || "")}
onClick={() => onPlayVideo(videoUrl)}
>
<ImageWithFallback
src={testimonial.videoThumbnail || avatarSrc || ""}
alt={`${testimonial.name} video testimonial`}
src={testimonial.videoThumbnail || avatarSrc || "https://images.unsplash.com/photo-1552664730-d307ca884978?w=600&h=300&fit=crop"}
alt={`${name} video testimonial`}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
@@ -203,16 +182,16 @@ function TestimonialCard({ testimonial, onPlayVideo }: {
<div className="w-10 h-10 rounded-full overflow-hidden bg-white shadow-lg flex-shrink-0">
<ImageWithFallback
src={avatarSrc || ""}
alt={testimonial.name}
alt={name}
className="w-full h-full object-cover"
/>
</div>
<div className="min-w-0 flex-1">
<h4 className="font-semibold text-white mb-1 text-sm">
{testimonial.name}
{name}
</h4>
<p className="text-xs text-white/80 truncate">
{testimonial.role}
{role}
{testimonial.company && `${testimonial.company}`}
</p>
</div>
@@ -223,7 +202,7 @@ function TestimonialCard({ testimonial, onPlayVideo }: {
<Star
key={star}
size={14}
className={star <= testimonial.rating ? 'fill-current text-yellow-400' : 'text-white/40'}
className={star <= rating ? 'fill-current text-yellow-400' : 'text-white/40'}
/>
))}
</div>
@@ -239,16 +218,16 @@ function TestimonialCard({ testimonial, onPlayVideo }: {
<div className="w-12 h-12 rounded-full overflow-hidden bg-gray-100 flex-shrink-0">
<ImageWithFallback
src={avatarSrc || ""}
alt={testimonial.name}
alt={name}
className="w-full h-full object-cover"
/>
</div>
<div className="min-w-0">
<h4 className="font-semibold text-black mb-1 text-sm">
{testimonial.name}
{name}
</h4>
<p className="text-xs text-gray-600">
{testimonial.role}
{role}
</p>
{testimonial.company && (
<p className="text-xs text-gray-500 font-medium">
@@ -264,7 +243,7 @@ function TestimonialCard({ testimonial, onPlayVideo }: {
<Star
key={star}
size={14}
className={star <= testimonial.rating ? 'fill-current text-yellow-400' : 'text-gray-300'}
className={star <= rating ? 'fill-current text-yellow-400' : 'text-gray-300'}
/>
))}
</div>
@@ -277,7 +256,7 @@ function TestimonialCard({ testimonial, onPlayVideo }: {
"
</span>
<span className="relative z-10">
{testimonial.quote}
{quote}
</span>
</div>
</blockquote>

View File

@@ -44,7 +44,7 @@ import { navigateTo } from './Router';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { BrandedTag } from './about/BrandedTag';
import { PrimaryCTAButton } from './PrimaryCTAButton';
import { toast } from 'sonner@2.0.3';
import { toast } from 'sonner';
import { getWebinarBySlug, sharedWebinarsData, type WebinarData } from '../data/webinarsData';
interface WebinarDetailProps {

View File

@@ -1,151 +1,100 @@
import React, { useState, useRef, useEffect } from 'react';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Input } from './ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Slider } from './ui/slider';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { PrimaryCTAButton } from './PrimaryCTAButton';
import { navigateTo } from './Router';
import { sharedWebinarsData, type WebinarData } from '../data/webinarsData';
import { WebcastCTABanner } from './WebcastCTABanner';
import {
Search,
Calendar,
Clock,
Users,
Play,
ArrowRight,
ChevronLeft,
ChevronRight,
Clock,
Eye,
Filter,
Grid,
List,
SortAsc,
Eye,
Play,
Search,
Star,
ChevronLeft,
ChevronRight,
Users,
X
} from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { useWebinarListQuery, type WebinarItem } from '../redux/services/webinarApi';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { FullScreenLoader } from './FullScreenLoader';
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 { Slider } from './ui/slider';
import { WebcastCTABanner } from './WebcastCTABanner';
// Status options with proper mapping to API values
const statusOptions = [
{ value: 'scheduled', label: '📅 Scheduled', color: 'bg-blue-100 text-blue-800 border-blue-200' },
{ value: 'live', label: '🔴 Live', color: 'bg-red-100 text-red-800 border-red-200' },
{ value: 'ended', label: '✅ Ended', color: 'bg-gray-100 text-gray-800 border-gray-200' },
{ value: 'cancelled', label: '❌ Cancelled', color: 'bg-red-50 text-red-600 border-red-200' }
];
const sortOptions = [
{ value: 'most_popular', label: 'Most Popular' },
{ value: 'newest', label: 'Newest First' },
{ value: 'oldest', label: 'Oldest First' },
{ value: 'title', label: 'Title A-Z' },
{ value: 'duration', label: 'Duration' }
];
// Static tags for all webinars
const staticTags = ['Leadership', 'Executive Development', 'Strategy', 'Innovation', 'Change Management', 'Business Growth', 'Team Building', 'Digital Transformation'];
export function Webinars() {
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('All Categories');
const [selectedFormat, setSelectedFormat] = useState('All Formats');
const [selectedLevel, setSelectedLevel] = useState('All Levels');
// Updated state for multi-select status pills
const [selectedStatuses, setSelectedStatuses] = useState<string[]>([]);
// Updated state for duration slider (min, max in minutes)
const [durationRange, setDurationRange] = useState([0, 120]);
// Attendee range slider state
const [attendeeRange, setAttendeeRange] = useState([0, 5000]);
const [sortBy, setSortBy] = useState('Most Popular');
const [sortBy, setSortBy] = useState('most_popular');
const [viewType, setViewType] = useState<'grid' | 'list'>('grid');
const [currentPage, setCurrentPage] = useState(1);
const webinarsPerPage = 6;
const containerRef = useRef<HTMLDivElement>(null);
// Use shared webinars data instead of local mock data
const webinars = sharedWebinarsData;
// Get unique values for filters from shared data
const categories = ['All Categories', ...Array.from(new Set(webinars.map(webinar => webinar.category)))];
const formats = ['All Formats', ...Array.from(new Set(webinars.map(webinar => webinar.format)))];
const levels = ['All Levels', ...Array.from(new Set(webinars.map(webinar => webinar.level)))];
// Status options for pills - updated to match shared data structure
const statusOptions = [
{ value: 'upcoming', label: '📅 Upcoming', color: 'bg-blue-100 text-blue-800 border-blue-200' },
{ value: 'live', label: '🔴 Live', color: 'bg-red-100 text-red-800 border-red-200' },
{ value: 'recorded', label: '▶️ Recorded', color: 'bg-green-100 text-green-800 border-green-200' },
{ value: 'featured', label: '⭐ Featured', color: 'bg-yellow-100 text-yellow-800 border-yellow-200' }
];
const sortOptions = [
{ value: 'Most Popular', label: 'Most Popular' },
{ value: 'newest', label: 'Newest First' },
{ value: 'oldest', label: 'Oldest First' },
{ value: 'title', label: 'Title A-Z' },
{ value: 'duration', label: 'Duration' }
];
// Helper function to convert attendees string to number
const parseAttendees = (attendeesStr: string): number => {
const numStr = attendeesStr.replace(/[^\d]/g, '');
return parseInt(numStr) || 0;
};
// Helper function to convert duration string to minutes
const parseDuration = (durationStr: string): number => {
const numStr = durationStr.replace(/[^\d]/g, '');
return parseInt(numStr) || 0;
};
// Filter and sort webinars
const filteredWebinars = webinars.filter(webinar => {
const matchesSearch = webinar.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
webinar.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
webinar.presenter.toLowerCase().includes(searchTerm.toLowerCase()) ||
webinar.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesCategory = selectedCategory === 'All Categories' || webinar.category === selectedCategory;
const matchesFormat = selectedFormat === 'All Formats' || webinar.format === selectedFormat;
const matchesLevel = selectedLevel === 'All Levels' || webinar.level === selectedLevel;
const matchesStatus = selectedStatuses.length === 0 ||
selectedStatuses.some(status => {
if (status === 'featured') return webinar.featured;
return webinar.status === status;
});
const durationMinutes = parseDuration(webinar.duration);
const matchesDuration = durationMinutes >= durationRange[0] && durationMinutes <= durationRange[1];
const attendeeCount = parseAttendees(webinar.attendees);
const matchesAttendees = attendeeCount >= attendeeRange[0] && attendeeCount <= attendeeRange[1];
return matchesSearch && matchesCategory && matchesFormat && matchesLevel && matchesStatus && matchesDuration && matchesAttendees;
}).sort((a, b) => {
switch (sortBy) {
case 'Most Popular':
// Add logic for "Most Popular" - you might want to use views, attendees, or featured status
return (b.featured ? 1 : 0) - (a.featured ? 1 : 0) ||
parseAttendees(b.attendees) - parseAttendees(a.attendees);
case 'newest':
return new Date(b.date).getTime() - new Date(a.date).getTime();
case 'oldest':
return new Date(a.date).getTime() - new Date(b.date).getTime();
case 'title':
return a.title.localeCompare(b.title);
case 'duration':
return parseDuration(b.duration) - parseDuration(a.duration);
default:
return 0;
}
// Fetch webinars from API
const {
data: webinarResponse,
isLoading,
isError,
} = useWebinarListQuery({
limit: 100,
offset: 0,
search: searchTerm || undefined,
status: selectedStatuses.length > 0 ? selectedStatuses : undefined,
minDuration: durationRange[0] > 0 ? durationRange[0] : undefined,
maxDuration: durationRange[1] < 120 ? durationRange[1] : undefined,
minAttendees: attendeeRange[0] > 0 ? attendeeRange[0] : undefined,
maxAttendees: attendeeRange[1] < 5000 ? attendeeRange[1] : undefined,
sortBy: sortBy as any,
});
// Statistics
const stats = {
total: webinars.length,
upcoming: webinars.filter(w => w.status === 'upcoming').length,
live: webinars.filter(w => w.status === 'live').length,
recorded: webinars.filter(w => w.status === 'recorded').length,
featured: webinars.filter(w => w.featured).length,
categories: new Set(webinars.map(w => w.category)).size
const webinars = webinarResponse?.data?.items || [];
// Get random tags for each webinar (3 random tags from staticTags)
const getRandomTags = (seed: string) => {
// Use the webinar ID as seed to get consistent tags for each webinar
const hash = seed.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const shuffled = [...staticTags];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled.slice(0, 3);
};
// Paginate results
const totalPages = Math.ceil(filteredWebinars.length / webinarsPerPage);
const currentWebinars = filteredWebinars.slice((currentPage - 1) * webinarsPerPage, currentPage * webinarsPerPage);
console.log('Filtered webinars:', filteredWebinars.length);
console.log('Total pages:', totalPages);
console.log('Current page:', currentPage);
console.log('Current webinars count:', currentWebinars.length);
// Get unique categories from API data
const categories = [
'All Categories',
...Array.from(new Set(webinars.map(webinar => webinar.session_title?.split(' ')[0] || 'General')))
];
// Helper functions
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
@@ -154,26 +103,81 @@ console.log('Current webinars count:', currentWebinars.length);
});
};
const formatDuration = (minutes: number) => {
if (minutes >= 60) {
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
}
return `${minutes}min`;
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'live':
return <Badge className="bg-red-600 text-white animate-pulse">LIVE NOW</Badge>;
case 'scheduled':
return <Badge className="bg-blue-600 text-white">SCHEDULED</Badge>;
case 'ended':
return <Badge className="bg-gray-600 text-white">ENDED</Badge>;
case 'cancelled':
return <Badge className="bg-red-400 text-white">CANCELLED</Badge>;
default:
return null;
}
};
const getActionText = (status: string) => {
switch (status) {
case 'live':
return 'Join Now';
case 'scheduled':
return 'Register';
case 'ended':
return 'Watch Recording';
case 'cancelled':
return 'Cancelled';
default:
return 'Learn More';
}
};
// Statistics
const stats = {
total: webinars.length,
scheduled: webinars.filter(w => w.webinar_status === 'scheduled').length,
live: webinars.filter(w => w.webinar_status === 'live').length,
ended: webinars.filter(w => w.webinar_status === 'ended').length,
cancelled: webinars.filter(w => w.webinar_status === 'cancelled').length,
categories: categories.length - 1
};
// Filter webinars
const filteredWebinars = webinars.filter(webinar => {
const matchesCategory = selectedCategory === 'All Categories' ||
(webinar.session_title && webinar.session_title.toLowerCase().includes(selectedCategory.toLowerCase()));
return matchesCategory;
});
// Paginate results
const totalPages = Math.ceil(filteredWebinars.length / webinarsPerPage);
const currentWebinars = filteredWebinars.slice((currentPage - 1) * webinarsPerPage, currentPage * webinarsPerPage);
const clearAllFilters = () => {
setSearchTerm('');
setSelectedCategory('All Categories');
setSelectedFormat('All Formats');
setSelectedLevel('All Levels');
setSelectedStatuses([]);
setDurationRange([0, 120]);
setAttendeeRange([0, 5000]);
setSortBy('Most Popular');
setSortBy('most_popular');
};
const hasActiveFilters = searchTerm ||
selectedCategory !== 'All Categories' ||
selectedFormat !== 'All Formats' ||
selectedLevel !== 'All Levels' ||
selectedStatuses.length > 0 ||
durationRange[0] !== 0 || durationRange[1] !== 120 ||
attendeeRange[0] !== 0 || attendeeRange[1] !== 5000;
// Status pill toggle function
const toggleStatus = (status: string) => {
setSelectedStatuses(prev =>
prev.includes(status)
@@ -182,95 +186,78 @@ console.log('Current webinars count:', currentWebinars.length);
);
};
// Reset to page 1 when filters change
useEffect(() => {
setCurrentPage(1);
}, [searchTerm, selectedCategory, selectedFormat, selectedLevel, selectedStatuses, durationRange, attendeeRange, sortBy]);
}, [searchTerm, selectedCategory, selectedStatuses, durationRange, attendeeRange, sortBy]);
// Updated WebinarCard component that navigates to consistent route
const WebinarCard = ({ webinar }: { webinar: WebinarData }) => {
const WebinarCard = ({ webinar }: { webinar: WebinarItem }) => {
const handleCardClick = () => {
// Navigate to consistent webinar detail route
navigateTo(`/webinar/${webinar.slug}`);
};
const getStatusBadge = () => {
switch (webinar.status) {
case 'live':
return <Badge className="bg-red-600 text-white animate-pulse">LIVE</Badge>;
case 'upcoming':
return <Badge className="bg-blue-600 text-white">UPCOMING</Badge>;
case 'recorded':
return <Badge className="bg-green-600 text-white">RECORDED</Badge>;
default:
return null;
if (webinar.webinar_status !== 'cancelled') {
navigateTo(`/webinar/${webinar.id}`);
}
};
const getActionText = () => {
switch (webinar.status) {
case 'live':
return 'Join Now';
case 'upcoming':
return 'Register';
case 'recorded':
return 'Watch Recording';
default:
return 'Learn More';
}
};
const isCancelled = webinar.webinar_status === 'cancelled';
const webinarTags = getRandomTags(webinar.id);
if (viewType === 'list') {
return (
<Card
className="mb-4 cursor-pointer transition-all duration-300 hover:shadow-lg hover:transform hover:-translate-y-1"
className={`mb-4 cursor-pointer transition-all duration-300 hover:shadow-lg hover:transform hover:-translate-y-1 ${isCancelled ? 'opacity-75' : ''}`}
onClick={handleCardClick}
style={isCancelled ? { cursor: 'not-allowed' } : {}}
>
<CardContent className="p-6">
<div className="flex gap-6">
{/* Thumbnail */}
<div className="flex-shrink-0 w-32 h-24 rounded-lg overflow-hidden">
<ImageWithFallback
src={webinar.thumbnail}
alt={webinar.title}
className="w-full h-full object-cover"
/>
<div className="flex-shrink-0 w-32 h-24 rounded-lg overflow-hidden bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center">
<Play className="w-8 h-8 text-gray-400" />
</div>
{/* Content */}
<div className="flex-1">
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-2">
{getStatusBadge()}
{webinar.featured && (
<Badge className="bg-yellow-100 text-yellow-800">Featured</Badge>
)}
{getStatusBadge(webinar.webinar_status)}
</div>
<span className="text-small text-gray-500">{formatDate(webinar.date)}</span>
<span className="text-small text-gray-500">{formatDate(webinar.session_datetime)}</span>
</div>
<h3 className="text-h4 mb-2 line-clamp-2">{webinar.title}</h3>
<p className="text-body text-gray-600 mb-3 line-clamp-2">{webinar.description}</p>
<h3 className="text-h4 mb-2 line-clamp-2">{webinar.session_title}</h3>
<p className="text-body text-gray-600 mb-3 line-clamp-2">
{webinar.description || 'No description available'}
</p>
{/* Tags */}
<div className="flex flex-wrap gap-2 mb-3">
{webinarTags.map((tag, idx) => (
<Badge key={idx} variant="outline" className="text-xs bg-gray-50">
{tag}
</Badge>
))}
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 text-small text-gray-500">
<span className="flex items-center gap-1">
<Users className="w-4 h-4" />
{webinar.presenter}
{webinar.owner || 'Kautilya Leadership'}
</span>
<span className="flex items-center gap-1">
<Clock className="w-4 h-4" />
{webinar.duration}
{formatDuration(webinar.duration_minutes)}
</span>
<span className="flex items-center gap-1">
<Eye className="w-4 h-4" />
{webinar.attendees}
Max {webinar.max_attendee.toLocaleString()}
</span>
</div>
<div className="flex items-center gap-2 text-primary font-medium">
<span className="text-small">{getActionText()}</span>
<ArrowRight className="w-4 h-4" />
</div>
{!isCancelled && (
<div className="flex items-center gap-2 font-medium" style={{ color: '#04045b' }}>
<span className="text-small">{getActionText(webinar.webinar_status)}</span>
<ArrowRight className="w-4 h-4" />
</div>
)}
</div>
</div>
</div>
@@ -279,100 +266,122 @@ console.log('Current webinars count:', currentWebinars.length);
);
}
// Grid View
return (
<Card
className="cursor-pointer transition-all duration-300 hover:shadow-lg hover:transform hover:-translate-y-2 group overflow-hidden"
className={`cursor-pointer transition-all duration-300 hover:shadow-lg hover:transform hover:-translate-y-2 group overflow-hidden ${isCancelled ? 'opacity-75' : ''}`}
onClick={handleCardClick}
style={isCancelled ? { cursor: 'not-allowed' } : {}}
>
{/* Image */}
<div className="aspect-video relative overflow-hidden">
<ImageWithFallback
src={webinar.thumbnail}
alt={webinar.title}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div className="aspect-video relative overflow-hidden bg-gradient-to-br from-gray-100 to-gray-200">
<div className="w-full h-full flex items-center justify-center">
<Play className="w-12 h-12 text-gray-400" />
</div>
{/* Status Badge */}
<div className="absolute top-4 left-4">
{getStatusBadge()}
{getStatusBadge(webinar.webinar_status)}
</div>
{/* Featured Badge */}
{webinar.featured && (
<div className="absolute top-4 right-4">
<Badge className="bg-yellow-100 text-yellow-800 border border-yellow-200">
<Star className="w-3 h-3 mr-1" />
Featured
</Badge>
</div>
)}
{/* Play Icon Overlay */}
<div className="absolute inset-0 bg-black bg-opacity-40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
<div className="bg-white bg-opacity-90 rounded-full p-3">
<Play className="w-6 h-6 text-gray-800" />
{!isCancelled && (
<div className="absolute inset-0 bg-black bg-opacity-40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
<div className="bg-white bg-opacity-90 rounded-full p-3">
<Play className="w-6 h-6 text-gray-800" />
</div>
</div>
</div>
)}
</div>
{/* Content */}
<CardContent className="p-6">
<div className="flex items-center justify-between mb-2">
<Badge variant="secondary" className="text-xs">
{webinar.category}
{webinar.recurring_webinar ? 'Recurring' : 'One-time'}
</Badge>
<span className="text-small text-gray-500">{formatDate(webinar.date)}</span>
<span className="text-small text-gray-500">{formatDate(webinar.session_datetime)}</span>
</div>
<h3 className="text-h4 mb-3 line-clamp-2 group-hover:text-primary transition-colors">
{webinar.title}
{webinar.session_title}
</h3>
<p className="text-body text-gray-600 mb-4 line-clamp-2">
{webinar.description}
{webinar.description || 'No description available'}
</p>
{/* Tags */}
<div className="flex flex-wrap gap-2 mb-4">
{webinarTags.slice(0, 2).map((tag, idx) => (
<Badge key={idx} variant="outline" className="text-xs bg-gray-50">
{tag}
</Badge>
))}
</div>
<div className="space-y-3">
<div className="flex items-center gap-2 text-small text-gray-500">
<Users className="w-4 h-4" />
<span>{webinar.presenter}</span>
<span>{webinar.owner || 'Kautilya Leadership'}</span>
</div>
<div className="flex items-center justify-between text-small text-gray-500">
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span>{webinar.duration}</span>
<span>{formatDuration(webinar.duration_minutes)}</span>
</div>
<div className="flex items-center gap-1">
<Eye className="w-4 h-4" />
<span>{webinar.attendees}</span>
<span>Max {webinar.max_attendee.toLocaleString()}</span>
</div>
</div>
</div>
<div className="flex items-center justify-between mt-4 pt-4 border-t">
<div className="flex items-center gap-1">
{webinar.tags.slice(0, 2).map((tag, index) => (
<Badge key={index} variant="outline" className="text-xs">
{tag}
</Badge>
))}
{!isCancelled && (
<div className="flex items-center justify-between mt-4 pt-4 border-t">
<div className="flex items-center gap-1 text-xs text-gray-500">
{webinar.require_registration && (
<>
<Star className="w-3 h-3 text-yellow-500" />
<span>Registration Required</span>
</>
)}
</div>
<div className="flex items-center gap-2 font-medium group-hover:translate-x-1 transition-transform" style={{ color: '#04045b' }}>
<span className="text-small">{getActionText(webinar.webinar_status)}</span>
<ArrowRight className="w-4 h-4" />
</div>
</div>
<div className="flex items-center gap-2 text-primary font-medium group-hover:translate-x-1 transition-transform">
<span className="text-small">{getActionText()}</span>
<ArrowRight className="w-4 h-4" />
</div>
</div>
)}
</CardContent>
</Card>
);
};
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<FullScreenLoader text="Loading webinars..." />
</div>
);
}
if (isError) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<div className="text-center">
<p className="text-red-600 mb-4">Error loading webinars. Please try again later.</p>
<Button onClick={() => window.location.reload()}>Retry</Button>
</div>
</div>
);
}
return (
<div style={{ backgroundColor: '#FFFFFF' }}>
{/* Hero Section with Background Image */}
{/* Hero Section */}
<section className="relative h-[400px] overflow-hidden">
{/* Background Image */}
<div className="absolute inset-0">
<ImageWithFallback
src="https://images.unsplash.com/photo-1652265540589-46f91535337b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxidXNpbmVzcyUyMHByZXNlbnRhdGlvbiUyMHdlYmluYXIlMjBjb25mZXJlbmNlfGVufDF8fHx8MTc1NTg1NDI3MHww&ixlib=rb-4.1.0&q=80&w=1080"
@@ -382,14 +391,12 @@ console.log('Current webinars count:', currentWebinars.length);
<div className="absolute inset-0 bg-black/60" />
</div>
{/* Hero Content */}
<div className="relative h-full flex flex-col justify-center section-margin-x">
<div className="text-center">
<h1 className="text-h1-white mb-6">
Leadership Webcasts &<br />
Expert Insights
</h1>
<p className="text-body-lg-white max-w-3xl mx-auto">
Explore our comprehensive collection of expert insights, research, and practical guidance
to elevate your leadership journey and drive organizational excellence.
@@ -397,8 +404,7 @@ console.log('Current webinars count:', currentWebinars.length);
</div>
</div>
{/* Statistics Strip at Bottom */}
<div className="absolute bottom-0 left-0 right-0">
<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">
@@ -424,12 +430,11 @@ console.log('Current webinars count:', currentWebinars.length);
<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 webcasts..."
placeholder="Search webinars..."
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"
@@ -441,7 +446,6 @@ console.log('Current webinars count:', currentWebinars.length);
/>
</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
@@ -451,7 +455,7 @@ console.log('Current webinars count:', currentWebinars.length);
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
style={{
backgroundColor: viewType === 'grid' ? 'var(--color-primary)' : undefined
backgroundColor: viewType === 'grid' ? '#04045b' : undefined
}}
aria-label="Grid view"
>
@@ -464,7 +468,7 @@ console.log('Current webinars count:', currentWebinars.length);
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
style={{
backgroundColor: viewType === 'list' ? 'var(--color-primary)' : undefined
backgroundColor: viewType === 'list' ? '#04045b' : undefined
}}
aria-label="List view"
>
@@ -489,31 +493,28 @@ console.log('Current webinars count:', currentWebinars.length);
</div>
</section>
{/* Main Content Section with Sidebar */}
{/* Main Content Section */}
<section className="pb-16" style={{ backgroundColor: '#FFFFFF' }}>
<div className="section-margin-x">
<div className="grid grid-cols-12 gap-8">
{/* Left Sidebar - Sticky Filters */}
{/* Left Sidebar 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)' }} />
<Filter className="w-3.5 h-3.5" style={{ color: '#04045b' }} />
</div>
<h3 className="text-body font-semibold text-gray-800">
Filters
</h3>
<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"
className="text-xs px-2 py-1 rounded-md transition-colors"
>
<X className="w-3 h-3 mr-1" />
Clear
@@ -522,67 +523,9 @@ console.log('Current webinars count:', currentWebinars.length);
</div>
</div>
{/* Filter Content */}
<div className="p-4">
<div className="space-y-6">
{/* Category Filter */}
<div className="filter-section">
<label className="block text-small mb-2 font-medium text-gray-700">
Category
</label>
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
<SelectValue placeholder="All Categories" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem key={category} value={category} className="text-small">
{category}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Format Filter */}
<div className="filter-section">
<label className="block text-small mb-2 font-medium text-gray-700">
Format
</label>
<Select value={selectedFormat} onValueChange={setSelectedFormat}>
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
<SelectValue placeholder="All Formats" />
</SelectTrigger>
<SelectContent>
{formats.map((format) => (
<SelectItem key={format} value={format} className="text-small">
{format}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Level Filter */}
<div className="filter-section">
<label className="block text-small mb-2 font-medium text-gray-700">
Level
</label>
<Select value={selectedLevel} onValueChange={setSelectedLevel}>
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
<SelectValue placeholder="All Levels" />
</SelectTrigger>
<SelectContent>
{levels.map((level) => (
<SelectItem key={level} value={level} className="text-small">
{level}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Status Filter - Multi-select Pills */}
{/* Status Filter */}
<div className="filter-section">
<label className="block text-small mb-3 font-medium text-gray-700">
Status
@@ -611,10 +554,10 @@ console.log('Current webinars count:', currentWebinars.length);
)}
</div>
{/* Duration Filter - Slider */}
{/* Duration Filter */}
<div className="filter-section">
<label className="block text-small mb-3 font-medium text-gray-700">
Duration
Duration (minutes)
</label>
<div className="px-2">
<Slider
@@ -629,19 +572,13 @@ console.log('Current webinars count:', currentWebinars.length);
<span>{durationRange[0]} min</span>
<span>{durationRange[1]} min</span>
</div>
<div className="mt-1 text-center text-xs text-gray-400">
{durationRange[0] === 0 && durationRange[1] === 120
? 'All durations'
: `${durationRange[0]}-${durationRange[1]} minutes`
}
</div>
</div>
</div>
{/* Attendee Count Filter - Slider */}
{/* Attendee Filter */}
<div className="filter-section">
<label className="block text-small mb-3 font-medium text-gray-700">
Attendees
Max Attendees
</label>
<div className="px-2">
<Slider
@@ -656,12 +593,6 @@ console.log('Current webinars count:', currentWebinars.length);
<span>{attendeeRange[0].toLocaleString()}</span>
<span>{attendeeRange[1].toLocaleString()}+</span>
</div>
<div className="mt-1 text-center text-xs text-gray-400">
{attendeeRange[0] === 0 && attendeeRange[1] === 5000
? 'Any size'
: `${attendeeRange[0].toLocaleString()}-${attendeeRange[1].toLocaleString()}+`
}
</div>
</div>
</div>
</div>
@@ -672,24 +603,22 @@ console.log('Current webinars count:', currentWebinars.length);
{/* Right Main Content */}
<div className="col-span-12 lg:col-span-9">
{/* Results Header */}
<div className="flex items-center justify-between mb-6">
<div className="text-body text-gray-600">
Showing {currentWebinars.length} of {filteredWebinars.length} webcasts
Showing {currentWebinars.length} of {filteredWebinars.length} webinars
</div>
<div className="text-small text-gray-500">
Page {currentPage} of {totalPages}
</div>
</div>
{/* Content Area */}
<div ref={containerRef}>
{currentWebinars.length === 0 ? (
<div className="text-center py-12">
<div className="text-gray-400 mb-4">
<Search className="w-12 h-12 mx-auto mb-4" />
</div>
<h3 className="text-h4 mb-2">No webcasts found</h3>
<h3 className="text-h4 mb-2">No webinars found</h3>
<p className="text-body text-gray-600 mb-4">
Try adjusting your filters or search terms
</p>
@@ -701,7 +630,6 @@ console.log('Current webinars count:', currentWebinars.length);
</div>
) : (
<>
{/* Grid View */}
{viewType === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 mb-8">
{currentWebinars.map((webinar) => (
@@ -709,7 +637,6 @@ console.log('Current webinars count:', currentWebinars.length);
))}
</div>
) : (
/* List View */
<div className="space-y-4 mb-8">
{currentWebinars.map((webinar) => (
<WebinarCard key={webinar.id} webinar={webinar} />
@@ -723,64 +650,46 @@ console.log('Current webinars count:', currentWebinars.length);
<Button
variant="outline"
size="sm"
onClick={() => {
setCurrentPage(prev => Math.max(1, prev - 1));
containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}}
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
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) => {
{Array.from({ length: Math.min(totalPages, 5) }, (_, 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-blue-600 text-white hover:bg-blue-700'
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
}`}
onClick={() => setCurrentPage(page)}
className="min-w-10"
style={currentPage === page ? { backgroundColor: '#04045b' } : {}}
>
{page}
</Button>
);
})}
{totalPages > 5 && <span className="px-2">...</span>}
{totalPages > 5 && (
<Button
variant={currentPage === totalPages ? "default" : "outline"}
size="sm"
onClick={() => setCurrentPage(totalPages)}
className="min-w-10"
style={currentPage === totalPages ? { backgroundColor: '#04045b' } : {}}
>
{totalPages}
</Button>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={() => {
setCurrentPage(prev => Math.min(totalPages, prev + 1));
containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}}
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
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" />
@@ -795,7 +704,6 @@ console.log('Current webinars count:', currentWebinars.length);
</div>
</section>
{/* Webcast CTA Banner */}
<WebcastCTABanner />
</div>
);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog@1.1.6";
import { XIcon } from "lucide-react@0.487.0";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "./utils";

View File

@@ -12,6 +12,7 @@ import { PrimaryCTAButton } from "../components/PrimaryCTAButton";
import { BrandedTag } from "../components/about/BrandedTag";
import { useNavigate } from "react-router-dom";
import { useGetHomepageQuery } from "../redux/services/homepageApi";
import { FullScreenLoader } from "../components/FullScreenLoader";
const HomePage: React.FC = () => {
const navigate = useNavigate();
@@ -24,6 +25,27 @@ const HomePage: React.FC = () => {
const stats = data?.stats_sections ?? [];
const highlightCards = data?.highlight_cards ?? [];
const ctaBands = data?.cta_bands ?? [];
const ctaSection = data?.cta_section;
// Transform testimonial section data to match Testimonial interface
const testimonialData = data?.testimonial_section?.map((item: any) => ({
id: item.id,
name: item.name,
role: item.designation,
quote: item.content,
videoUrl: item.video_url,
isVideo: !!item.video_url,
rating: 5, // Default rating, can be updated from API if available
avatar: item.profile_xid ? `https://example.com/avatars/${item.profile_xid}.jpg` : undefined,
})) || [];
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<FullScreenLoader text="Loading Homepage..." />
</div>
);
}
return (
<>
@@ -92,9 +114,15 @@ const HomePage: React.FC = () => {
<VirtualSpaceSection />
</div>
<TestimonialsSection />
{/* Pass testimonial data to the TestimonialsSection */}
<TestimonialsSection
customTestimonials={testimonialData}
title="What Our Clients Say"
subtitle="Hear from industry leaders who have transformed their organizations with our solutions."
tagText="Client Stories"
/>
<InsightsSection />
<CTABannerSection ctaBands={ctaBands} isLoading={isLoading} />
<CTABannerSection ctaSection={ctaSection} isLoading={isLoading} />
</>
);
};

View File

@@ -0,0 +1,17 @@
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}

View File

@@ -20,6 +20,17 @@ export interface HowWeWorkItem {
display_order: number;
}
export interface Testimonial {
id: string;
profile_xid: string;
name: string;
designation: string;
content: string;
video_url: string;
display_order: number;
testimonial_page_type: string;
}
export interface StatItem {
id: string;
number: number;
@@ -36,6 +47,16 @@ export interface TeamMember {
alt_text: string;
bio: string;
}
export interface CtaData {
id: string;
background_image_url: string;
text: string;
cta_text: string;
cta_destination: string;
description: string;
landing_page_type: string;
service_type: string | null;
}
export interface AboutUsData {
hero_section: HeroSection;
@@ -43,11 +64,37 @@ export interface AboutUsData {
how_we_work_title: string;
who_we_are_title: string;
our_team_title: string;
our_team_description: string;
how_we_work: HowWeWorkItem[];
stat_section: StatItem[];
our_team: TeamMember[];
methodology: Methodology;
philosophy: Philosophy;
testimonials: Testimonial[];
cta_section: CtaData;
}
export interface Methodology {
title: string;
subtitle: string;
phases: Phase[];
}
export interface Phase {
id?: string;
phase_number: number;
phase_label: string;
title: string;
description: string;
bullet_title: string;
bullets: string[];
display_order: number;
}
export interface Philosophy {
title: string;
description: string;
points: string[];
}
export interface AboutUsResponse {
success: boolean;
status: number;

View File

@@ -0,0 +1,371 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import baseQueryWithReauth from "./baseQuery";
/* ================= TYPES ================= */
export type CourseStatus =
| "publish"
| "unpublish"
| "archive"
| "processing"
| "in_draft";
export interface GetCoursesParams {
limit?: number;
offset?: number;
status?: CourseStatus;
search_query?: string;
course_category?: string[];
price_range?: string;
duration_range?: string;
min_rating?: number;
sort_by?: string;
}
export interface Course {
id: string;
course_name: string;
course_desc: string;
thumbnail_img: string;
course_category_xid: string;
course_category_name: string;
best_value: number;
avg_rating: number;
total_reviews: number;
retail_type: string;
price: number;
is_certificate_available: boolean;
course_status: CourseStatus;
updated_at: string;
total_duration: number;
no_of_modules: number;
}
export interface PaginationInfo {
total_count: number;
limit: number;
offset: number;
applied_filters: {
status: string | null;
course_category_xid: string[] | null;
content_types_xid: string[] | null;
search_query: string | null;
price_range: string | null;
duration_range: string | null;
min_rating: number | null;
sort_by: string | null;
};
}
export interface CourseListResponse {
success: boolean;
status: number;
message: string;
data: {
pagination_info: PaginationInfo;
items: Course[];
};
}
export interface CourseReview {
id: string;
rating: number;
comment: string;
video_url: string | null;
reviewer_name: string;
profile_image: string | null;
bio: string | null;
created_at: string;
}
export interface CourseFaq {
id: string;
question: string;
answer: string;
}
export interface CourseTargetAudience {
id: string;
course_xid: string;
course_icon_xid: string;
title: string;
description: string;
display_order: number;
}
export interface CourseLearningOutcome {
id: string;
course_xid: string;
title: string;
description: string;
display_order: number;
}
export interface CourseLearningStructure {
id: string;
course_xid: string;
title: string;
description: string;
display_order: number;
}
export interface CourseFacultyCredential {
id: string;
course_xid: string;
course_faculty_xid: string;
credential_name: string;
display_order: number;
}
export interface CourseFaculty {
id: string;
course_xid: string;
faculty_name: string;
faculty_title: string;
faculty_organization_name: string;
faculty_biography: string;
display_order: number;
expertises: string[] | null;
credentials: CourseFacultyCredential[];
}
export interface CourseLessonResource {
id: string;
course_xid: string;
module_xid: string;
lesson_xid: string;
content_xid: string;
content_type_xid: string;
content_title: string;
total_duration: number | null;
content_type_name: string;
is_active: boolean;
}
export interface CourseLesson {
id: string;
course_xid: string;
module_xid: string;
lesson_title: string;
lesson_description: string;
is_lock_lesson: boolean;
display_order: number;
lesson_resources: CourseLessonResource[];
}
export interface CourseModule {
id: string;
course_xid: string;
module_name: string;
display_order: number;
lessons: CourseLesson[];
}
export interface CourseResource {
id: string;
course_xid: string;
content_xid: string;
content_type_xid: string;
display_order: number | null;
}
export interface CourseCertificateTemplate {
id: string;
template_name: string;
template_code: string;
display_order: number;
is_active: boolean;
}
export interface CourseCertificate {
id: string;
course_xid: string;
certificate_template_xid: string;
company_logo_url: string | null;
institution_name: string;
program_title: string;
signatory_name: string;
signatory_title: string;
digital_signature_url: string | null;
minimum_pass_percentage: number;
complete_all_lesson_required: boolean;
certificate_template: CourseCertificateTemplate;
}
export interface RecommendedCourse {
id: string;
course_name: string;
course_desc: string;
thumbnail_img: string;
price: string;
best_value: number;
duration: number;
recommended_course_reviews: {
id: string;
rating: number;
comment: string;
reviewer_name: string;
}[];
}
export interface CourseDetail {
id: string;
course_name: string;
course_desc: string;
thumbnail_img: string;
course_category_xid: string;
duration: number;
retail_type: string;
price: string | number;
best_value: number;
target_audience_desc: string | null;
learning_outcomes_desc: string | null;
learning_structure_desc: string | null;
our_methodology_desc: string | null;
is_certificate_available: boolean;
course_status: CourseStatus;
benefit_section: string | null;
learning_section: string | null;
structure_section: string | null;
approach_section: string | null;
faculty_section: string | null;
avg_rating: number;
total_reviews: number;
reviews: CourseReview[];
course_faqs: CourseFaq[];
total_modules: number;
total_lessons: number;
formatted_duration: string;
course_category_name: string;
course_language_xids: string[];
course_target_audiences: CourseTargetAudience[];
course_learning_outcomes: CourseLearningOutcome[];
course_learning_structures: CourseLearningStructure[];
course_facilities: CourseFaculty[];
modules: CourseModule[];
course_resources: CourseResource[];
course_certificate: CourseCertificate | null;
recommended_courses: RecommendedCourse[];
}
export interface CourseDetailResponse {
success: boolean;
status: number;
message: string;
data: CourseDetail;
errors: unknown;
correlation_id: string;
}
/* ================= PREPOPULATE TYPES ================= */
export interface CourseCategory {
id: string;
category_name: string;
category_code: string;
display_order: number;
is_active: boolean;
}
export interface GetCourseCategoriesParams {
limit?: number;
offset?: number;
is_active?: boolean;
}
export interface CourseCategoriesResponse {
success: boolean;
status: number;
message: string;
data: {
pagination_info: {
total_count: number;
limit: number;
offset: number;
};
items: CourseCategory[];
};
}
/* ================= API ================= */
export const courseApi = createApi({
reducerPath: "courseApi",
baseQuery: baseQueryWithReauth,
tagTypes: ["Course", "CourseCategories"],
endpoints: (builder) => ({
// GET Courses
getCourses: builder.query<CourseListResponse, GetCoursesParams | void>({
query: (params) => {
const searchParams = new URLSearchParams();
if (params) {
if (params.limit) searchParams.append("limit", params.limit.toString());
if (params.offset) searchParams.append("offset", params.offset.toString());
if (params.status) searchParams.append("status", params.status);
if (params.search_query) searchParams.append("search_query", params.search_query);
if (params.price_range) searchParams.append("price_range", params.price_range);
if (params.duration_range) searchParams.append("duration_range", params.duration_range);
if (params.min_rating !== undefined)
searchParams.append("min_rating", params.min_rating.toString());
if (params.sort_by) searchParams.append("sort_by", params.sort_by);
// ✅ array support
if (params.course_category?.length) {
params.course_category.forEach((cat) => {
searchParams.append("course_category", cat);
});
}
}
const queryString = searchParams.toString();
return queryString
? `admin/course/list?${queryString}`
: `admin/course/list`;
},
providesTags: (result) =>
result
? [
...result.data.items.map(({ id }) => ({
type: "Course" as const,
id,
})),
{ type: "Course", id: "LIST" },
]
: [{ type: "Course", id: "LIST" }],
}),
// GET Course Categories (Prepopulate)
getCourseCategories: builder.query<CourseCategoriesResponse, GetCourseCategoriesParams | void>({
query: (params) => {
const searchParams = new URLSearchParams();
if (params) {
if (params.limit) searchParams.append("limit", params.limit.toString());
if (params.offset) searchParams.append("offset", params.offset.toString());
if (params.is_active !== undefined) searchParams.append("is_active", params.is_active.toString());
}
const queryString = searchParams.toString();
return queryString
? `admin/prepopulate/course-categories/list?${queryString}`
: `admin/prepopulate/course-categories/list`;
},
providesTags: ["CourseCategories"],
}),
// GET Course By Id
getcoursebyid: builder.query<CourseDetailResponse, string>({
query: (course_id) => `admin/course/${course_id}`,
providesTags: (_result, _error, course_id) => [{ type: "Course", id: course_id }],
}),
}),
});
export const {
useGetCoursesQuery,
useGetCourseCategoriesQuery,
useGetcoursebyidQuery,
} = courseApi;

View File

@@ -28,6 +28,7 @@ export interface StatItem {
/* ================= HIGHLIGHT CARD ================= */
export interface HighlightCard {
id?: string;
card_title: string;
icon_url: string;
accessible_label: string;
@@ -46,6 +47,31 @@ export interface CtaBand {
cta_destination: string;
}
/* ================= TESTIMONIAL TYPES ================= */
export interface TestimonialItem {
id: string;
profile_xid: string;
name: string;
designation: string;
content: string;
video_url: string | null;
display_order: number;
}
/* ================= CTA SECTION TYPES ================= */
export interface CtaSection {
id: string;
background_image_url: string;
text: string;
cta_text: string;
cta_destination: string;
description: string;
landing_page_type: string;
service_type: string | null;
}
/* ================= RESPONSE ================= */
export interface HomePageResponse {
@@ -57,6 +83,8 @@ export interface HomePageResponse {
stats_sections: StatItem[];
highlight_cards: HighlightCard[];
cta_bands: CtaBand[];
cta_section: CtaSection;
testimonial_section: TestimonialItem[];
};
errors: any;
correlation_id: string;

View File

@@ -0,0 +1,122 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import baseQueryWithReauth from "./baseQuery";
export interface KautilyaPageResponse {
success: boolean;
status: number;
message: string;
data: {
hero_sections: {
id: string;
background_image_url: string;
background_image_alt_text: string;
headline: string;
subtext: string;
cta_text: string;
cta_destination: string;
};
our_story: {
id: string;
tag: string;
title: string;
content: string;
image_url: string;
};
why_choose_us: {
id: string;
tag: string;
title: string;
description: string;
cards: Array<{
id: string;
title: string;
description: string;
image_url: string;
icon: string;
display_order: number;
bullets: Array<{
id: string;
text: string;
}>;
}>;
};
facility_features: {
id: string;
title: string;
description: string;
features: Array<{
id: string;
title: string;
description: string;
image_url: string;
sub_title: string;
sub_description: string;
display_order: number;
points: Array<{
id: string;
text: string;
}>;
}>;
};
visual_tour: {
id: string;
title: string;
description: string;
categories: Array<{
id: string;
name: string;
display_order: number;
images: Array<{
id: string;
image_url: string;
title: string;
subtitle: string;
display_order: number;
}>;
}>;
};
daily_experience: {
id: string;
title: string;
description: string;
items: Array<{
id: string;
label: string;
title: string;
description: string;
image_url: string;
display_order: number;
}>;
};
cta_section: {
id: string;
background_image_url: string;
text: string;
cta_text: string;
cta_destination: string;
description: string;
};
};
errors: any;
correlation_id: string;
}
export const learningFacilityApi = createApi({
reducerPath: "learningFacilityApi",
baseQuery: baseQueryWithReauth,
tagTypes: ["KautilyaPage"],
endpoints: (builder) => ({
getKautilyaPage: builder.query<
KautilyaPageResponse["data"],
{ }
>({
query: ({ }) => ({
url: "/admin/kautilya-page/get",
}),
transformResponse: (response: KautilyaPageResponse) => response.data,
providesTags: [{ type: "KautilyaPage", id: "LIST" }],
}),
}),
});
export const { useGetKautilyaPageQuery } = learningFacilityApi;

View File

@@ -0,0 +1,20 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import baseQueryWithReauth from "./baseQuery";
export const sercicesApi = createApi({
reducerPath: "sercicesApi",
baseQuery: baseQueryWithReauth,
tagTypes: ["services"],
endpoints: (builder) => ({
// GET services LIST
getServiceList: builder.query<any, { service_type: string }>({
query: ({ service_type }) => ({
url: `/admin/service-page/list`,
params: { service_type },
}),
}),
}),
});
export const { useGetServiceListQuery } = sercicesApi;

View File

@@ -0,0 +1,128 @@
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import baseQueryWithReauth from "./baseQuery";
/* ================= TYPES ================= */
export interface WebinarItem {
id: string;
session_title: string;
description: string | null;
session_datetime: string;
duration_minutes: number;
timezone_xid: string;
max_attendee: number;
passcode: string;
require_registration: boolean;
recurring_webinar: boolean;
webinar_status: "scheduled" | "live" | "ended" | "cancelled";
owner?: string;
}
export interface WebinarListData {
total: number;
limit: number;
offset: number;
items: WebinarItem[];
}
export interface WebinarListResponse {
success: boolean;
status: number;
message: string;
data: WebinarListData;
errors: any;
correlation_id: string;
}
/* ================= QUERY PARAM TYPE ================= */
export interface WebinarListParams {
limit?: number;
offset?: number;
search?: string;
status?: string[]; // ✅ multiple status
fromDate?: string;
toDate?: string;
minDuration?: number;
maxDuration?: number;
minAttendees?: number; // ✅ NEW
maxAttendees?: number; // ✅ NEW
sortBy?: "most_popular" | "newest" | "oldest" | "title" | "duration";
}
/* ================= API ================= */
export const webinarApi = createApi({
reducerPath: "webinarApi",
baseQuery: baseQueryWithReauth,
tagTypes: ["Webinar"],
endpoints: (builder) => ({
webinarList: builder.query<WebinarListResponse, WebinarListParams>({
query: ({
limit = 10,
offset = 0,
search,
status,
fromDate,
toDate,
minDuration,
maxDuration,
minAttendees,
maxAttendees,
sortBy,
}) => {
const params = new URLSearchParams();
params.append("limit", String(limit));
params.append("offset", String(offset));
if (search) {
params.append("search_term", search); // ✅ FIXED NAME
}
if (status && status.length > 0) {
status.forEach((s) =>
params.append("session_status", s) // ✅ array support
);
}
if (fromDate) {
params.append("from_date", fromDate);
}
if (toDate) {
params.append("to_date", toDate);
}
if (minDuration !== undefined) {
params.append("min_duration", String(minDuration));
}
if (maxDuration !== undefined) {
params.append("max_duration", String(maxDuration));
}
if (minAttendees !== undefined) {
params.append("min_attendee", String(minAttendees)); // ✅ NEW
}
if (maxAttendees !== undefined) {
params.append("max_attendee", String(maxAttendees)); // ✅ NEW
}
if (sortBy) {
params.append("sort_by", sortBy);
}
return `/admin/webinars/list?${params.toString()}`;
},
providesTags: ["Webinar"],
}),
}),
});
/* ================= EXPORT HOOK ================= */
export const { useWebinarListQuery } = webinarApi;

View File

@@ -4,6 +4,10 @@ import { faqApi } from "../services/faqApi";
import { contactUsApi } from "../services/contactUsApi";
import { blogApi } from "../services/blogApi";
import { aboutUsApi } from "../services/aboutUsApi";
import { sercicesApi } from "../services/sercicesApi";
import { courseApi } from "../services/courseApi";
import { learningFacilityApi } from "../services/learningFacilityApi";
import { webinarApi } from "../services/webinarApi";
export const store = configureStore({
reducer: {
@@ -12,6 +16,11 @@ export const store = configureStore({
[contactUsApi.reducerPath]: contactUsApi.reducer,
[blogApi.reducerPath]: blogApi.reducer,
[aboutUsApi.reducerPath]: aboutUsApi.reducer,
[sercicesApi.reducerPath]: sercicesApi.reducer,
[courseApi.reducerPath]: courseApi.reducer,
[learningFacilityApi.reducerPath]: learningFacilityApi.reducer,
[webinarApi.reducerPath]: webinarApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
@@ -20,6 +29,10 @@ export const store = configureStore({
contactUsApi.middleware,
blogApi.middleware,
aboutUsApi.middleware,
sercicesApi.middleware,
courseApi.middleware,
learningFacilityApi.middleware,
webinarApi.middleware,
),
});

View File

@@ -167,6 +167,9 @@
/* Small text */
--font-small: 0.875rem;
/* 14px */
/* Extra small text */
--font-extra-small: 0.75rem;
/* 12px */
--line-height-small: 1.5;
--font-weight-small: 400;
@@ -601,6 +604,14 @@ html {
color: var(--color-black);
}
.text-small-extra {
font-size: var(--font-extra-small);
line-height: var(--line-height-small);
font-weight: var(--font-weight-small);
font-family: var(--font-family-base);
color: var(--color-black);
}
.text-eyebrow {
font-size: var(--font-eyebrow);
line-height: var(--line-height-eyebrow);
@@ -5294,4 +5305,31 @@ html {
font-size: calc(var(--font-h2) * 0.8); /* reduce by 20% */
line-height: calc(var(--line-height-h2) * 0.9);
}
}
}
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 20px;
}
/* Optional: Show scrollbar only on hover */
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
}
.custom-scrollbar:hover {
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}