Files
KLC-Website-Frontend/src/components/ServicesSection.tsx

320 lines
12 KiB
TypeScript
Raw Permalink Normal View History

2025-08-28 13:14:51 +05:30
import { useState, useEffect, useRef } from "react";
import { motion } from "motion/react";
2026-03-20 19:43:27 +05:30
import {
Users,
Settings,
User,
Globe,
MessageSquare,
2025-08-28 13:14:51 +05:30
GraduationCap,
TrendingUp,
Building,
2025-08-28 13:14:51 +05:30
ArrowRight
} from "lucide-react";
import { BrandedTag } from "./about/BrandedTag";
import { StandardCTAButton } from "./StandardCTAButton";
2025-08-28 13:14:51 +05:30
import { navigateTo } from "./Router";
2026-03-20 19:43:27 +05:30
import { ImageWithFallback } from "./figma/ImageWithFallback";
2025-08-28 13:14:51 +05:30
2026-03-20 19:43:27 +05:30
interface HighlightCard {
card_title: string;
icon_url: string;
accessible_label: string;
body_text: string;
display_order: number;
}
interface ServicesSectionProps {
highlightCards?: HighlightCard[];
isLoading?: boolean;
}
2025-08-28 13:14:51 +05:30
2026-03-20 19:43:27 +05:30
export function ServicesSection({ highlightCards = [], isLoading = false }: ServicesSectionProps) {
2025-08-28 13:14:51 +05:30
const [isVisible, setIsVisible] = useState(false);
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
const sectionRef = useRef<HTMLDivElement>(null);
2026-03-20 19:43:27 +05:30
// Create service items from highlightCards data
const serviceItems = highlightCards.map((card, index) => ({
id: index + 1,
title: card.card_title,
description: card.body_text,
iconUrl: card.icon_url,
accessibleLabel: card.accessible_label,
route: '/services' // You might want to map to specific routes based on title
}));
2025-08-28 13:14:51 +05:30
// Add card refs helper
const addCardRef = (el: HTMLDivElement | null, index: number) => {
cardRefs.current[index] = el;
};
// Intersection observer for animations
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsVisible(true);
}
});
},
{ threshold: 0.2 }
);
if (sectionRef.current) {
observer.observe(sectionRef.current);
}
return () => observer.disconnect();
}, []);
// Keyboard navigation
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
if (e.key === 'ArrowDown' && index < cardRefs.current.length - 1) {
cardRefs.current[index + 1]?.focus();
e.preventDefault();
} else if (e.key === 'ArrowUp' && index > 0) {
cardRefs.current[index - 1]?.focus();
e.preventDefault();
}
};
2026-03-20 19:43:27 +05:30
// Show loading skeleton if isLoading is true
if (isLoading) {
return (
<section ref={sectionRef} className="py-16 lg:py-20" style={{ backgroundColor: '#fff' }}>
<div className="section-margin-x">
<div className="max-w-7xl mx-auto">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
<div className="h-12 bg-gray-200 rounded w-2/3 mb-4"></div>
<div className="h-24 bg-gray-200 rounded w-full mb-8"></div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-48 bg-gray-200 rounded"></div>
))}
</div>
</div>
</div>
</div>
</section>
);
}
2025-08-28 13:14:51 +05:30
return (
2026-03-20 19:43:27 +05:30
<section
2025-08-28 13:14:51 +05:30
ref={sectionRef}
className="py-16 lg:py-20"
2026-03-20 19:43:27 +05:30
style={{
2025-09-25 16:45:18 +05:30
backgroundColor: '#fff',
2025-08-28 13:14:51 +05:30
fontFamily: 'var(--font-family-brand)'
}}
aria-labelledby="recognition-section-heading"
>
<div className="section-margin-x">
<div className="max-w-7xl mx-auto">
{/* Desktop Layout - Grid with Sticky Sidebar */}
<div className="hidden lg:grid grid-cols-12 gap-12 min-h-screen">
2025-08-28 13:14:51 +05:30
{/* Left Side - Sticky Content */}
<div className="col-span-5 sticky top-24 self-start">
<div className="recognition-header pr-8">
2026-03-20 19:43:27 +05:30
<BrandedTag
text="Our Services"
2025-08-28 13:14:51 +05:30
/>
2026-03-20 19:43:27 +05:30
<h2
id="recognition-section-heading"
2025-08-28 13:14:51 +05:30
className="text-h2 mb-6"
>
2025-09-05 17:59:33 +05:30
Shaping Leaders, Cultures, and Institutions
2025-08-28 13:14:51 +05:30
</h2>
<p className="text-body-lg text-muted mb-8">
No two institutions are alike and neither are their leadership needs. That's why every KLC service is rooted in research, tailored to context, and aligned with strategy. From shaping leaders and managers to shaping culture, developing talent frameworks, and offering practical high impact learning, we partner with you to create leadership solutions that deliver lasting value.
</p>
{/* CTA Button - Left aligned */}
<div className="primary-cta-container-left cta-left-locked">
2026-03-20 19:43:27 +05:30
<StandardCTAButton
text="Services Page"
onClick={() => navigateTo('/services')}
ariaLabel="Explore our services"
/>
</div>
2025-08-28 13:14:51 +05:30
</div>
</div>
{/* Right Side - Scrolling Cards */}
<div className="col-span-7">
2026-03-20 19:43:27 +05:30
<div
2025-08-28 13:14:51 +05:30
className="recognition-cards space-y-6"
role="list"
aria-label="Leadership development services"
>
2026-03-20 19:43:27 +05:30
{serviceItems.map((item, index) => (
2025-08-28 13:14:51 +05:30
<div
key={item.id}
ref={(el) => addCardRef(el, index)}
className={`recognition-card group scroll-animate-stagger cursor-pointer focus-ring ${isVisible ? 'animate-in' : ''}`}
2025-08-28 13:14:51 +05:30
role="listitem"
aria-labelledby={`recognition-title-${item.id}`}
aria-describedby={`recognition-desc-${item.id}`}
tabIndex={0}
onKeyDown={(e) => handleKeyDown(e, index)}
2026-03-20 19:43:27 +05:30
style={{
2025-08-28 13:14:51 +05:30
transitionDelay: `${(index + 1) * 150}ms`,
opacity: isVisible ? 1 : 0
}}
onClick={() => navigateTo(item.route)}
2025-08-28 13:14:51 +05:30
>
2026-03-20 19:43:27 +05:30
<div
2025-08-28 13:14:51 +05:30
className="p-8 transition-all duration-300 hover:shadow-xl hover:-translate-y-1 border bg-white"
2026-03-20 19:43:27 +05:30
style={{
2025-08-28 13:14:51 +05:30
borderColor: 'var(--color-border)',
borderRadius: '12px',
fontFamily: 'var(--font-family-brand)'
}}
>
<div className="flex items-start mb-6">
2026-03-20 19:43:27 +05:30
<div
2025-08-28 13:14:51 +05:30
className="w-14 h-14 flex items-center justify-center transition-transform duration-300 group-hover:scale-110"
2026-03-20 19:43:27 +05:30
style={{
2025-08-28 13:14:51 +05:30
backgroundColor: 'var(--color-brand-primary)',
borderRadius: '12px',
}}
>
2026-03-20 19:43:27 +05:30
{/* Image icon from icon_url */}
<img
src={item.iconUrl}
alt={item.accessibleLabel || item.title}
className="w-8 h-8 object-contain filter brightness-0 invert" // Makes white icon on colored background
onError={(e) => {
// Fallback if image fails to load
e.currentTarget.style.display = 'none';
// You could add a fallback icon here if needed
}}
/>
2025-08-28 13:14:51 +05:30
</div>
</div>
2026-03-20 19:43:27 +05:30
2025-08-28 13:14:51 +05:30
<div className="recognition-card-content">
2026-03-20 19:43:27 +05:30
<h3
2025-08-28 13:14:51 +05:30
id={`recognition-title-${item.id}`}
className="text-h4 mb-4"
>
{item.title}
</h3>
2026-03-20 19:43:27 +05:30
<p
2025-08-28 13:14:51 +05:30
id={`recognition-desc-${item.id}`}
className="text-small text-muted leading-relaxed"
>
{item.description}
</p>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Mobile Layout - Stacked Header + Horizontal Scrollable Cards */}
<div className="lg:hidden">
{/* Mobile Header */}
<div className="text-center mb-8">
2026-03-20 19:43:27 +05:30
<BrandedTag
text="Our Services"
2025-08-28 13:14:51 +05:30
/>
2026-03-20 19:43:27 +05:30
<h2
id="recognition-section-heading-mobile"
className="text-h2 mb-6"
>
Shaping Leaders, Cultures, and Institutions
</h2>
<p className="text-body-lg text-muted mb-8">
No two institutions are alike and neither are their leadership needs. That's why every KLC service is rooted in research, tailored to context, and aligned with strategy. From shaping leaders and managers to shaping culture, developing talent frameworks, and offering practical high impact learning, we partner with you to create leadership solutions that deliver lasting value.
</p>
{/* CTA Button - Left aligned for mobile */}
<div className="primary-cta-container-left cta-left-locked">
2026-03-20 19:43:27 +05:30
<StandardCTAButton
text="Services Page"
onClick={() => navigateTo('/services')}
ariaLabel="Explore our services"
/>
</div>
</div>
{/* Mobile Horizontal Scrollable Cards */}
<div className="relative">
2026-03-20 19:43:27 +05:30
<div
className="flex gap-6 overflow-x-auto scrollbar-hide pb-4"
2026-03-20 19:43:27 +05:30
style={{
scrollSnapType: 'x mandatory',
WebkitOverflowScrolling: 'touch'
}}
role="list"
aria-label="Leadership development services"
>
2026-03-20 19:43:27 +05:30
{serviceItems.map((item, index) => (
<div
key={item.id}
className={`recognition-card-mobile group focus-ring flex-shrink-0 ${isVisible ? 'animate-in' : ''}`}
role="listitem"
aria-labelledby={`recognition-title-mobile-${item.id}`}
aria-describedby={`recognition-desc-mobile-${item.id}`}
tabIndex={0}
onKeyDown={(e) => handleKeyDown(e, index)}
2026-03-20 19:43:27 +05:30
style={{
scrollSnapAlign: 'start',
2026-03-20 19:43:27 +05:30
width: '320px',
transitionDelay: `${(index + 1) * 150}ms`,
opacity: isVisible ? 1 : 0
}}
>
2026-03-20 19:43:27 +05:30
<div
className="p-6 transition-all duration-300 hover:shadow-xl hover:-translate-y-1 border bg-white h-full"
2026-03-20 19:43:27 +05:30
style={{
borderColor: 'var(--color-border)',
borderRadius: '12px',
fontFamily: 'var(--font-family-brand)'
}}
>
<div className="flex items-start mb-6">
2026-03-20 19:43:27 +05:30
<div
className="w-12 h-12 flex items-center justify-center transition-transform duration-300 group-hover:scale-110"
2026-03-20 19:43:27 +05:30
style={{
backgroundColor: 'var(--color-brand-primary)',
borderRadius: '12px',
}}
>
2026-03-20 19:43:27 +05:30
<ImageWithFallback
src={item.iconUrl}
alt={item.accessibleLabel || item.title}
className="w-full h-full object-cover"
/>
</div>
</div>
2026-03-20 19:43:27 +05:30
<div className="recognition-card-content">
2026-03-20 19:43:27 +05:30
<h3
id={`recognition-title-mobile-${item.id}`}
className="text-h4 mb-4"
>
{item.title}
</h3>
2026-03-20 19:43:27 +05:30
<p
id={`recognition-desc-mobile-${item.id}`}
className="text-small text-muted leading-relaxed"
>
{item.description}
</p>
</div>
</div>
</div>
))}
</div>
</div>
2025-08-28 13:14:51 +05:30
</div>
</div>
</div>
</section>
);
}