File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
17
src/redux/hooks/useDebounce.ts
Normal file
17
src/redux/hooks/useDebounce.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
371
src/redux/services/courseApi.ts
Normal file
371
src/redux/services/courseApi.ts
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
122
src/redux/services/learningFacilityApi.ts
Normal file
122
src/redux/services/learningFacilityApi.ts
Normal 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;
|
||||
20
src/redux/services/sercicesApi.ts
Normal file
20
src/redux/services/sercicesApi.ts
Normal 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;
|
||||
128
src/redux/services/webinarApi.ts
Normal file
128
src/redux/services/webinarApi.ts
Normal 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;
|
||||
@@ -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,
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user