main #1

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

View File

@@ -1,92 +1,39 @@
import { useState, useEffect } from 'react';
import { motion } from 'motion/react';
import {
Target,
Users,
Globe,
Lightbulb,
ArrowRight,
CheckCircle,
Star,
Award,
Brain,
TrendingUp,
Shield,
Heart,
Zap,
Eye,
BookOpen,
Brain,
CheckCircle,
Heart,
Puzzle,
Building,
ArrowLeft
Shield,
Target,
TrendingUp,
Users,
Zap
} from 'lucide-react';
import { BrandedTag } from './about/BrandedTag';
import { PrimaryCTAButton } from './PrimaryCTAButton';
import { CTABannerSection } from './CTABannerSection';
import { TestimonialsSection } from './TestimonialsSection';
import { TeamMemberModal } from './TeamMemberModal';
import { navigateTo } from './Router';
import { Button } from './ui/button';
import { motion } from 'motion/react';
import { useEffect, useState } from 'react';
import Aparna from '../assets/Aparna-Nair.png';
import Balaji from '../assets/Balaji-Chandrakumar.jpeg';
import Diju from '../assets/Diju.jpeg';
import Ramkumar from '../assets/K-Ramkumar.png';
import Muralidharan from '../assets/R-Muralidharan.png';
import Aparna from '../assets/Aparna-Nair.png';
import Swaminathan from '../assets/v-Swaminathan.jpg';
import Balaji from '../assets/Balaji-Chandrakumar.jpeg';
import Ramesh from '../assets/Ramesh-Padmanabhan.jpeg';
import Diju from '../assets/Diju.jpeg';
import Swaminathan from '../assets/v-Swaminathan.jpg';
import svgPaths from '../imports/svg-kw7r0ellyk';
import { useGetAboutUsQuery } from '../redux/services/aboutUsApi';
import { BrandedTag } from './about/BrandedTag';
import { CTABannerSection } from './CTABannerSection';
import { PrimaryCTAButton } from './PrimaryCTAButton';
import { navigateTo } from './Router';
import { TeamMemberModal } from './TeamMemberModal';
import { TestimonialsSection } from './TestimonialsSection';
import { Button } from './ui/button';
import { FullScreenLoader } from './FullScreenLoader';
// Leadership Orientations Data
const leadershipOrientations = [
{ name: 'Thinking', icon: Brain, description: 'Strategic and analytical mindset' },
{ name: 'Risk Appetite', icon: TrendingUp, description: 'Calculated risk-taking approach' },
{ name: 'Power', icon: Shield, description: 'Authority and influence dynamics' },
{ name: 'Interpersonal/Political', icon: Users, description: 'Relationship and network building' },
{ name: 'Ambition', icon: Target, description: 'Drive for achievement and growth' },
{ name: 'Trust', icon: Heart, description: 'Building confidence and reliability' },
{ name: 'Learning', icon: BookOpen, description: 'Continuous development mindset' },
{ name: 'Nurturance', icon: Heart, description: 'Supporting and developing others' },
{ name: 'Result/Closure', icon: CheckCircle, description: 'Focus on outcomes and completion' }
];
// Our Uniqueness Data
const uniquenessPoints = [
{
icon: Target,
title: 'Context & Strategy Alignment',
description: 'We align our work to the client\'s specific context & strategy'
},
{
icon: BookOpen,
title: 'Research-Anchored Approach',
description: 'Our work is anchored on research and work of scholars'
},
{
icon: Users,
title: 'Client-Specific Needs',
description: 'We blend this with the specific needs of the client'
},
{
icon: Puzzle,
title: 'Co-Creation Process',
description: 'We co-create the design with our clients'
}
];
// Benefits Data
const benefits = [
'We use proprietary exercises, custom written cases, curated films and proprietary tools',
'Facilitate insights on the connect between one\'s leadership orientations and their leadership abilities',
'We create learning at an individual level and at a group level',
'Our designs focus on application and practice',
'We bring in the connect of the learning to the Business contexts',
'We recommend that the Leadership intervention is designed for a period of 12-15 months with multiple touch points which can constitute a combination of classroom, fire side chats, one-on-one sessions, address by an expert, use of profilers, accessing online content on concepts and accomplished leaders\' experiences'
];
// Team Members Data with Full Profiles (Static - can be kept or also fetched from API if needed)
const staticTeamMembers = [
{
// Static detailed team member data for modal
const staticTeamMembersDetails = {
'Mr. K Ramkumar': {
name: 'Mr. K Ramkumar',
role: 'Managing Director',
image: Ramkumar,
@@ -109,12 +56,12 @@ He co-created the ICICI Manipal Academy for Banking and Insurance, which inducte
'Executive Director on ICICI Bank Board',
'Created ICICI Manipal Academy (12,000 leaders trained)',
'Founded ICICI Academy for Skills (35,000+ youth skilled)',
'Author of Leveraging Human Capital (McGraw Hill)'
'Author of "Leveraging Human Capital" (McGraw Hill)'
],
clientWork: 'Guided leadership development across ICICI Group and worked with Manipal Global Education to groom future banking leaders.',
boardRoles: 'Former Board Member of ICICI Prudential Life, ICICI Ventures; served on CSR and leadership committees.'
},
{
'Mr. R. Muralidharan': {
name: 'Mr. R. Muralidharan',
role: 'Practice Head Leadership Development',
image: Muralidharan,
@@ -140,7 +87,7 @@ At ICICI Bank, he was part of the founding team in 1994 and rose to become GM
clientWork: 'Worked with public and private banks, financial services firms, and non-profits on leadership and customer service transformation.',
boardRoles: 'Held board positions in business and non-profits; Vice-Chair, Customer Service Excellence Foundation.'
},
{
'Ms. Aparna Nair': {
name: 'Ms. Aparna Nair',
role: 'Practice Head Leadership Development',
image: Aparna,
@@ -166,7 +113,7 @@ She is certified in MBTI and OPQ, has applied Balanced Scorecard frameworks, and
clientWork: 'Worked with Godrej & Boyce, ICICI Prudential, Citi WAI, WNS, ThyssenKrupp, and others across pharma, BFSI, retail, auto, and private equity.',
boardRoles: 'Independent Woman Director; held leadership positions at ICICI Bank and Blue Dart-FedEx.'
},
{
'Mr. V. Swaminathan': {
name: 'Mr. V. Swaminathan',
role: 'Practice Head Leadership Development',
image: Swaminathan,
@@ -192,7 +139,7 @@ He stepped down as Joint President of Kotak Mahindra Bank in 2021 before joining
clientWork: 'Extensive work with BFSI organizations and leadership development initiatives at scale.',
boardRoles: 'Key member of Kotaks Management Committee; contributor to strategic boards within Kotak divisions.'
},
{
'Mr. Balaji Chandrakumar': {
name: 'Mr. Balaji Chandrakumar',
role: 'Practice Head Leadership Development',
image: Balaji,
@@ -218,7 +165,7 @@ Earlier, he worked in consulting with top Indian firms and began his HR journey
clientWork: 'Worked with leading companies in telecom, food, and HR consulting sectors across India and SE Asia.',
boardRoles: 'Advisor in HR capability building across organizations.'
},
{
'Mr. Ramesh Padmanabhan': {
name: 'Mr. Ramesh Padmanabhan',
role: 'Practice Head Leadership Development',
image: Ramesh,
@@ -244,7 +191,7 @@ He has consistently worked on capability building and leadership development alo
clientWork: 'Significant work in BFSI sector; tailored leadership development for managers and executives.',
boardRoles: 'Served on MANCOM and strategic boards in ICICI, Dhanlaxmi, and ADCB India.'
},
{
'Ms. Diju S': {
name: 'Ms. Diju S',
role: 'Practice Head Leadership Development',
image: Diju,
@@ -270,41 +217,68 @@ After a career break, she joined KLC as Practice Head. She now co-creates leader
clientWork: 'Worked with BFSI clients and KLC partners to create custom leadership programs.',
boardRoles: 'Active in leadership forums and project management at KLC.'
}
];
};
// Loading Skeleton Component
const AboutUsSkeleton = () => (
<div className="animate-pulse">
{/* Hero Section Skeleton */}
<section className="relative min-h-[85vh] flex flex-col bg-gray-200">
<div className="absolute inset-0 bg-gray-300"></div>
<div className="relative z-10 flex-1 flex items-center">
<div className="w-full section-margin-x">
<div className="max-w-6xl">
<div className="h-16 bg-gray-400 rounded-lg w-3/4 mb-8"></div>
<div className="h-24 bg-gray-400 rounded-lg w-2/3 mb-8"></div>
<div className="h-12 bg-gray-400 rounded-lg w-48"></div>
</div>
</div>
</div>
</section>
{/* Add more skeleton sections as needed */}
<div className="py-24 text-center text-gray-500">Loading...</div>
</div>
);
// Helper function to get member details by name
const getMemberDetails = (nameRole: string) => {
// Extract the name from "Name - Role" format
const name = nameRole.split(' - ')[0];
return staticTeamMembersDetails[name as keyof typeof staticTeamMembersDetails] || null;
};
export function AboutUs() {
const [isVisible, setIsVisible] = useState(false);
const [expandedValue, setExpandedValue] = useState<string | null>('context');
const [selectedMember, setSelectedMember] = useState<typeof staticTeamMembers[0] | null>(null);
const [selectedMember, setSelectedMember] = useState<any>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
// Fetch About Us data from API
const { data: aboutUsData, isLoading, isError, error } = useGetAboutUsQuery();
const handleMemberClick = (member: typeof staticTeamMembers[0]) => {
setSelectedMember(member);
const transformTestimonials = (apiTestimonials: any[]) => {
if (!apiTestimonials || apiTestimonials.length === 0) return [];
return apiTestimonials.map((testimonial, index) => ({
id: testimonial.id || index,
name: testimonial.name || "Anonymous",
role: testimonial.designation || "Client",
company: undefined,
avatar: testimonial.profile_photo_url || undefined,
image: testimonial.profile_photo_url || undefined,
quote: testimonial.content || "",
rating: 5,
isVideo: !!testimonial.video_url,
videoThumbnail: testimonial.video_thumbnail_url || testimonial.profile_photo_url,
videoUrl: testimonial.video_url || undefined
}));
};
// Transform the testimonials
const testimonialsData = transformTestimonials(aboutUsData?.testimonials || []);
// Get team members from API
const apiTeamMembers = aboutUsData?.our_team || [];
const handleMemberClick = (member: any) => {
// Get detailed static data for the clicked member
const memberDetails = getMemberDetails(member.name_role);
if (memberDetails) {
setSelectedMember(memberDetails);
} else {
// Fallback to API data if no static details found
setSelectedMember({
name: member.name_role.split(' - ')[0],
role: member.name_role.split(' - ')[1] || 'Team Member',
image: member.photo_url,
experience: member.bio,
fullBio: member.bio,
expertise: [],
education: '',
achievements: [],
clientWork: '',
boardRoles: ''
});
}
setIsModalOpen(true);
};
@@ -361,12 +335,14 @@ export function AboutUs() {
};
}, []);
// Show loading skeleton while fetching data
if (isLoading) {
return <AboutUsSkeleton />;
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<FullScreenLoader text="Loading articles..." />
</div>
);
}
// Show error state if API call fails
if (isError) {
return (
<div className="min-h-screen flex items-center justify-center">
@@ -460,7 +436,6 @@ export function AboutUs() {
<h2 className="text-h2 mb-8">{aboutUsData?.how_we_work_title || "How We Work"}</h2>
</motion.div>
{/* Four Key Points Grid - Using API data if available, otherwise fallback to static */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 lg:gap-12">
{(aboutUsData?.how_we_work && aboutUsData.how_we_work.length > 0) ? (
aboutUsData.how_we_work.map((item, index) => (
@@ -492,7 +467,6 @@ export function AboutUs() {
</motion.div>
))
) : (
// Fallback to static data if API data is not available
<>
<motion.div
initial={{ opacity: 0, y: 30 }}
@@ -602,9 +576,7 @@ export function AboutUs() {
transition={{ duration: 0.6 }}
viewport={{ once: true }}
>
{/* Split Layout - Left: Eyebrow Text, Right: Main Heading */}
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-12 mb-16">
{/* Left Side - Eyebrow Text */}
<div className="lg:col-span-1">
<div className="branded-tag-system">
<div className="dot"></div>
@@ -612,7 +584,6 @@ export function AboutUs() {
</div>
</div>
{/* Right Side - Main Heading */}
<div className="lg:col-span-4">
<h2 className="text-h1 leading-tight" style={{
fontSize: 'clamp(2.5rem, 5vw, 4rem)',
@@ -624,7 +595,6 @@ export function AboutUs() {
</div>
</div>
{/* Updated Statistics Grid - Dynamic from API */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8 lg:gap-12 pt-12 border-t border-gray-200">
{(aboutUsData?.stat_section && aboutUsData.stat_section.length > 0) ? (
aboutUsData.stat_section.map((stat, index) => (
@@ -650,7 +620,6 @@ export function AboutUs() {
</motion.div>
))
) : (
// Fallback to static statistics if API data is not available
<>
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -739,11 +708,10 @@ export function AboutUs() {
</div>
</section>
{/* Section 4: Our Team - Dynamic from API */}
{/* Section 4: Our Team - Dynamic from API (outer grid from API, modal from static) */}
<section className="py-24 lg:py-32" style={{ backgroundColor: '#F9F9F9' }}>
<div className="section-margin-x">
<div className="max-w-6xl mx-auto">
{/* Centered Header Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@@ -755,79 +723,39 @@ export function AboutUs() {
<h2 className="text-h2 mb-8">{aboutUsData?.our_team_title || "Our Team"}</h2>
<div className="max-w-4xl mx-auto text-center space-y-6">
<p className="text-body-lg text-muted leading-relaxed">
We have a team of 7 consultants and 4 young consultants. All our senior Consultants are ex-business professionals with experience ranging from 15-30 years in varied business functions and carry a deep understanding of the area they are engaging in. Two of them bring in Board room experience. Meet them
{aboutUsData?.our_team_description || "We have a team of 7 consultants and 4 young consultants. All our senior Consultants are ex-business professionals with experience ranging from 15-30 years in varied business functions and carry a deep understanding of the area they are engaging in. Two of them bring in Board room experience. Meet them"}
</p>
</div>
</motion.div>
{/* Team Members Grid - Using static team members with full profiles */}
{/* Team Members Grid - Using API data for outer display */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 lg:gap-12">
{staticTeamMembers.map((member, index) => (
{apiTeamMembers.map((member, index) => {
const name = member.name_role.split(' - ')[0];
const role = member.name_role.split(' - ')[1] || 'Team Member';
return (
<motion.div
key={member.name}
key={member.id || index}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
className="text-left cursor-pointer"
onClick={() => handleMemberClick(member)}
>
<div className="relative mb-6 group">
<div className="aspect-square rounded-2xl overflow-hidden bg-gray-100 shadow-lg group-hover:shadow-xl transition-all duration-300">
<img
src={member.image}
alt={member.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
{/* Hover Overlay */}
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-all duration-300 rounded-2xl flex items-center justify-center">
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div
className="px-4 py-2 rounded-lg text-white text-small"
style={{
backgroundColor: '#04045B',
fontFamily: 'var(--font-family-base)',
fontWeight: '500'
}}
>
View Profile
</div>
</div>
</div>
</div>
<div className="space-y-2">
<h3 className="text-h4 text-black group-hover:text-primary transition-colors duration-300">{member.name}</h3>
<p className="text-body text-muted leading-relaxed">
{member.role}
</p>
</div>
</motion.div>
))}
</div>
{/* Alternative: Use API team data if needed */}
{/* {aboutUsData?.our_team && aboutUsData.our_team.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 lg:gap-12">
{aboutUsData.our_team.map((member, index) => (
<motion.div
key={member.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
className="text-left cursor-pointer"
>
<div className="relative mb-6 group">
<div className="aspect-square rounded-2xl overflow-hidden bg-gray-100 shadow-lg group-hover:shadow-xl transition-all duration-300">
<img
src={member.photo_url}
alt={member.alt_text}
alt={member.alt_text || name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
onError={(e) => {
(e.target as HTMLImageElement).src = 'https://ui-avatars.com/api/?name=' + encodeURIComponent(name) + '&background=04045B&color=fff&size=200';
}}
/>
</div>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-all duration-300 rounded-2xl flex items-center justify-center">
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div
@@ -843,24 +771,27 @@ export function AboutUs() {
</div>
</div>
</div>
<div className="space-y-2">
<h3 className="text-h4 text-black group-hover:text-primary transition-colors duration-300">
{member.name_role.split(' - ')[0]}
</h3>
<p className="text-body text-muted leading-relaxed">
{member.name_role.split(' - ')[1] || ''}
<h3 className="text-h4 text-black group-hover:text-primary transition-colors duration-300">{name}</h3>
<p className="text-body text-muted leading-relaxed font-medium">
{role}
</p>
{/* Bio Section */}
<p className="text-small text-muted leading-relaxed mt-2 line-clamp-3">
{member.bio || "No bio available"}
</p>
<p className="text-small text-muted">{member.bio}</p>
</div>
</motion.div>
))}
);
})}
</div>
)} */}
</div>
</div>
</section>
{/* Section 5: Our Methodology (Static - unchanged) */}
{/* Section 5: Our Methodology - Dynamic from API */}
{aboutUsData?.methodology && (
<section className="py-16 lg:py-20" style={{ backgroundColor: '#FFFFFF' }}>
<div className="section-margin-x">
<div className="max-w-6xl mx-auto">
@@ -871,13 +802,13 @@ export function AboutUs() {
viewport={{ once: true }}
className="text-center mb-16"
>
<BrandedTag text="Our Methodology" />
<h2 className="text-h2 mb-8">Our Methodology</h2>
<BrandedTag text={aboutUsData.methodology.title || "Our Methodology"} />
{aboutUsData.methodology.subtitle && (
<h2 className="text-h2 mb-8">{aboutUsData.methodology.subtitle}</h2>
)}
</motion.div>
{/* Vertical Timeline Container */}
<div className="relative max-w-6xl mx-auto">
{/* Vertical Line Background - Gray - Ends exactly at Phase 3 dot */}
<div
className="absolute left-4 top-0 w-0.5 bg-gray-300"
style={{
@@ -886,7 +817,6 @@ export function AboutUs() {
}}
></div>
{/* Vertical Line Fill - Blue - Animated on Scroll */}
<div
id="timeline-fill-line"
className="absolute left-4 top-0 w-0.5 transition-all duration-1000 ease-out"
@@ -898,10 +828,11 @@ export function AboutUs() {
}}
></div>
{/* Phase 1 - Understanding & Assessment */}
<div className="relative pb-20">
{[...(aboutUsData.methodology.phases || [])]
.sort((a, b) => (a.display_order || 0) - (b.display_order || 0))
.map((phase, phaseIndex) => (
<div key={phase.id || phaseIndex} className="relative pb-20">
<div className="grid lg:grid-cols-12 gap-8 lg:gap-12 pl-12">
{/* Phase dot positioned absolutely */}
<div
className="absolute left-3 top-1 w-2.5 h-2.5 rounded-full bg-white border-2 z-10"
style={{
@@ -909,43 +840,41 @@ export function AboutUs() {
}}
></div>
{/* Column 1: Phase Label */}
<div className="lg:col-span-2">
<div className="branded-tag-system">
<div className="dot"></div>
<span className="text">Phase 1</span>
<span className="text">{phase.phase_label || `Phase ${phase.phase_number}`}</span>
</div>
</div>
{/* Column 2: Main Heading */}
<div className="lg:col-span-3">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
transition={{ duration: 0.6, delay: phaseIndex * 0.2 }}
viewport={{ once: true }}
>
<h3 className="text-h3">Understanding & Assessment</h3>
<h3 className="text-h3">{phase.title}</h3>
</motion.div>
</div>
{/* Column 3: Content - Description and Bullet Points */}
<div className="lg:col-span-7">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
transition={{ duration: 0.6, delay: phaseIndex * 0.2 }}
viewport={{ once: true }}
>
<p className="text-body-lg text-muted leading-relaxed mb-6">
We believe that leadership is more than skill and style. Leadership is predicated by a person's orientations (typical behaviors) which shapes the leadership abilities and its fit to a context. The broader the fit of the orientation the more versatile a person is in his exercise of leadership in a variety of contexts.
{phase.description}
</p>
{phase.bullet_title && phase.bullets && phase.bullets.length > 0 && (
<div className="mb-8">
<h4 className="text-h4 mb-4">Key Focus Areas:</h4>
<h4 className="text-h4 mb-4">{phase.bullet_title}</h4>
<div className="space-y-3">
<div className="flex items-start gap-3">
{phase.bullets.map((bullet, bulletIndex) => (
<div key={bulletIndex} className="flex items-start gap-3">
<div
className="w-1.5 h-1.5 rounded-full mt-2 flex-shrink-0"
style={{
@@ -953,225 +882,21 @@ export function AboutUs() {
}}
></div>
<span className="text-body text-muted">
Leadership orientations (typical behaviors) assessment.
</span>
</div>
<div className="flex items-start gap-3">
<div
className="w-1.5 h-1.5 rounded-full mt-2 flex-shrink-0"
style={{
backgroundColor: 'var(--color-primary)',
}}
></div>
<span className="text-body text-muted">
Action-consequence model application.
</span>
</div>
<div className="flex items-start gap-3">
<div
className="w-1.5 h-1.5 rounded-full mt-2 flex-shrink-0"
style={{
backgroundColor: 'var(--color-primary)',
}}
></div>
<span className="text-body text-muted">
Context alignment and strategy integration.
{bullet}
</span>
</div>
))}
</div>
</div>
)}
</motion.div>
</div>
</div>
</div>
))}
{/* Phase 2 - Development & Practice */}
{aboutUsData?.philosophy && (
<div className="relative pb-20">
<div className="grid lg:grid-cols-12 gap-8 lg:gap-12 pl-12">
{/* Phase dot positioned absolutely */}
<div
className="absolute left-3 top-1 w-2.5 h-2.5 rounded-full bg-white border-2 z-10"
style={{
borderColor: 'var(--color-primary)',
}}
></div>
{/* Column 1: Phase Label */}
<div className="lg:col-span-2">
<div className="branded-tag-system">
<div className="dot"></div>
<span className="text">Phase 2</span>
</div>
</div>
{/* Column 2: Main Heading */}
<div className="lg:col-span-3">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
viewport={{ once: true }}
>
<h3 className="text-h3">Development & Practice</h3>
</motion.div>
</div>
{/* Column 3: Content - Description and Bullet Points */}
<div className="lg:col-span-7">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
viewport={{ once: true }}
>
<p className="text-body-lg text-muted leading-relaxed mb-4">
Thus, in our engagements with our clients we work with the individual using the action-consequence model to link up one's leadership orientations (typical behaviors) to outcomes.
</p>
<p className="text-body-lg text-muted leading-relaxed mb-6">
Leadership is a social skill. Socially aware people shape the thought, emotions and actions of others better. The key to social-awareness is self-awareness. We help our learners become aware, gain insights and discover their fullest leadership potential. This in turn builds organizational leadership capacity & capabilities.
</p>
<div className="mb-8">
<h4 className="text-h4 mb-4">Development Process:</h4>
<div className="space-y-3">
<div className="flex items-start gap-3">
<div
className="w-1.5 h-1.5 rounded-full mt-2 flex-shrink-0"
style={{
backgroundColor: 'var(--color-primary)',
}}
></div>
<span className="text-body text-muted">
Self-awareness and insight facilitation.
</span>
</div>
<div className="flex items-start gap-3">
<div
className="w-1.5 h-1.5 rounded-full mt-2 flex-shrink-0"
style={{
backgroundColor: 'var(--color-primary)',
}}
></div>
<span className="text-body text-muted">
Leadership potential discovery sessions.
</span>
</div>
<div className="flex items-start gap-3">
<div
className="w-1.5 h-1.5 rounded-full mt-2 flex-shrink-0"
style={{
backgroundColor: 'var(--color-primary)',
}}
></div>
<span className="text-body text-muted">
Organizational capacity building.
</span>
</div>
</div>
</div>
</motion.div>
</div>
</div>
</div>
{/* Phase 3 - Implementation & Mastery */}
<div className="relative">
<div className="grid lg:grid-cols-12 gap-8 lg:gap-12 pl-12">
{/* Phase dot positioned absolutely */}
<div
className="absolute left-3 top-1 w-2.5 h-2.5 rounded-full bg-white border-2 z-10"
style={{
borderColor: 'var(--color-primary)',
}}
></div>
{/* Column 1: Phase Label */}
<div className="lg:col-span-2">
<div className="branded-tag-system">
<div className="dot"></div>
<span className="text">Phase 3</span>
</div>
</div>
{/* Column 2: Main Heading */}
<div className="lg:col-span-3">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
viewport={{ once: true }}
>
<h3 className="text-h3">Implementation & Mastery</h3>
</motion.div>
</div>
{/* Column 3: Content - Description and Bullet Points */}
<div className="lg:col-span-7">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
viewport={{ once: true }}
>
<p className="text-body-lg text-muted leading-relaxed mb-6">
Our approach to learning works on the 'Orientation Toning process', which is using 'Restraint and Release' of thought, emotions and actions. This release is moderated according to the demands of a context, without being tied down by one's default settings.
</p>
<div className="mb-8">
<h4 className="text-h4 mb-4">Mastery Framework:</h4>
<div className="space-y-3 mb-8">
<div className="flex items-start gap-3">
<div
className="w-1.5 h-1.5 rounded-full mt-2 flex-shrink-0"
style={{
backgroundColor: 'var(--color-primary)',
}}
></div>
<span className="text-body text-muted">
Orientation Toning process implementation.
</span>
</div>
<div className="flex items-start gap-3">
<div
className="w-1.5 h-1.5 rounded-full mt-2 flex-shrink-0"
style={{
backgroundColor: 'var(--color-primary)',
}}
></div>
<span className="text-body text-muted">
Context-responsive leadership adaptation.
</span>
</div>
<div className="flex items-start gap-3">
<div
className="w-1.5 h-1.5 rounded-full mt-2 flex-shrink-0"
style={{
backgroundColor: 'var(--color-primary)',
}}
></div>
<span className="text-body text-muted">
Sustainable behavior change integration.
</span>
</div>
</div>
</div>
</motion.div>
</div>
</div>
</div>
{/* Our Philosophy - Full Width Figma Design */}
<div className="relative pb-20">
{/* Phase dot positioned absolutely */}
<div
className="absolute left-3 top-1 w-2.5 h-2.5 rounded-full bg-white border-2 z-10"
style={{
@@ -1193,22 +918,19 @@ export function AboutUs() {
}}
>
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12">
{/* Left Section: Heading */}
<div className="lg:w-1/3">
<h3 className="text-h3 text-white mb-6 lg:mb-0">Our Philosophy</h3>
<h3 className="text-h3 text-white mb-6 lg:mb-0">{aboutUsData.philosophy.title || "Our Philosophy"}</h3>
</div>
{/* Right Section: Content */}
<div className="lg:w-2/3">
{/* Philosophy Description */}
<p className="text-body-white mb-8 opacity-90">
Our philosophy is that leadership begins with orientations the inner drivers and behaviors that shape who we are. These orientations combine to create leadership abilities, which in turn lead to meaningful outcomes. At Kautilya Leadership Centre, we see leadership as a journey of connecting orientations to abilities and outcomes through structured development.
{aboutUsData.philosophy.description}
</p>
{/* Three Philosophy Items */}
{aboutUsData.philosophy.points && aboutUsData.philosophy.points.length > 0 && (
<div className="flex flex-col gap-4">
{/* Leadership Orientations */}
<div className="flex items-start gap-4">
{aboutUsData.philosophy.points.map((point, pointIndex) => (
<div key={pointIndex} className="flex items-start gap-4">
<div
className="bg-[#f8c301] content-stretch flex items-center justify-center rounded-full shrink-0 size-[32px]"
>
@@ -1221,64 +943,20 @@ export function AboutUs() {
</div>
<div className="flex flex-col gap-1">
<p className="font-['Inter',sans-serif] font-normal leading-[25.6px] text-[16px] text-white">
Leadership Orientations
</p>
<p className="font-['Inter',sans-serif] font-normal leading-[25.6px] text-[16px] text-white opacity-80">
Who am I? (inner drivers & behaviors)
</p>
</div>
</div>
{/* Leadership Abilities */}
<div className="flex items-start gap-4">
<div
className="bg-[#f8c301] content-stretch flex items-center justify-center rounded-full shrink-0 size-[32px]"
>
<svg className="block size-[16px]" fill="none" preserveAspectRatio="none" viewBox="0 0 16 16">
<g>
<path d="M3.33398 8H12.6673" stroke="#26231A" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.33333" />
<path d={svgPaths.p2c1c9a80} stroke="#26231A" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.33333" />
</g>
</svg>
</div>
<div className="flex flex-col gap-1">
<p className="font-['Inter',sans-serif] font-normal leading-[25.6px] text-[16px] text-white">
Leadership Abilities
</p>
<p className="font-['Inter',sans-serif] font-normal leading-[25.6px] text-[16px] text-white opacity-80">
What I can do (competencies & skills)
</p>
</div>
</div>
{/* Leadership Outcomes */}
<div className="flex items-start gap-4">
<div
className="bg-[#f8c301] content-stretch flex items-center justify-center rounded-full shrink-0 size-[32px]"
>
<svg className="block size-[16px]" fill="none" preserveAspectRatio="none" viewBox="0 0 16 16">
<g>
<path d="M3.33398 8H12.6673" stroke="#26231A" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.33333" />
<path d={svgPaths.p2c1c9a80} stroke="#26231A" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.33333" />
</g>
</svg>
</div>
<div className="flex flex-col gap-1">
<p className="font-['Inter',sans-serif] font-normal leading-[25.6px] text-[16px] text-white">
Leadership Outcomes
</p>
<p className="font-['Inter',sans-serif] font-normal leading-[25.6px] text-[16px] text-white opacity-80">
What I deliver (results for self & organization)
{point}
</p>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</motion.div>
</div>
</div>
)}
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -1298,22 +976,20 @@ export function AboutUs() {
</div>
</div>
</section>
)}
{/* Testimonials Section */}
<TestimonialsSection
customTestimonials={testimonialsData}
title="What Our Clients Say About Us"
subtitle="Hear from leaders who have transformed their approach through our comprehensive leadership development programs."
tagText="Client Success Stories"
/>
{/* CTA Banner Section */}
<CTABannerSection />
{/* Team Member Modal */}
<TeamMemberModal
member={selectedMember}
isOpen={isModalOpen}
onClose={handleCloseModal}
{/* CTA Banner Section - Dynamic from API */}
<CTABannerSection
ctaSection={aboutUsData?.cta_section}
isLoading={isLoading}
/>
</div>
);

View File

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

View File

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

View File

@@ -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

View File

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

View File

@@ -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 */}
@@ -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 */}
@@ -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,21 +1677,151 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
</section>
{/* Enhanced Related Programmes */}
{(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 programmes to continue your leadership journey and expand your expertise across different domains.
Explore related courses to continue your learning journey and expand your expertise across different domains.
</p>
</div>
{courseDetail?.recommended_courses && courseDetail.recommended_courses.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{relatedProgrammes.map((related, index) => (
{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;
// Format price
const formattedPrice = formatIndianPrice(course.best_value);
const originalPrice = course.price ? formatIndianPrice(course.price) : null;
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)}
>
<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
@@ -1375,9 +1883,18 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
variant="outline"
size="sm"
className="flex-1 rounded-xl border-2 hover:bg-gray-50 transition-all duration-200"
onClick={(e) => {
onClick={(e: any) => {
e.stopPropagation();
// Add to cart functionality
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
@@ -1385,9 +1902,9 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
<Button
size="sm"
className="flex-1 rounded-xl bg-primary hover:bg-primary/90 text-white transition-all duration-200"
onClick={(e) => {
onClick={(e: any) => {
e.stopPropagation();
navigateTo(`/course/${related.id}`);
handleCourseClick(related.id);
}}
>
Learn More
@@ -1397,9 +1914,11 @@ export function ProgrammeDetail({ slug }: ProgrammeDetailProps) {
</Card>
))}
</div>
)}
</div>
</div>
</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>

View File

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

View File

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

View File

@@ -1,151 +1,100 @@
import React, { useState, useRef, useEffect } from 'react';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Input } from './ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Slider } from './ui/slider';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { PrimaryCTAButton } from './PrimaryCTAButton';
import { navigateTo } from './Router';
import { sharedWebinarsData, type WebinarData } from '../data/webinarsData';
import { WebcastCTABanner } from './WebcastCTABanner';
import {
Search,
Calendar,
Clock,
Users,
Play,
ArrowRight,
ChevronLeft,
ChevronRight,
Clock,
Eye,
Filter,
Grid,
List,
SortAsc,
Eye,
Play,
Search,
Star,
ChevronLeft,
ChevronRight,
Users,
X
} from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { useWebinarListQuery, type WebinarItem } from '../redux/services/webinarApi';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { FullScreenLoader } from './FullScreenLoader';
import { navigateTo } from './Router';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Card, CardContent } from './ui/card';
import { Input } from './ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Slider } from './ui/slider';
import { WebcastCTABanner } from './WebcastCTABanner';
// Status options with proper mapping to API values
const statusOptions = [
{ value: 'scheduled', label: '📅 Scheduled', color: 'bg-blue-100 text-blue-800 border-blue-200' },
{ value: 'live', label: '🔴 Live', color: 'bg-red-100 text-red-800 border-red-200' },
{ value: 'ended', label: '✅ Ended', color: 'bg-gray-100 text-gray-800 border-gray-200' },
{ value: 'cancelled', label: '❌ Cancelled', color: 'bg-red-50 text-red-600 border-red-200' }
];
const sortOptions = [
{ value: 'most_popular', label: 'Most Popular' },
{ value: 'newest', label: 'Newest First' },
{ value: 'oldest', label: 'Oldest First' },
{ value: 'title', label: 'Title A-Z' },
{ value: 'duration', label: 'Duration' }
];
// Static tags for all webinars
const staticTags = ['Leadership', 'Executive Development', 'Strategy', 'Innovation', 'Change Management', 'Business Growth', 'Team Building', 'Digital Transformation'];
export function Webinars() {
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('All Categories');
const [selectedFormat, setSelectedFormat] = useState('All Formats');
const [selectedLevel, setSelectedLevel] = useState('All Levels');
// Updated state for multi-select status pills
const [selectedStatuses, setSelectedStatuses] = useState<string[]>([]);
// Updated state for duration slider (min, max in minutes)
const [durationRange, setDurationRange] = useState([0, 120]);
// Attendee range slider state
const [attendeeRange, setAttendeeRange] = useState([0, 5000]);
const [sortBy, setSortBy] = useState('Most Popular');
const [sortBy, setSortBy] = useState('most_popular');
const [viewType, setViewType] = useState<'grid' | 'list'>('grid');
const [currentPage, setCurrentPage] = useState(1);
const webinarsPerPage = 6;
const containerRef = useRef<HTMLDivElement>(null);
// Use shared webinars data instead of local mock data
const webinars = sharedWebinarsData;
// Get unique values for filters from shared data
const categories = ['All Categories', ...Array.from(new Set(webinars.map(webinar => webinar.category)))];
const formats = ['All Formats', ...Array.from(new Set(webinars.map(webinar => webinar.format)))];
const levels = ['All Levels', ...Array.from(new Set(webinars.map(webinar => webinar.level)))];
// Status options for pills - updated to match shared data structure
const statusOptions = [
{ value: 'upcoming', label: '📅 Upcoming', color: 'bg-blue-100 text-blue-800 border-blue-200' },
{ value: 'live', label: '🔴 Live', color: 'bg-red-100 text-red-800 border-red-200' },
{ value: 'recorded', label: '▶️ Recorded', color: 'bg-green-100 text-green-800 border-green-200' },
{ value: 'featured', label: '⭐ Featured', color: 'bg-yellow-100 text-yellow-800 border-yellow-200' }
];
const sortOptions = [
{ value: 'Most Popular', label: 'Most Popular' },
{ value: 'newest', label: 'Newest First' },
{ value: 'oldest', label: 'Oldest First' },
{ value: 'title', label: 'Title A-Z' },
{ value: 'duration', label: 'Duration' }
];
// Helper function to convert attendees string to number
const parseAttendees = (attendeesStr: string): number => {
const numStr = attendeesStr.replace(/[^\d]/g, '');
return parseInt(numStr) || 0;
};
// Helper function to convert duration string to minutes
const parseDuration = (durationStr: string): number => {
const numStr = durationStr.replace(/[^\d]/g, '');
return parseInt(numStr) || 0;
};
// Filter and sort webinars
const filteredWebinars = webinars.filter(webinar => {
const matchesSearch = webinar.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
webinar.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
webinar.presenter.toLowerCase().includes(searchTerm.toLowerCase()) ||
webinar.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesCategory = selectedCategory === 'All Categories' || webinar.category === selectedCategory;
const matchesFormat = selectedFormat === 'All Formats' || webinar.format === selectedFormat;
const matchesLevel = selectedLevel === 'All Levels' || webinar.level === selectedLevel;
const matchesStatus = selectedStatuses.length === 0 ||
selectedStatuses.some(status => {
if (status === 'featured') return webinar.featured;
return webinar.status === status;
// 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,
});
const durationMinutes = parseDuration(webinar.duration);
const matchesDuration = durationMinutes >= durationRange[0] && durationMinutes <= durationRange[1];
const webinars = webinarResponse?.data?.items || [];
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;
// 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]];
}
});
// 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
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>
{!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 */}
{!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>
{/* 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>
{!isCancelled && (
<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>
))}
<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 text-primary font-medium group-hover:translate-x-1 transition-transform">
<span className="text-small">{getActionText()}</span>
<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>
)}
</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,7 +404,6 @@ console.log('Current webinars count:', currentWebinars.length);
</div>
</div>
{/* Statistics Strip at Bottom */}
<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">
@@ -424,12 +430,11 @@ console.log('Current webinars count:', currentWebinars.length);
<section className="py-8" style={{ backgroundColor: '#FFFFFF' }}>
<div className="section-margin-x">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
{/* Search Bar */}
<div className="relative max-w-md flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
type="text"
placeholder="Search webcasts..."
placeholder="Search webinars..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-4 py-3 text-body rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all duration-200 w-full bg-gray-50"
@@ -441,7 +446,6 @@ console.log('Current webinars count:', currentWebinars.length);
/>
</div>
{/* View Toggle and Sort */}
<div className="flex items-center gap-4">
<div className="flex items-center border border-gray-300 rounded-lg overflow-hidden">
<button
@@ -451,7 +455,7 @@ console.log('Current webinars count:', currentWebinars.length);
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
style={{
backgroundColor: viewType === 'grid' ? 'var(--color-primary)' : undefined
backgroundColor: viewType === 'grid' ? '#04045b' : undefined
}}
aria-label="Grid view"
>
@@ -464,7 +468,7 @@ console.log('Current webinars count:', currentWebinars.length);
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
style={{
backgroundColor: viewType === 'list' ? 'var(--color-primary)' : undefined
backgroundColor: viewType === 'list' ? '#04045b' : undefined
}}
aria-label="List view"
>
@@ -489,31 +493,28 @@ console.log('Current webinars count:', currentWebinars.length);
</div>
</section>
{/* Main Content Section with Sidebar */}
{/* Main Content Section */}
<section className="pb-16" style={{ backgroundColor: '#FFFFFF' }}>
<div className="section-margin-x">
<div className="grid grid-cols-12 gap-8">
{/* Left Sidebar - Sticky Filters */}
{/* Left Sidebar Filters */}
<div className="col-span-12 lg:col-span-3">
<div className="sticky top-4">
<Card className="bg-white border border-gray-200 rounded-lg shadow-md overflow-hidden">
{/* Filter Header */}
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-md" style={{ backgroundColor: 'rgba(4, 4, 91, 0.1)' }}>
<Filter className="w-3.5 h-3.5" style={{ color: 'var(--color-primary)' }} />
<Filter className="w-3.5 h-3.5" style={{ color: '#04045b' }} />
</div>
<h3 className="text-body font-semibold text-gray-800">
Filters
</h3>
<h3 className="text-body font-semibold text-gray-800">Filters</h3>
</div>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearAllFilters}
className="text-xs px-2 py-1 rounded-md transition-colors filter-clear-btn"
className="text-xs px-2 py-1 rounded-md transition-colors"
>
<X className="w-3 h-3 mr-1" />
Clear
@@ -522,67 +523,9 @@ console.log('Current webinars count:', currentWebinars.length);
</div>
</div>
{/* Filter Content */}
<div className="p-4">
<div className="space-y-6">
{/* Category Filter */}
<div className="filter-section">
<label className="block text-small mb-2 font-medium text-gray-700">
Category
</label>
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
<SelectValue placeholder="All Categories" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem key={category} value={category} className="text-small">
{category}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Format Filter */}
<div className="filter-section">
<label className="block text-small mb-2 font-medium text-gray-700">
Format
</label>
<Select value={selectedFormat} onValueChange={setSelectedFormat}>
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
<SelectValue placeholder="All Formats" />
</SelectTrigger>
<SelectContent>
{formats.map((format) => (
<SelectItem key={format} value={format} className="text-small">
{format}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Level Filter */}
<div className="filter-section">
<label className="block text-small mb-2 font-medium text-gray-700">
Level
</label>
<Select value={selectedLevel} onValueChange={setSelectedLevel}>
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
<SelectValue placeholder="All Levels" />
</SelectTrigger>
<SelectContent>
{levels.map((level) => (
<SelectItem key={level} value={level} className="text-small">
{level}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Status Filter - Multi-select Pills */}
{/* Status Filter */}
<div className="filter-section">
<label className="block text-small mb-3 font-medium text-gray-700">
Status
@@ -611,10 +554,10 @@ console.log('Current webinars count:', currentWebinars.length);
)}
</div>
{/* Duration Filter - Slider */}
{/* Duration Filter */}
<div className="filter-section">
<label className="block text-small mb-3 font-medium text-gray-700">
Duration
Duration (minutes)
</label>
<div className="px-2">
<Slider
@@ -629,19 +572,13 @@ console.log('Current webinars count:', currentWebinars.length);
<span>{durationRange[0]} min</span>
<span>{durationRange[1]} min</span>
</div>
<div className="mt-1 text-center text-xs text-gray-400">
{durationRange[0] === 0 && durationRange[1] === 120
? 'All durations'
: `${durationRange[0]}-${durationRange[1]} minutes`
}
</div>
</div>
</div>
{/* Attendee Count Filter - Slider */}
{/* Attendee Filter */}
<div className="filter-section">
<label className="block text-small mb-3 font-medium text-gray-700">
Attendees
Max Attendees
</label>
<div className="px-2">
<Slider
@@ -656,12 +593,6 @@ console.log('Current webinars count:', currentWebinars.length);
<span>{attendeeRange[0].toLocaleString()}</span>
<span>{attendeeRange[1].toLocaleString()}+</span>
</div>
<div className="mt-1 text-center text-xs text-gray-400">
{attendeeRange[0] === 0 && attendeeRange[1] === 5000
? 'Any size'
: `${attendeeRange[0].toLocaleString()}-${attendeeRange[1].toLocaleString()}+`
}
</div>
</div>
</div>
</div>
@@ -672,24 +603,22 @@ console.log('Current webinars count:', currentWebinars.length);
{/* Right Main Content */}
<div className="col-span-12 lg:col-span-9">
{/* Results Header */}
<div className="flex items-center justify-between mb-6">
<div className="text-body text-gray-600">
Showing {currentWebinars.length} of {filteredWebinars.length} webcasts
Showing {currentWebinars.length} of {filteredWebinars.length} webinars
</div>
<div className="text-small text-gray-500">
Page {currentPage} of {totalPages}
</div>
</div>
{/* Content Area */}
<div ref={containerRef}>
{currentWebinars.length === 0 ? (
<div className="text-center py-12">
<div className="text-gray-400 mb-4">
<Search className="w-12 h-12 mx-auto mb-4" />
</div>
<h3 className="text-h4 mb-2">No webcasts found</h3>
<h3 className="text-h4 mb-2">No webinars found</h3>
<p className="text-body text-gray-600 mb-4">
Try adjusting your filters or search terms
</p>
@@ -701,7 +630,6 @@ console.log('Current webinars count:', currentWebinars.length);
</div>
) : (
<>
{/* Grid View */}
{viewType === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 mb-8">
{currentWebinars.map((webinar) => (
@@ -709,7 +637,6 @@ console.log('Current webinars count:', currentWebinars.length);
))}
</div>
) : (
/* List View */
<div className="space-y-4 mb-8">
{currentWebinars.map((webinar) => (
<WebinarCard key={webinar.id} webinar={webinar} />
@@ -723,64 +650,46 @@ console.log('Current webinars count:', currentWebinars.length);
<Button
variant="outline"
size="sm"
onClick={() => {
setCurrentPage(prev => Math.max(1, prev - 1));
containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}}
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="flex items-center gap-1 border-gray-300 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronLeft className="w-4 h-4" />
Previous
</Button>
<div className="flex items-center gap-1">
{Array.from({ length: totalPages }, (_, i) => {
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
const page = i + 1;
// Show limited pages for better UX
if (totalPages > 7) {
const showPage =
page === 1 ||
page === totalPages ||
(page >= currentPage - 1 && page <= currentPage + 1);
if (!showPage) {
if (page === currentPage - 2 || page === currentPage + 2) {
return <span key={page} className="px-2">...</span>;
}
return null;
}
}
return (
<Button
key={page}
variant={currentPage === page ? "default" : "outline"}
size="sm"
onClick={() => {
setCurrentPage(page);
containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}}
className={`min-w-10 ${currentPage === page
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
}`}
onClick={() => setCurrentPage(page)}
className="min-w-10"
style={currentPage === page ? { backgroundColor: '#04045b' } : {}}
>
{page}
</Button>
);
})}
{totalPages > 5 && <span className="px-2">...</span>}
{totalPages > 5 && (
<Button
variant={currentPage === totalPages ? "default" : "outline"}
size="sm"
onClick={() => setCurrentPage(totalPages)}
className="min-w-10"
style={currentPage === totalPages ? { backgroundColor: '#04045b' } : {}}
>
{totalPages}
</Button>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={() => {
setCurrentPage(prev => Math.min(totalPages, prev + 1));
containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}}
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className="flex items-center gap-1 border-gray-300 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
<ChevronRight className="w-4 h-4" />
@@ -795,7 +704,6 @@ console.log('Current webinars count:', currentWebinars.length);
</div>
</section>
{/* Webcast CTA Banner */}
<WebcastCTABanner />
</div>
);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -167,6 +167,9 @@
/* Small text */
--font-small: 0.875rem;
/* 14px */
/* Extra small text */
--font-extra-small: 0.75rem;
/* 12px */
--line-height-small: 1.5;
--font-weight-small: 400;
@@ -601,6 +604,14 @@ html {
color: var(--color-black);
}
.text-small-extra {
font-size: var(--font-extra-small);
line-height: var(--line-height-small);
font-weight: var(--font-weight-small);
font-family: var(--font-family-base);
color: var(--color-black);
}
.text-eyebrow {
font-size: var(--font-eyebrow);
line-height: var(--line-height-eyebrow);
@@ -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;
}