33import { useState , useEffect , useRef } from "react" ;
44import PropTypes from "prop-types" ;
55
6- // --- Custom Hook for Intersection Observer ---
76const useIntersectionObserver = ( ref , options ) => {
87 const [ inView , setInView ] = useState ( false ) ;
98
@@ -15,98 +14,27 @@ const useIntersectionObserver = (ref, options) => {
1514 }
1615 } , options ) ;
1716
18- if ( ref . current ) {
19- observer . observe ( ref . current ) ;
20- }
21-
22- return ( ) => {
23- if ( ref . current ) {
24- observer . unobserve ( ref . current ) ;
25- }
26- } ;
17+ if ( ref . current ) observer . observe ( ref . current ) ;
18+ return ( ) => { if ( ref . current ) observer . unobserve ( ref . current ) ; } ;
2719 } , [ ref , options ] ) ;
2820
2921 return inView ;
3022} ;
3123
32- // Continuous carousel - shows 3 cards, scrolls one at a time, loops infinitely
3324const Carousel = ( { slides, title, sectionId } ) => {
34- if ( ! slides || ! Array . isArray ( slides ) || slides . length === 0 ) {
35- return null ;
36- }
25+ if ( ! slides || ! Array . isArray ( slides ) || slides . length === 0 ) return null ;
3726
3827 const titleRef = useRef ( null ) ;
39- const carouselRef = useRef ( null ) ;
40- const navRef = useRef ( null ) ;
28+ const gridRef = useRef ( null ) ;
4129
4230 const titleInView = useIntersectionObserver ( titleRef , { threshold : 0.1 } ) ;
43- const carouselInView = useIntersectionObserver ( carouselRef , { threshold : 0.2 } ) ;
44- const navInView = useIntersectionObserver ( navRef , { threshold : 0.1 } ) ;
45-
46- const [ currentIndex , setCurrentIndex ] = useState ( 0 ) ;
47- const [ isAnimating , setIsAnimating ] = useState ( false ) ;
48- const [ animationDirection , setAnimationDirection ] = useState ( 'next' ) ;
49- const autoplayInterval = useRef ( null ) ;
50- const totalSlides = slides . length ;
51-
52- // Get visible cards (3 cards starting from currentIndex, wrapping around)
53- const getVisibleCards = ( ) => {
54- const cards = [ ] ;
55- for ( let i = 0 ; i < 3 ; i ++ ) {
56- const index = ( currentIndex + i ) % totalSlides ;
57- cards . push ( { ...slides [ index ] , originalIndex : index } ) ;
58- }
59- return cards ;
60- } ;
61-
62- const nextSlide = ( ) => {
63- if ( isAnimating ) return ;
64- setAnimationDirection ( 'next' ) ;
65- setIsAnimating ( true ) ;
66- setTimeout ( ( ) => {
67- setCurrentIndex ( ( prev ) => ( prev + 1 ) % totalSlides ) ;
68- setIsAnimating ( false ) ;
69- } , 300 ) ;
70- } ;
71-
72- const prevSlide = ( ) => {
73- if ( isAnimating ) return ;
74- setAnimationDirection ( 'prev' ) ;
75- setIsAnimating ( true ) ;
76- setTimeout ( ( ) => {
77- setCurrentIndex ( ( prev ) => ( prev - 1 + totalSlides ) % totalSlides ) ;
78- setIsAnimating ( false ) ;
79- } , 300 ) ;
80- } ;
81-
82- const goToSlide = ( index ) => {
83- if ( isAnimating || index === currentIndex ) return ;
84- setAnimationDirection ( index > currentIndex ? 'next' : 'prev' ) ;
85- setIsAnimating ( true ) ;
86- setTimeout ( ( ) => {
87- setCurrentIndex ( index ) ;
88- setIsAnimating ( false ) ;
89- } , 300 ) ;
90- } ;
91-
92- const startAutoPlay = ( ) => {
93- stopAutoPlay ( ) ;
94- autoplayInterval . current = setInterval ( nextSlide , 4000 ) ;
95- } ;
96-
97- const stopAutoPlay = ( ) => {
98- clearInterval ( autoplayInterval . current ) ;
99- } ;
100-
101- useEffect ( ( ) => {
102- startAutoPlay ( ) ;
103- return ( ) => stopAutoPlay ( ) ;
104- } , [ ] ) ;
105-
106- const visibleCards = getVisibleCards ( ) ;
31+ const gridInView = useIntersectionObserver ( gridRef , { threshold : 0.1 } ) ;
10732
10833 return (
109- < section className = "py-16 sm:py-20 lg:py-24 bg-gradient-to-b from-dark-bg via-medium-bg to-dark-bg relative overflow-hidden" id = { sectionId } >
34+ < section
35+ className = "py-16 sm:py-20 lg:py-24 bg-gradient-to-b from-dark-bg via-medium-bg to-dark-bg relative overflow-hidden"
36+ id = { sectionId }
37+ >
11038 { /* Background decorative elements */ }
11139 < div className = "absolute inset-0 opacity-[0.06] overflow-hidden" >
11240 < div className = "absolute top-20 left-32 w-96 h-96 bg-gradient-to-br from-primary-orange via-orange-500 to-amber-600 rounded-full blur-3xl animate-pulse" > </ div >
@@ -118,10 +46,9 @@ const Carousel = ({ slides, title, sectionId }) => {
11846 { title && (
11947 < div
12048 ref = { titleRef }
121- className = { `
122- text-center mb-12 sm:mb-14 lg:mb-16 transition-all duration-700 ease-out
123- ${ titleInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10' }
124- ` }
49+ className = { `text-center mb-12 sm:mb-14 lg:mb-16 transition-all duration-700 ease-out ${
50+ titleInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
51+ } `}
12552 >
12653 < h2 className = "text-2xl sm:text-3xl lg:text-4xl font-bold text-white mb-4 bg-gradient-to-r from-white via-primary-orange to-amber-500 bg-clip-text text-transparent" >
12754 { title }
@@ -133,96 +60,51 @@ const Carousel = ({ slides, title, sectionId }) => {
13360 </ div >
13461 ) }
13562
63+ { /* All cards displayed at once — 1 col mobile, 2 col tablet, 4 col desktop */ }
13664 < div
137- ref = { carouselRef }
138- className = { `
139- relative transition-all duration-700 ease-out delay-200
140- ${ carouselInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10' }
141- ` }
142- onMouseEnter = { stopAutoPlay }
143- onMouseLeave = { startAutoPlay }
65+ ref = { gridRef }
66+ className = { `grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8 transition-all duration-700 ease-out delay-200 ${
67+ gridInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
68+ } `}
14469 >
145- { /* Cards Container */ }
146- < div className = { `grid grid-cols-1 lg:grid-cols-3 gap-6 lg:gap-8 transition-all duration-300 ${ isAnimating ? ( animationDirection === 'next' ? 'opacity-0 -translate-x-4' : 'opacity-0 translate-x-4' ) : 'opacity-100 translate-x-0' } ` } >
147- { visibleCards . map ( ( card , idx ) => (
148- < div
149- key = { `${ card . originalIndex } -${ currentIndex } -${ idx } ` }
150- className = "bg-gradient-to-br from-dark-bg/90 via-medium-bg/80 to-dark-bg/90 backdrop-blur-md rounded-2xl border border-light-bg/30 p-8 sm:p-10 shadow-card hover:shadow-card-hover transition-all duration-300 hover:border-primary-orange/50 hover:scale-[1.03] hover:-translate-y-2 group"
151- >
152- { /* Card Header */ }
153- < div className = "flex items-center gap-5 mb-6" >
154- < div className = "w-16 h-16 sm:w-18 sm:h-18 bg-gradient-to-br from-primary-orange/25 to-primary-yellow/25 backdrop-blur-md rounded-2xl border border-primary-orange/40 flex items-center justify-center text-3xl sm:text-4xl group-hover:scale-110 group-hover:shadow-glow-sm transition-all duration-300 shadow-lg" >
155- { card . icon }
156- </ div >
157- < div >
158- < h3 className = "text-white font-bold text-xl sm:text-2xl group-hover:text-primary-orange transition-colors duration-300" >
159- { card . title }
160- </ h3 >
161- </ div >
70+ { slides . map ( ( card , idx ) => (
71+ < div
72+ key = { idx }
73+ className = "bg-gradient-to-br from-dark-bg/90 via-medium-bg/80 to-dark-bg/90 backdrop-blur-md rounded-2xl border border-light-bg/30 p-8 sm:p-10 shadow-card hover:shadow-card-hover transition-all duration-300 hover:border-primary-orange/50 hover:scale-[1.03] hover:-translate-y-2 group flex flex-col"
74+ >
75+ { /* Card Header */ }
76+ < div className = "flex items-center gap-5 mb-6" >
77+ < div className = "w-16 h-16 bg-gradient-to-br from-primary-orange/25 to-primary-yellow/25 backdrop-blur-md rounded-2xl border border-primary-orange/40 flex items-center justify-center group-hover:scale-110 group-hover:shadow-glow-sm transition-all duration-300 shadow-lg shrink-0" >
78+ { card . icon }
16279 </ div >
163-
164- { /* Card Content */ }
165- < p className = "text-gray-300 text-base sm:text-lg leading-relaxed mb-8" >
166- { card . description }
167- </ p >
168-
169- { /* Card Action */ }
170- { card . link && (
171- < div className = "flex justify-start" >
172- < a
173- href = { card . link }
174- target = "_blank"
175- rel = "noopener noreferrer"
176- className = "inline-flex items-center justify-center px-6 py-3 bg-gradient-to-r from-primary-orange/20 to-primary-yellow/20 text-primary-orange font-semibold rounded-xl border border-primary-orange/40 hover:bg-gradient-to-r hover:from-primary-orange hover:to-primary-yellow hover:text-white transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-primary-orange/20 text-base"
177- >
178- { card . linkText }
179- < svg className = "ml-2 w-5 h-5" fill = "currentColor" viewBox = "0 0 24 24" >
180- < path d = "M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z" />
181- </ svg >
182- </ a >
183- </ div >
184- ) }
80+ < h3 className = "text-white font-bold text-xl sm:text-2xl group-hover:text-primary-orange transition-colors duration-300" >
81+ { card . title }
82+ </ h3 >
18583 </ div >
186- ) ) }
187- </ div >
18884
189- { /* Navigation */ }
190- < div
191- ref = { navRef }
192- className = { `
193- flex justify-center items-center gap-6 mt-12 transition-all duration-700 ease-out delay-300
194- ${ navInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10' }
195- ` }
196- >
197- < button
198- onClick = { prevSlide }
199- className = "bg-gradient-to-r from-primary-orange to-primary-yellow text-white w-12 h-12 sm:w-14 sm:h-14 rounded-xl flex items-center justify-center cursor-pointer transition-all duration-300 text-2xl font-bold hover:scale-110 hover:shadow-xl hover:shadow-primary-orange/30 hover:-translate-y-0.5"
200- aria-label = "Previous slide"
201- >
202- ‹
203- </ button >
204- < div className = "flex gap-3" >
205- { slides . map ( ( _ , index ) => (
206- < div
207- key = { index }
208- onClick = { ( ) => goToSlide ( index ) }
209- className = { `w-3 h-3 sm:w-3.5 sm:h-3.5 rounded-full cursor-pointer transition-all duration-300 ${
210- currentIndex === index
211- ? "bg-gradient-to-r from-primary-orange to-primary-yellow scale-125 shadow-lg shadow-primary-orange/50"
212- : "bg-gray-600 hover:bg-gray-500 hover:scale-110"
213- } `}
214- aria-label = { `Go to slide ${ index + 1 } ` }
215- />
216- ) ) }
85+ { /* Card Content */ }
86+ < p className = "text-gray-300 text-base sm:text-lg leading-relaxed mb-8 flex-1" >
87+ { card . description }
88+ </ p >
89+
90+ { /* Card Action */ }
91+ { card . link && (
92+ < div className = "flex justify-start" >
93+ < a
94+ href = { card . link }
95+ target = "_blank"
96+ rel = "noopener noreferrer"
97+ className = "inline-flex items-center justify-center px-6 py-3 bg-gradient-to-r from-primary-orange/20 to-primary-yellow/20 text-primary-orange font-semibold rounded-xl border border-primary-orange/40 hover:bg-gradient-to-r hover:from-primary-orange hover:to-primary-yellow hover:text-white transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-primary-orange/20 text-base"
98+ >
99+ { card . linkText }
100+ < svg className = "ml-2 w-5 h-5" fill = "currentColor" viewBox = "0 0 24 24" >
101+ < path d = "M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z" />
102+ </ svg >
103+ </ a >
104+ </ div >
105+ ) }
217106 </ div >
218- < button
219- onClick = { nextSlide }
220- className = "bg-gradient-to-r from-primary-orange to-primary-yellow text-white w-12 h-12 sm:w-14 sm:h-14 rounded-xl flex items-center justify-center cursor-pointer transition-all duration-300 text-2xl font-bold hover:scale-110 hover:shadow-xl hover:shadow-primary-orange/30 hover:-translate-y-0.5"
221- aria-label = "Next slide"
222- >
223- ›
224- </ button >
225- </ div >
107+ ) ) }
226108 </ div >
227109 </ div >
228110 </ section >
0 commit comments