320 lines
12 KiB
TypeScript
320 lines
12 KiB
TypeScript
import { useState, useEffect, useRef } from "react";
|
|
import { motion } from "motion/react";
|
|
import {
|
|
Users,
|
|
Settings,
|
|
User,
|
|
Globe,
|
|
MessageSquare,
|
|
GraduationCap,
|
|
TrendingUp,
|
|
Building,
|
|
ArrowRight
|
|
} from "lucide-react";
|
|
import { BrandedTag } from "./about/BrandedTag";
|
|
import { StandardCTAButton } from "./StandardCTAButton";
|
|
import { navigateTo } from "./Router";
|
|
import { ImageWithFallback } from "./figma/ImageWithFallback";
|
|
|
|
interface HighlightCard {
|
|
card_title: string;
|
|
icon_url: string;
|
|
accessible_label: string;
|
|
body_text: string;
|
|
display_order: number;
|
|
}
|
|
|
|
interface ServicesSectionProps {
|
|
highlightCards?: HighlightCard[];
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
export function ServicesSection({ highlightCards = [], isLoading = false }: ServicesSectionProps) {
|
|
const [isVisible, setIsVisible] = useState(false);
|
|
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
|
|
const sectionRef = useRef<HTMLDivElement>(null);
|
|
|
|
// 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
|
|
}));
|
|
|
|
// 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();
|
|
}
|
|
};
|
|
|
|
// 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>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section
|
|
ref={sectionRef}
|
|
className="py-16 lg:py-20"
|
|
style={{
|
|
backgroundColor: '#fff',
|
|
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">
|
|
{/* Left Side - Sticky Content */}
|
|
<div className="col-span-5 sticky top-24 self-start">
|
|
<div className="recognition-header pr-8">
|
|
<BrandedTag
|
|
text="Our Services"
|
|
/>
|
|
<h2
|
|
id="recognition-section-heading"
|
|
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 */}
|
|
<div className="primary-cta-container-left cta-left-locked">
|
|
<StandardCTAButton
|
|
text="Services Page"
|
|
onClick={() => navigateTo('/services')}
|
|
ariaLabel="Explore our services"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Side - Scrolling Cards */}
|
|
<div className="col-span-7">
|
|
<div
|
|
className="recognition-cards space-y-6"
|
|
role="list"
|
|
aria-label="Leadership development services"
|
|
>
|
|
{serviceItems.map((item, index) => (
|
|
<div
|
|
key={item.id}
|
|
ref={(el) => addCardRef(el, index)}
|
|
className={`recognition-card group scroll-animate-stagger cursor-pointer focus-ring ${isVisible ? 'animate-in' : ''}`}
|
|
role="listitem"
|
|
aria-labelledby={`recognition-title-${item.id}`}
|
|
aria-describedby={`recognition-desc-${item.id}`}
|
|
tabIndex={0}
|
|
onKeyDown={(e) => handleKeyDown(e, index)}
|
|
style={{
|
|
transitionDelay: `${(index + 1) * 150}ms`,
|
|
opacity: isVisible ? 1 : 0
|
|
}}
|
|
onClick={() => navigateTo(item.route)}
|
|
>
|
|
<div
|
|
className="p-8 transition-all duration-300 hover:shadow-xl hover:-translate-y-1 border bg-white"
|
|
style={{
|
|
borderColor: 'var(--color-border)',
|
|
borderRadius: '12px',
|
|
fontFamily: 'var(--font-family-brand)'
|
|
}}
|
|
>
|
|
<div className="flex items-start mb-6">
|
|
<div
|
|
className="w-14 h-14 flex items-center justify-center transition-transform duration-300 group-hover:scale-110"
|
|
style={{
|
|
backgroundColor: 'var(--color-brand-primary)',
|
|
borderRadius: '12px',
|
|
}}
|
|
>
|
|
{/* 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
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="recognition-card-content">
|
|
<h3
|
|
id={`recognition-title-${item.id}`}
|
|
className="text-h4 mb-4"
|
|
>
|
|
{item.title}
|
|
</h3>
|
|
<p
|
|
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">
|
|
<BrandedTag
|
|
text="Our Services"
|
|
/>
|
|
<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">
|
|
<StandardCTAButton
|
|
text="Services Page"
|
|
onClick={() => navigateTo('/services')}
|
|
ariaLabel="Explore our services"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Horizontal Scrollable Cards */}
|
|
<div className="relative">
|
|
<div
|
|
className="flex gap-6 overflow-x-auto scrollbar-hide pb-4"
|
|
style={{
|
|
scrollSnapType: 'x mandatory',
|
|
WebkitOverflowScrolling: 'touch'
|
|
}}
|
|
role="list"
|
|
aria-label="Leadership development services"
|
|
>
|
|
{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)}
|
|
style={{
|
|
scrollSnapAlign: 'start',
|
|
width: '320px',
|
|
transitionDelay: `${(index + 1) * 150}ms`,
|
|
opacity: isVisible ? 1 : 0
|
|
}}
|
|
>
|
|
<div
|
|
className="p-6 transition-all duration-300 hover:shadow-xl hover:-translate-y-1 border bg-white h-full"
|
|
style={{
|
|
borderColor: 'var(--color-border)',
|
|
borderRadius: '12px',
|
|
fontFamily: 'var(--font-family-brand)'
|
|
}}
|
|
>
|
|
<div className="flex items-start mb-6">
|
|
<div
|
|
className="w-12 h-12 flex items-center justify-center transition-transform duration-300 group-hover:scale-110"
|
|
style={{
|
|
backgroundColor: 'var(--color-brand-primary)',
|
|
borderRadius: '12px',
|
|
}}
|
|
>
|
|
<ImageWithFallback
|
|
src={item.iconUrl}
|
|
alt={item.accessibleLabel || item.title}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="recognition-card-content">
|
|
<h3
|
|
id={`recognition-title-mobile-${item.id}`}
|
|
className="text-h4 mb-4"
|
|
>
|
|
{item.title}
|
|
</h3>
|
|
<p
|
|
id={`recognition-desc-mobile-${item.id}`}
|
|
className="text-small text-muted leading-relaxed"
|
|
>
|
|
{item.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
} |