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);
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface Course {
|
||||
level: string;
|
||||
format: string;
|
||||
rating: number;
|
||||
participants: string;
|
||||
reviews: string;
|
||||
category: string;
|
||||
description: string;
|
||||
price: string;
|
||||
@@ -89,20 +89,6 @@ 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 */}
|
||||
@@ -161,7 +147,7 @@ export function CourseCard({ course, onClick, className, onAddToCart }: CourseCa
|
||||
color: 'var(--color-gray-muted)',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
{course.participants}
|
||||
{course.reviews}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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,59 +205,61 @@ 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>
|
||||
|
||||
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"
|
||||
|
||||
@@ -1,48 +1,43 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Textarea } from './ui/textarea';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './ui/accordion';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { Separator } from './ui/separator';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
import {
|
||||
Clock,
|
||||
Users,
|
||||
Award,
|
||||
Play,
|
||||
Download,
|
||||
ChevronRight,
|
||||
ChevronLeft,
|
||||
Star,
|
||||
Calendar,
|
||||
Globe,
|
||||
Building2,
|
||||
User,
|
||||
Mail,
|
||||
Phone,
|
||||
MessageCircle,
|
||||
CheckCircle,
|
||||
BookOpen,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Download,
|
||||
ExternalLink,
|
||||
Lightbulb,
|
||||
MessageCircle,
|
||||
Play,
|
||||
Star,
|
||||
Target,
|
||||
TrendingUp,
|
||||
Lightbulb,
|
||||
ArrowRight,
|
||||
ExternalLink,
|
||||
Quote,
|
||||
X,
|
||||
AlertCircle
|
||||
User,
|
||||
Users,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
import { navigateTo } from './Router';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { useGetcoursebyidQuery } from '../redux/services/courseApi';
|
||||
import { BrandedTag } from './about/BrandedTag';
|
||||
import { PrimaryCTAButton } from './PrimaryCTAButton';
|
||||
import { useCart } from './CartContext';
|
||||
import { toast } from 'sonner@2.0.3';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { PrimaryCTAButton } from './PrimaryCTAButton';
|
||||
import { navigateTo } from './Router';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogOverlay,
|
||||
DialogPortal
|
||||
} from './ui/dialog';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
|
||||
// Mock data structure matching API contracts
|
||||
const mockProgrammeData = {
|
||||
@@ -54,6 +49,9 @@ const mockProgrammeData = {
|
||||
format: 'Hybrid',
|
||||
price: '₹2,50,000',
|
||||
originalPrice: '₹3,00,000',
|
||||
methodologyDesc: 'Formulating Strategy is a theme that requires the exercise of leadership. Thus, the course starts with offering you an opportunity to review your lens to leadership and provides alternate perspectives to leadership',
|
||||
reviewCount: 24,
|
||||
rating: 4.8,
|
||||
currency: 'INR',
|
||||
published: true,
|
||||
enrollmentStatus: 'open', // open, closed, enrolled, waitlist
|
||||
@@ -350,11 +348,18 @@ const mockRelatedProgrammes = [
|
||||
}
|
||||
];
|
||||
|
||||
const formatIndianPrice = (value: string | number | null | undefined) => {
|
||||
const amount = Number(value ?? 0);
|
||||
return `₹${amount.toLocaleString('en-IN')}`;
|
||||
};
|
||||
|
||||
interface ProgrammeDetailProps {
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
const params = useParams<{ slug: string }>();
|
||||
const courseId = slug || params.slug || '';
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
const [selectedFaculty, setSelectedFaculty] = useState(0);
|
||||
const [showVideoModal, setShowVideoModal] = useState(false);
|
||||
@@ -370,8 +375,28 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
designation: ''
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const heroRef = useRef<HTMLDivElement>(null);
|
||||
const { addToCart } = useCart();
|
||||
const {
|
||||
data: courseDetailResponse,
|
||||
isLoading,
|
||||
isError
|
||||
} = useGetcoursebyidQuery(courseId, {
|
||||
skip: !courseId
|
||||
});
|
||||
const courseDetail = courseDetailResponse?.data;
|
||||
|
||||
const handleCourseClick = (courseId: string) => {
|
||||
navigate(`/course/${courseId}`);
|
||||
// Optionally scroll to top
|
||||
window.scrollTo(0, 0);
|
||||
};
|
||||
|
||||
|
||||
// Add this with your other useState declarations
|
||||
const [showCertificatePreview, setShowCertificatePreview] = useState(false);
|
||||
|
||||
// Sticky CTA logic
|
||||
useEffect(() => {
|
||||
@@ -386,15 +411,140 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
// Mock API calls (replace with real API calls)
|
||||
const programme = mockProgrammeData;
|
||||
const outcomes = mockOutcomes;
|
||||
const curriculum = mockCurriculum;
|
||||
const faculty = mockFaculty;
|
||||
const testimonials = mockTestimonials;
|
||||
const faqs = mockFAQs;
|
||||
const programme = useMemo(() => {
|
||||
if (!courseDetail) return mockProgrammeData;
|
||||
|
||||
const bestValue = Number(courseDetail.best_value ?? 0);
|
||||
const originalValue = Number(courseDetail.price ?? 0);
|
||||
const computedPrice = formatIndianPrice(bestValue);
|
||||
const computedOriginalPrice =
|
||||
originalValue > 0 && originalValue !== bestValue
|
||||
? formatIndianPrice(originalValue)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...mockProgrammeData,
|
||||
id: courseDetail.id,
|
||||
title: courseDetail.course_name || mockProgrammeData.title,
|
||||
subtitle: courseDetail.course_desc || mockProgrammeData.subtitle,
|
||||
duration: courseDetail.duration || mockProgrammeData.duration,
|
||||
// level: courseDetail.retail_type === 'public' ? 'Public' : 'Private',
|
||||
// format: courseDetail.retail_type === 'public' ? 'Cohort-based' : 'Self-paced',
|
||||
price: computedPrice,
|
||||
originalPrice: computedOriginalPrice,
|
||||
methodologyDesc: courseDetail.our_methodology_desc,
|
||||
reviewCount: courseDetail.total_reviews || 0,
|
||||
rating: Number(courseDetail.avg_rating ?? 0),
|
||||
published: courseDetail.course_status === 'publish',
|
||||
spotsLeft: courseDetail.total_lessons || mockProgrammeData.spotsLeft,
|
||||
maxCapacity: Math.max(courseDetail.total_lessons || 0, mockProgrammeData.maxCapacity),
|
||||
thumbnail: courseDetail.thumbnail_img || mockProgrammeData.thumbnail,
|
||||
badges: [
|
||||
courseDetail.course_category_name,
|
||||
courseDetail.retail_type === 'public' ? 'Public' : 'Private',
|
||||
courseDetail.is_certificate_available ? 'Certified' : 'Non-Certified'
|
||||
].filter(Boolean),
|
||||
previewVideoUrl:
|
||||
courseDetail.reviews.find((review) => review.video_url)?.video_url ||
|
||||
mockProgrammeData.previewVideoUrl,
|
||||
highlights:
|
||||
courseDetail.course_learning_outcomes.slice(0, 5).map((item) => item.title) ||
|
||||
mockProgrammeData.highlights,
|
||||
deliveryMethods: courseDetail.modules.length
|
||||
? courseDetail.modules.map((module) => module.module_name)
|
||||
: mockProgrammeData.deliveryMethods,
|
||||
credentials:
|
||||
courseDetail.course_certificate?.program_title ||
|
||||
courseDetail.course_certificate?.institution_name ||
|
||||
mockProgrammeData.credentials,
|
||||
targetROI: courseDetail.our_methodology_desc || mockProgrammeData.targetROI
|
||||
};
|
||||
}, [courseDetail]);
|
||||
|
||||
const outcomes = useMemo(() => {
|
||||
if (!courseDetail?.course_learning_outcomes?.length) return mockOutcomes;
|
||||
|
||||
return courseDetail.course_learning_outcomes.map((item) => ({
|
||||
title: item.title,
|
||||
description: item.description
|
||||
}));
|
||||
}, [courseDetail]);
|
||||
|
||||
const curriculum = useMemo(() => {
|
||||
if (!courseDetail?.modules?.length) return mockCurriculum;
|
||||
|
||||
return courseDetail.modules.map((module, index) => ({
|
||||
moduleNumber: index + 1,
|
||||
title: module.module_name,
|
||||
duration: `${module.lessons.length} lesson${module.lessons.length === 1 ? '' : 's'}`,
|
||||
deliveryStyle: module.lessons.some((lesson) => !lesson.is_lock_lesson) ? 'Self-Paced' : 'Locked',
|
||||
topics: module.lessons.map((lesson) => lesson.lesson_title)
|
||||
}));
|
||||
}, [courseDetail]);
|
||||
|
||||
const faculty = useMemo(() => {
|
||||
if (!courseDetail?.course_facilities?.length) return mockFaculty;
|
||||
|
||||
return courseDetail.course_facilities.map((member) => ({
|
||||
id: member.id,
|
||||
name: member.faculty_name,
|
||||
title: member.faculty_title,
|
||||
organization: member.faculty_organization_name,
|
||||
bio: member.faculty_biography,
|
||||
image: '',
|
||||
linkedinUrl: '',
|
||||
credentials: member.credentials.map((credential) => credential.credential_name),
|
||||
expertise: member.expertises || []
|
||||
}));
|
||||
}, [courseDetail]);
|
||||
|
||||
const testimonials = useMemo(() => {
|
||||
if (!courseDetail?.reviews?.length) return mockTestimonials;
|
||||
|
||||
return courseDetail.reviews.map((review) => ({
|
||||
id: review.id,
|
||||
type: review.video_url ? 'video' : 'text',
|
||||
name: review.reviewer_name,
|
||||
title: review.bio || 'Learner Review',
|
||||
company: courseDetail.course_category_name,
|
||||
content: review.comment,
|
||||
videoThumbnail: programme.thumbnail,
|
||||
videoUrl: review.video_url || '',
|
||||
rating: review.rating,
|
||||
programmeCompleted: courseDetail.course_name
|
||||
}));
|
||||
}, [courseDetail, programme.thumbnail]);
|
||||
|
||||
const faqs = useMemo(() => {
|
||||
if (!courseDetail?.course_faqs?.length) return mockFAQs;
|
||||
return courseDetail.course_faqs;
|
||||
}, [courseDetail]);
|
||||
|
||||
const audienceSegments = useMemo(() => {
|
||||
if (!courseDetail?.course_target_audiences?.length) return mockAudienceSegments;
|
||||
|
||||
return courseDetail.course_target_audiences.map((segment, index) => ({
|
||||
title: segment.title,
|
||||
description: segment.description,
|
||||
icon: index % 3 === 0
|
||||
? <Award className="w-6 h-6" />
|
||||
: index % 3 === 1
|
||||
? <TrendingUp className="w-6 h-6" />
|
||||
: <Lightbulb className="w-6 h-6" />
|
||||
}));
|
||||
}, [courseDetail]);
|
||||
|
||||
const useCases = useMemo(() => {
|
||||
if (!courseDetail?.modules?.length) return mockUseCases;
|
||||
return courseDetail.modules.flatMap((module) => module.lessons.map((lesson) => lesson.lesson_title)).slice(0, 6);
|
||||
}, [courseDetail]);
|
||||
|
||||
const relatedProgrammes = mockRelatedProgrammes;
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedFaculty(0);
|
||||
}, [courseId]);
|
||||
|
||||
// Handle enrollment based on status
|
||||
const handleEnrollment = (type: string = 'individual') => {
|
||||
if (programme.enrollmentStatus === 'enrolled') {
|
||||
@@ -451,6 +601,33 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center" style={{ backgroundColor: '#FFFFFF' }}>
|
||||
<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 programme details...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !courseDetail) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center" style={{ backgroundColor: '#FFFFFF' }}>
|
||||
<div className="text-center">
|
||||
<h1 className="text-h2 mb-4">Programme Not Found</h1>
|
||||
<p className="text-body-lg text-muted mb-8">
|
||||
We couldn't load the programme details right now.
|
||||
</p>
|
||||
<Button onClick={() => navigateTo('/learning-online')}>
|
||||
Browse All Programmes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle 404 for unpublished programmes
|
||||
if (!programme.published) {
|
||||
return (
|
||||
@@ -533,6 +710,10 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
<Users className="w-3.5 h-3.5 text-primary" />
|
||||
<span className="font-medium text-primary">{programme.spotsLeft} spots left</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1 bg-blue-50 rounded-full">
|
||||
<MessageCircle className="w-3.5 h-3.5 text-primary" />
|
||||
<span className="font-medium text-primary">{programme.reviewCount} reviews</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Price Display */}
|
||||
@@ -566,7 +747,7 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
<div className="relative">
|
||||
<PrimaryCTAButton
|
||||
text={programme.enrollmentStatus === 'enrolled' ? 'Go to Course' :
|
||||
programme.spotsLeft === 0 ? 'Join Waitlist' : 'Enroll Now'}
|
||||
programme.spotsLeft === 0 ? 'Join Waitlist' : 'Enroll Now'}
|
||||
onClick={() => handleEnrollment()}
|
||||
className="cta-text-black h-12 px-8 transition-all duration-200"
|
||||
/>
|
||||
@@ -618,18 +799,22 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
<Clock className="w-3 h-3 mr-2" />
|
||||
Duration: {programme.duration}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="bg-primary/5 text-primary border border-primary/20 px-4 py-2 text-small font-medium hover:bg-primary/10 transition-colors duration-200">
|
||||
{/* <Badge variant="secondary" className="bg-primary/5 text-primary border border-primary/20 px-4 py-2 text-small font-medium hover:bg-primary/10 transition-colors duration-200">
|
||||
<Award className="w-3 h-3 mr-2" />
|
||||
Level: {programme.level}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="bg-primary/5 text-primary border border-primary/20 px-4 py-2 text-small font-medium hover:bg-primary/10 transition-colors duration-200">
|
||||
<Globe className="w-3 h-3 mr-2" />
|
||||
Format: {programme.format}
|
||||
</Badge>
|
||||
</Badge> */}
|
||||
<Badge variant="secondary" className="bg-accent/5 text-accent border border-accent/20 px-4 py-2 text-small font-medium hover:bg-accent/10 transition-colors duration-200">
|
||||
<Users className="w-3 h-3 mr-2" />
|
||||
Spots Left: {programme.spotsLeft}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="bg-primary/5 text-primary border border-primary/20 px-4 py-2 text-small font-medium hover:bg-primary/10 transition-colors duration-200">
|
||||
<MessageCircle className="w-3 h-3 mr-2" />
|
||||
Reviews: {programme.reviewCount}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* REDESIGNED: Professional Badges Section */}
|
||||
@@ -677,7 +862,7 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
<div className="flex gap-4">
|
||||
<PrimaryCTAButton
|
||||
text={programme.enrollmentStatus === 'enrolled' ? 'Go to Course' :
|
||||
programme.spotsLeft === 0 ? 'Join Waitlist' : 'Enroll Now'}
|
||||
programme.spotsLeft === 0 ? 'Join Waitlist' : 'Enroll Now'}
|
||||
onClick={() => handleEnrollment()}
|
||||
className="cta-text-black flex-1 justify-center"
|
||||
/>
|
||||
@@ -748,8 +933,213 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
{/* Credentials */}
|
||||
<div>
|
||||
<h4 className="text-subhead font-semibold mb-2">Certification</h4>
|
||||
<p className="text-body text-muted">{programme.credentials}</p>
|
||||
{courseDetail?.course_certificate ? (
|
||||
<div>
|
||||
{/* Certificate Available Message */}
|
||||
<p className="text-body text-muted mb-4">
|
||||
Certificate Available <span className="text-muted">upon completion</span>
|
||||
</p>
|
||||
<div
|
||||
className="border border-gray-200 rounded-lg p-4 bg-[#fbf9fa] cursor-pointer hover:shadow-md transition-shadow duration-300">
|
||||
{/* Certificate Preview */}
|
||||
<h5 className="text-small font-semibold text-black mb-3">Certificate Preview:</h5>
|
||||
|
||||
{/* Certificate Card - Exact design from image */}
|
||||
<div className="border-2 border-[#04045b] border rounded-lg p-4 bg-white cursor-pointer hover:shadow-md transition-shadow duration-300">
|
||||
<div
|
||||
className="border-2 border-[#04045b] border rounded-lg p-4 bg-white cursor-pointer hover:shadow-md transition-shadow duration-300"
|
||||
onClick={() => setShowCertificatePreview(true)}
|
||||
>
|
||||
<div className="text-center">
|
||||
{/* Title */}
|
||||
<h3 className="text-md font-medium font-serif text-[#04045b] mb-4">Certificate of Completion</h3>
|
||||
|
||||
{/* Body Text */}
|
||||
<p className="text-gray-600 mb-2 text-small-extra">This is to certify that</p>
|
||||
<span className="text-xl font-semibold text-primary mb-2 text-sm border-b border-black">
|
||||
[Learner Name]
|
||||
</span>
|
||||
<p className="text-gray-600 mb-2 text-small-extra">has successfully completed</p>
|
||||
<p className="text-md font-medium text-gray-800 mb-2 text-small">
|
||||
{courseDetail.course_certificate.program_title || programme.title}
|
||||
</p>
|
||||
<p className="text-gray-600 mb-2 text-small-extra">offered by</p>
|
||||
<p className="text-md font-medium text-gray-800 mb-2 text-small">
|
||||
{courseDetail.course_certificate.institution_name || "Kautilya Leadership Centre"}
|
||||
</p>
|
||||
|
||||
{/* Footer with Signature and Date */}
|
||||
<div className="flex justify-between items-center mt-4 pt-4 border-t border-gray-200">
|
||||
<div className="text-left">
|
||||
<div className="mb-1">
|
||||
{courseDetail.course_certificate.digital_signature_url ? (
|
||||
<img
|
||||
src={courseDetail.course_certificate.digital_signature_url}
|
||||
alt="Signature"
|
||||
className="h-6 object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-24 h-10 border-b border-gray-400"></div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">Signature</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-gray-500 mb-1">Date</p>
|
||||
<p className="text-xs text-gray-700">
|
||||
{new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-body text-muted">{programme.credentials}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Certificate Preview Modal - Full View */}
|
||||
{courseDetail?.course_certificate && (
|
||||
<Dialog open={showCertificatePreview} onOpenChange={setShowCertificatePreview}>
|
||||
<DialogPortal>
|
||||
<DialogOverlay className="fixed inset-0 bg-black/70 z-[9999]" />
|
||||
<DialogContent className="fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] w-full max-w-3xl z-[9999] focus:outline-none bg-transparent border-none shadow-none p-0">
|
||||
<div className="relative">
|
||||
{/* <DialogClose asChild>
|
||||
<button
|
||||
className="absolute -top-12 right-0 text-white hover:bg-white/20 rounded-full p-1 z-10 focus:outline-none"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</DialogClose> */}
|
||||
|
||||
{/* Certificate Card - Authentic Certificate Style */}
|
||||
<div className="bg-white rounded-lg shadow-2xl overflow-hidden max-h-[90vh] overflow-y-auto custom-scrollbar">
|
||||
{/* Decorative Border */}
|
||||
<div className="p-1 bg-gradient-to-r from-amber-400 via-yellow-500 to-amber-400">
|
||||
<div className="bg-white p-2">
|
||||
{/* Inner Border */}
|
||||
<div className="border-2 border-amber-200 p-6 md:p-10">
|
||||
{/* Certificate Content */}
|
||||
<div className="text-center">
|
||||
{/* Decorative Top Border */}
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="h-1 w-20 bg-amber-400 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
{/* Certificate Title */}
|
||||
<h2 className="text-3xl md:text-4xl font-serif font-bold text-gray-800 mb-2">
|
||||
Certificate of Completion
|
||||
</h2>
|
||||
|
||||
{/* Decorative Line */}
|
||||
<div className="w-24 h-0.5 bg-amber-400 mx-auto mb-6"></div>
|
||||
|
||||
{/* Body Text */}
|
||||
<p className="text-gray-600 text-lg mb-4">This is to certify that</p>
|
||||
|
||||
{/* Learner Name */}
|
||||
<p className="text-3xl md:text-4xl font-serif font-bold text-amber-700 mb-4 border-b-2 border-amber-200 inline-block px-8 pb-2">
|
||||
[Learner Name]
|
||||
</p>
|
||||
|
||||
<p className="text-gray-600 text-lg mt-6 mb-4">has successfully completed</p>
|
||||
|
||||
{/* Program Title */}
|
||||
<p className="text-2xl md:text-3xl font-serif font-semibold text-gray-800 mb-4">
|
||||
{courseDetail.course_certificate.program_title || programme.title}
|
||||
</p>
|
||||
|
||||
<p className="text-gray-600 text-lg mb-4">offered by</p>
|
||||
|
||||
{/* Institution Name */}
|
||||
<p className="text-2xl md:text-3xl font-serif font-semibold text-gray-800 mb-8">
|
||||
{courseDetail.course_certificate.institution_name || "Kautilya Leadership Centre"}
|
||||
</p>
|
||||
|
||||
{/* Certificate ID */}
|
||||
{courseDetail.course_certificate.id && (
|
||||
<div className="mb-6">
|
||||
<p className="text-xs text-gray-400">
|
||||
Certificate ID: {courseDetail.course_certificate.id}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer with Signature and Date */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-6 mt-8 pt-6 border-t border-amber-200">
|
||||
{/* Signature Section */}
|
||||
<div className="text-center md:text-left flex-1">
|
||||
{courseDetail.course_certificate.digital_signature_url ? (
|
||||
<div className="mb-2">
|
||||
<img
|
||||
src={courseDetail.course_certificate.digital_signature_url}
|
||||
alt="Signature"
|
||||
className="h-14 object-contain mx-auto md:mx-0"
|
||||
/>
|
||||
<div className="w-32 h-px bg-gray-300 mx-auto md:mx-0 mt-1"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-2">
|
||||
<div className="w-32 h-12 border-b border-gray-400 mx-auto md:mx-0"></div>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm font-medium text-gray-700">Authorized Signature</p>
|
||||
{courseDetail.course_certificate.signatory_name && (
|
||||
<p className="text-xs text-gray-500 mt-1">{courseDetail.course_certificate.signatory_name}</p>
|
||||
)}
|
||||
{courseDetail.course_certificate.signatory_title && (
|
||||
<p className="text-xs text-gray-400">{courseDetail.course_certificate.signatory_title}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Date Section */}
|
||||
<div className="text-center flex-1">
|
||||
<p className="text-sm font-medium text-gray-700 mb-1">Date of Issue</p>
|
||||
<p className="text-base text-gray-800 font-serif">
|
||||
{new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Seal/Logo Section */}
|
||||
<div className="text-center flex-1">
|
||||
{courseDetail.course_certificate.company_logo_url ? (
|
||||
<img
|
||||
src={courseDetail.course_certificate.company_logo_url}
|
||||
alt="Institution Logo"
|
||||
className="h-16 w-16 object-contain mx-auto"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-full border-2 border-amber-400 flex items-center justify-center mx-auto">
|
||||
<span className="text-2xl">🎓</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative Bottom Border */}
|
||||
<div className="flex justify-center mt-8">
|
||||
<div className="h-1 w-20 bg-amber-400 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
{/* Verification Note */}
|
||||
<p className="text-xs text-gray-400 mt-6">
|
||||
This certificate is digitally verified and can be authenticated online
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</Dialog>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -824,7 +1214,7 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
|
||||
{mockAudienceSegments.map((segment, index) => (
|
||||
{audienceSegments.map((segment, index) => (
|
||||
<div key={index} className="text-center p-6 bg-gray-50 rounded-xl">
|
||||
<div className="w-16 h-16 bg-[#04045B] rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<div className="text-white">
|
||||
@@ -1013,19 +1403,7 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
<h3 className="text-h4 font-semibold mb-4">Our Methodology</h3>
|
||||
<div className="space-y-4 text-body text-muted leading-relaxed">
|
||||
<p>
|
||||
Formulating Strategy is a theme that requires the exercise of leadership. Thus, the course starts with offering you an opportunity to review your lens to leadership and provides alternate perspectives to leadership.
|
||||
</p>
|
||||
<p>
|
||||
You will be effective in the exercise of leadership based the leadership resources you possess and your ability to apply those resources to suit the situation. We refer to these leadership resources as Leadership Orientations. Orientations are characteristics or traits. In this course you will learn the leadership orientations that have an impact on Formulating Strategies.
|
||||
</p>
|
||||
<p>
|
||||
The course will help you be self-aware and gain insights on your leadership orientations. You will learn the leadership abilities that have an impact on strategy formulation. Leadership abilities require the combined applying of the leadership orientations learnt.
|
||||
</p>
|
||||
<p>
|
||||
Developing these leadership orientations and leadership abilities will help you think strategically. These include thinking orientation, offering, drawing and managing of perspectives, the understanding of risk, its assessment, management and mitigation and your orientation to learn. Applying thinking, perspectives, risk appetite and learning appropriately will enable you to spot opportunities and make judgement calls with limited information. All these ingredients come together to help you think through strategy from an outside in perspective and helps in formulating strategies.
|
||||
</p>
|
||||
<p>
|
||||
This course will conclude offering you the perspectives and experiences of leaders when engaging with strategy and their learnings from it.
|
||||
{programme.methodologyDesc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1299,107 +1677,248 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
</section>
|
||||
|
||||
{/* Enhanced Related Programmes */}
|
||||
<section className="py-24 bg-gray-50/30" style={{ backgroundColor: 'rgba(249, 250, 251, 0.3)' }}>
|
||||
<div className="section-margin-x">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-h2 mb-4">You Might Also Like</h2>
|
||||
<p className="text-body-lg text-muted max-w-3xl mx-auto">
|
||||
Explore related programmes to continue your leadership journey and expand your expertise across different domains.
|
||||
</p>
|
||||
</div>
|
||||
{(courseDetail?.recommended_courses && courseDetail.recommended_courses.length > 0) && (
|
||||
<section className="py-24 bg-gray-50/30" style={{ backgroundColor: 'rgba(249, 250, 251, 0.3)' }}>
|
||||
<div className="section-margin-x">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-h2 mb-4">You Might Also Like</h2>
|
||||
<p className="text-body-lg text-muted max-w-3xl mx-auto">
|
||||
Explore related courses to continue your learning journey and expand your expertise across different domains.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{relatedProgrammes.map((related, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="cursor-pointer card-hover-dramatic rounded-2xl border-0 card-shadow-base group overflow-hidden"
|
||||
>
|
||||
<div className="aspect-video bg-gray-100 overflow-hidden relative">
|
||||
<ImageWithFallback
|
||||
src={related.thumbnail}
|
||||
alt={related.title}
|
||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
{courseDetail?.recommended_courses && courseDetail.recommended_courses.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{courseDetail.recommended_courses.map((course: any, index: number) => {
|
||||
// Calculate average rating from reviews
|
||||
const avgRating = course.recommended_course_reviews && course.recommended_course_reviews.length > 0
|
||||
? course.recommended_course_reviews.reduce((sum: number, review: any) => sum + review.rating, 0) / course.recommended_course_reviews.length
|
||||
: 4.5;
|
||||
|
||||
{/* Top Badges */}
|
||||
<div className="absolute top-4 left-4 flex gap-2">
|
||||
<Badge className="bg-[#F8C301] text-white rounded-lg text-xs px-3 py-1 font-medium">
|
||||
{related.category}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="bg-gray-100 text-gray-700 rounded-lg text-xs px-3 py-1 font-medium">
|
||||
{related.level}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
// Format price
|
||||
const formattedPrice = formatIndianPrice(course.best_value);
|
||||
const originalPrice = course.price ? formatIndianPrice(course.price) : null;
|
||||
|
||||
<CardContent className="p-6">
|
||||
<h3 className="text-h4 mb-3 font-semibold group-hover:text-primary transition-colors duration-200 leading-tight">
|
||||
{related.title}
|
||||
</h3>
|
||||
<p className="text-body text-muted mb-4 leading-relaxed line-clamp-2">{related.description}</p>
|
||||
|
||||
{/* Metadata Section */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4 text-small text-muted">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
{related.duration}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Users className="w-4 h-4" />
|
||||
1,500+
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Star key={star} size={16} className="fill-current text-yellow-400" />
|
||||
))}
|
||||
<span className="text-small text-muted ml-1">4.8</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-h4 font-bold text-primary">{related.price}</span>
|
||||
<span className="text-body text-muted line-through">₹49,999</span>
|
||||
<span className="text-sm text-green-600 bg-green-50 px-2 py-1 rounded font-medium">Save 25%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 rounded-xl border-2 hover:bg-gray-50 transition-all duration-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// Add to cart functionality
|
||||
}}
|
||||
return (
|
||||
<Card
|
||||
key={course.id || index}
|
||||
className="cursor-pointer card-hover-dramatic rounded-2xl border-0 card-shadow-base group overflow-hidden"
|
||||
onClick={() => handleCourseClick(course.id)}
|
||||
>
|
||||
Add to Cart
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1 rounded-xl bg-primary hover:bg-primary/90 text-white transition-all duration-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigateTo(`/course/${related.id}`);
|
||||
}}
|
||||
>
|
||||
Learn More →
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
<div className="aspect-video bg-gray-100 overflow-hidden relative">
|
||||
<ImageWithFallback
|
||||
src={course.thumbnail_img || "https://images.unsplash.com/photo-1552581234-26160f608093?w=400&h=300&fit=crop"}
|
||||
alt={course.course_name}
|
||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
|
||||
{/* Top Badges */}
|
||||
<div className="absolute top-4 left-4 flex gap-2">
|
||||
<Badge className="bg-[#F8C301] text-white rounded-lg text-xs px-3 py-1 font-medium">
|
||||
{course.course_category_name || "Course"}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="bg-gray-100 text-gray-700 rounded-lg text-xs px-3 py-1 font-medium">
|
||||
{course.duration || "Self-Paced"} Hours
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-6">
|
||||
<h3 className="text-h4 mb-3 font-semibold group-hover:text-primary transition-colors duration-200 leading-tight">
|
||||
{course.course_name}
|
||||
</h3>
|
||||
<p className="text-body text-muted mb-4 leading-relaxed line-clamp-2">
|
||||
{course.course_desc || "Discover new skills and advance your career with this comprehensive course."}
|
||||
</p>
|
||||
|
||||
{/* Metadata Section */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4 text-small text-muted">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
{course.duration
|
||||
? `${course.duration} ${course.duration === 1 ? "Hour" : "Hours"}`
|
||||
: "20 hours"}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Users className="w-4 h-4" />
|
||||
{course.total_lessons || 1500}+
|
||||
{/* lessons */}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Star
|
||||
key={star}
|
||||
size={16}
|
||||
className={star <= Math.round(avgRating) ? 'fill-current text-yellow-400' : 'text-gray-300'}
|
||||
/>
|
||||
))}
|
||||
<span className="text-small text-muted ml-1">{avgRating.toFixed(1)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-h4 font-bold text-primary">{formattedPrice}</span>
|
||||
{originalPrice && originalPrice !== formattedPrice && (
|
||||
<span className="text-body text-muted line-through">{originalPrice}</span>
|
||||
)}
|
||||
{originalPrice && originalPrice !== formattedPrice && (
|
||||
<span className="text-sm text-green-600 bg-green-50 px-2 py-1 rounded font-medium">
|
||||
Save {Math.round(((parseFloat(course.price) - parseFloat(course.best_value)) / parseFloat(course.price)) * 100)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 rounded-xl border-2 hover:bg-gray-50 transition-all duration-200"
|
||||
onClick={(e: any) => {
|
||||
e.stopPropagation();
|
||||
// Add to cart functionality
|
||||
addToCart({
|
||||
id: course.id,
|
||||
title: course.course_name,
|
||||
thumbnail: course.thumbnail_img,
|
||||
price: formattedPrice,
|
||||
originalPrice: originalPrice || undefined,
|
||||
category: course.course_category_name || 'Course',
|
||||
level: 'Intermediate'
|
||||
});
|
||||
toast.success(`${course.course_name} added to cart!`);
|
||||
}}
|
||||
>
|
||||
Add to Cart
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1 rounded-xl bg-primary hover:bg-primary/90 text-white transition-all duration-200"
|
||||
onClick={(e: any) => {
|
||||
e.stopPropagation();
|
||||
handleCourseClick(course.id);
|
||||
}}
|
||||
>
|
||||
Learn More →
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
// Fallback to mock data if no recommended courses
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{mockRelatedProgrammes.map((related, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="cursor-pointer card-hover-dramatic rounded-2xl border-0 card-shadow-base group overflow-hidden"
|
||||
onClick={() => handleCourseClick(related.id)}
|
||||
>
|
||||
<div className="aspect-video bg-gray-100 overflow-hidden relative">
|
||||
<ImageWithFallback
|
||||
src={related.thumbnail}
|
||||
alt={related.title}
|
||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
|
||||
{/* Top Badges */}
|
||||
<div className="absolute top-4 left-4 flex gap-2">
|
||||
<Badge className="bg-[#F8C301] text-white rounded-lg text-xs px-3 py-1 font-medium">
|
||||
{related.category}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="bg-gray-100 text-gray-700 rounded-lg text-xs px-3 py-1 font-medium">
|
||||
{related.level}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-6">
|
||||
<h3 className="text-h4 mb-3 font-semibold group-hover:text-primary transition-colors duration-200 leading-tight">
|
||||
{related.title}
|
||||
</h3>
|
||||
<p className="text-body text-muted mb-4 leading-relaxed line-clamp-2">{related.description}</p>
|
||||
|
||||
{/* Metadata Section */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4 text-small text-muted">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
{related.duration}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Users className="w-4 h-4" />
|
||||
1,500+
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Star key={star} size={16} className="fill-current text-yellow-400" />
|
||||
))}
|
||||
<span className="text-small text-muted ml-1">4.8</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-h4 font-bold text-primary">{related.price}</span>
|
||||
<span className="text-body text-muted line-through">₹49,999</span>
|
||||
<span className="text-sm text-green-600 bg-green-50 px-2 py-1 rounded font-medium">Save 25%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 rounded-xl border-2 hover:bg-gray-50 transition-all duration-200"
|
||||
onClick={(e: any) => {
|
||||
e.stopPropagation();
|
||||
addToCart({
|
||||
id: related.id,
|
||||
title: related.title,
|
||||
thumbnail: related.thumbnail,
|
||||
price: related.price,
|
||||
originalPrice: '₹49,999',
|
||||
category: related.category,
|
||||
level: related.level
|
||||
});
|
||||
toast.success(`${related.title} added to cart!`);
|
||||
}}
|
||||
>
|
||||
Add to Cart
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1 rounded-xl bg-primary hover:bg-primary/90 text-white transition-all duration-200"
|
||||
onClick={(e: any) => {
|
||||
e.stopPropagation();
|
||||
handleCourseClick(related.id);
|
||||
}}
|
||||
>
|
||||
Learn More →
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Focused CTA Banner - Blue Box Design */}
|
||||
<section className="py-20 relative overflow-hidden">
|
||||
@@ -1491,7 +2010,7 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
id="name"
|
||||
required
|
||||
value={brochureFormData.name}
|
||||
onChange={(e) => setBrochureFormData({...brochureFormData, name: e.target.value})}
|
||||
onChange={(e) => setBrochureFormData({ ...brochureFormData, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -1501,7 +2020,7 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
type="email"
|
||||
required
|
||||
value={brochureFormData.email}
|
||||
onChange={(e) => setBrochureFormData({...brochureFormData, email: e.target.value})}
|
||||
onChange={(e) => setBrochureFormData({ ...brochureFormData, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -1510,7 +2029,7 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
id="phone"
|
||||
type="tel"
|
||||
value={brochureFormData.phone}
|
||||
onChange={(e) => setBrochureFormData({...brochureFormData, phone: e.target.value})}
|
||||
onChange={(e) => setBrochureFormData({ ...brochureFormData, phone: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -1518,7 +2037,7 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
<Input
|
||||
id="company"
|
||||
value={brochureFormData.company}
|
||||
onChange={(e) => setBrochureFormData({...brochureFormData, company: e.target.value})}
|
||||
onChange={(e) => setBrochureFormData({ ...brochureFormData, company: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -1526,7 +2045,7 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
|
||||
<Input
|
||||
id="designation"
|
||||
value={brochureFormData.designation}
|
||||
onChange={(e) => setBrochureFormData({...brochureFormData, designation: e.target.value})}
|
||||
onChange={(e) => setBrochureFormData({ ...brochureFormData, designation: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -5295,3 +5306,30 @@ html {
|
||||
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