12
12
{% if upcoming_events %}
13
13
## Upcoming events
14
14
15
- < div class ="events-grid ">
15
+ < div class ="events-carousel-container ">
16
+ < div class ="events-carousel ">
17
+ < div class ="events-carousel-track ">
16
18
{% for event in site.data.events %}
17
19
{% assign event_date = event.date | date: "%Y-%m-%d" %}
18
20
{% assign today = site.time | date: "%Y-%m-%d" %}
@@ -112,12 +114,251 @@ <h3 class="event-title">
112
114
</ article >
113
115
{% endif %}
114
116
{% endfor %}
117
+ </ div >
118
+ </ div >
119
+
120
+ <!-- Carousel Navigation -->
121
+ < button class ="carousel-btn carousel-btn-prev " onclick ="moveCarousel(-1) ">
122
+ < svg viewBox ="0 0 24 24 " fill ="none " stroke ="currentColor " stroke-width ="2 ">
123
+ < polyline points ="15,18 9,12 15,6 "> </ polyline >
124
+ </ svg >
125
+ </ button >
126
+ < button class ="carousel-btn carousel-btn-next " onclick ="moveCarousel(1) ">
127
+ < svg viewBox ="0 0 24 24 " fill ="none " stroke ="currentColor " stroke-width ="2 ">
128
+ < polyline points ="9,6 15,12 9,18 "> </ polyline >
129
+ </ svg >
130
+ </ button >
131
+
132
+ <!-- Carousel Indicators -->
133
+ < div class ="carousel-indicators ">
134
+ <!-- Will be populated by JavaScript -->
135
+ </ div >
115
136
</ div >
116
137
117
138
< script >
118
- // Add intersection observer for scroll animations (upcoming events)
139
+ // Carousel functionality for upcoming events
140
+ let currentSlide = 0 ;
141
+ let eventsCarousel = null ;
142
+ let eventsTrack = null ;
143
+ let totalSlides = 0 ;
144
+ let isInitialized = false ;
145
+ let autoAdvanceInterval = null ;
146
+
147
+ function initializeCarousel ( ) {
148
+ if ( isInitialized ) return ;
149
+
150
+ eventsCarousel = document . querySelector ( '.events-carousel' ) ;
151
+ eventsTrack = document . querySelector ( '.events-carousel-track' ) ;
152
+
153
+ if ( ! eventsCarousel || ! eventsTrack ) return ;
154
+
155
+ const eventCards = eventsTrack . querySelectorAll ( '.event-card' ) ;
156
+ totalSlides = eventCards . length ;
157
+
158
+ if ( totalSlides === 0 ) return ;
159
+
160
+ const carouselContainer = document . querySelector ( '.events-carousel-container' ) ;
161
+
162
+ // Mark container for single slide styling
163
+ if ( totalSlides === 1 ) {
164
+ carouselContainer . setAttribute ( 'data-single-slide' , 'true' ) ;
165
+ } else {
166
+ carouselContainer . setAttribute ( 'data-single-slide' , 'false' ) ;
167
+ }
168
+
169
+ // Create indicators only if more than one slide
170
+ if ( totalSlides > 1 ) {
171
+ createIndicators ( ) ;
172
+ setupNavigationButtons ( ) ;
173
+ }
174
+
175
+ // Set initial ARIA attributes
176
+ updateAriaAttributes ( ) ;
177
+
178
+ updateCarouselPosition ( ) ;
179
+ isInitialized = true ;
180
+
181
+ console . log ( `Carousel initialized with ${ totalSlides } slides` ) ;
182
+ }
183
+
184
+ function createIndicators ( ) {
185
+ const indicatorsContainer = document . querySelector ( '.carousel-indicators' ) ;
186
+ indicatorsContainer . innerHTML = '' ;
187
+
188
+ for ( let i = 0 ; i < totalSlides ; i ++ ) {
189
+ const indicator = document . createElement ( 'button' ) ;
190
+ indicator . className = `carousel-indicator ${ i === 0 ? 'active' : '' } ` ;
191
+ indicator . setAttribute ( 'aria-label' , `Go to slide ${ i + 1 } ` ) ;
192
+ indicator . onclick = ( ) => goToSlide ( i ) ;
193
+ indicatorsContainer . appendChild ( indicator ) ;
194
+ }
195
+ }
196
+
197
+ function setupNavigationButtons ( ) {
198
+ const prevBtn = document . querySelector ( '.carousel-btn-prev' ) ;
199
+ const nextBtn = document . querySelector ( '.carousel-btn-next' ) ;
200
+
201
+ if ( prevBtn && nextBtn ) {
202
+ prevBtn . setAttribute ( 'aria-label' , 'Previous event' ) ;
203
+ nextBtn . setAttribute ( 'aria-label' , 'Next event' ) ;
204
+
205
+ // Add keyboard support
206
+ prevBtn . addEventListener ( 'keydown' , ( e ) => {
207
+ if ( e . key === 'Enter' || e . key === ' ' ) {
208
+ e . preventDefault ( ) ;
209
+ moveCarousel ( - 1 ) ;
210
+ }
211
+ } ) ;
212
+
213
+ nextBtn . addEventListener ( 'keydown' , ( e ) => {
214
+ if ( e . key === 'Enter' || e . key === ' ' ) {
215
+ e . preventDefault ( ) ;
216
+ moveCarousel ( 1 ) ;
217
+ }
218
+ } ) ;
219
+ }
220
+ }
221
+
222
+ function moveCarousel ( direction ) {
223
+ if ( totalSlides <= 1 ) return ;
224
+
225
+ stopAutoAdvance ( ) ; // Stop auto-advance when user interacts
226
+
227
+ currentSlide += direction ;
228
+
229
+ if ( currentSlide >= totalSlides ) {
230
+ currentSlide = 0 ;
231
+ } else if ( currentSlide < 0 ) {
232
+ currentSlide = totalSlides - 1 ;
233
+ }
234
+
235
+ updateCarouselPosition ( ) ;
236
+
237
+ // Restart auto-advance after user interaction
238
+ setTimeout ( ( ) => {
239
+ if ( document . querySelector ( '.events-carousel-container:hover' ) === null ) {
240
+ startAutoAdvance ( ) ;
241
+ }
242
+ } , 3000 ) ;
243
+ }
244
+
245
+ function goToSlide ( slideIndex ) {
246
+ if ( slideIndex >= 0 && slideIndex < totalSlides && slideIndex !== currentSlide ) {
247
+ stopAutoAdvance ( ) ;
248
+ currentSlide = slideIndex ;
249
+ updateCarouselPosition ( ) ;
250
+
251
+ // Restart auto-advance
252
+ setTimeout ( ( ) => {
253
+ if ( document . querySelector ( '.events-carousel-container:hover' ) === null ) {
254
+ startAutoAdvance ( ) ;
255
+ }
256
+ } , 3000 ) ;
257
+ }
258
+ }
259
+
260
+ function updateCarouselPosition ( ) {
261
+ if ( ! eventsTrack ) return ;
262
+
263
+ const translateX = - currentSlide * 100 ;
264
+ eventsTrack . style . transform = `translateX(${ translateX } %)` ;
265
+
266
+ // Update indicators
267
+ const indicators = document . querySelectorAll ( '.carousel-indicator' ) ;
268
+ indicators . forEach ( ( indicator , index ) => {
269
+ indicator . classList . toggle ( 'active' , index === currentSlide ) ;
270
+ indicator . setAttribute ( 'aria-pressed' , index === currentSlide ? 'true' : 'false' ) ;
271
+ } ) ;
272
+
273
+ // Update button states (don't disable for infinite loop)
274
+ const prevBtn = document . querySelector ( '.carousel-btn-prev' ) ;
275
+ const nextBtn = document . querySelector ( '.carousel-btn-next' ) ;
276
+
277
+ if ( prevBtn && nextBtn ) {
278
+ // For infinite carousel, buttons are never disabled
279
+ prevBtn . disabled = false ;
280
+ nextBtn . disabled = false ;
281
+ }
282
+
283
+ updateAriaAttributes ( ) ;
284
+ }
285
+
286
+ function updateAriaAttributes ( ) {
287
+ const eventCards = eventsTrack . querySelectorAll ( '.event-card' ) ;
288
+ eventCards . forEach ( ( card , index ) => {
289
+ const isVisible = index === currentSlide ;
290
+ card . setAttribute ( 'aria-hidden' , ! isVisible ) ;
291
+ card . setAttribute ( 'tabindex' , isVisible ? '0' : '-1' ) ;
292
+ } ) ;
293
+
294
+ // Update carousel region
295
+ const carouselContainer = document . querySelector ( '.events-carousel-container' ) ;
296
+ if ( carouselContainer ) {
297
+ carouselContainer . setAttribute ( 'aria-label' , `Event ${ currentSlide + 1 } of ${ totalSlides } ` ) ;
298
+ }
299
+ }
300
+
301
+ function startAutoAdvance ( ) {
302
+ if ( totalSlides <= 1 || autoAdvanceInterval ) return ;
303
+
304
+ // Check if user prefers reduced motion
305
+ if ( window . matchMedia ( '(prefers-reduced-motion: reduce)' ) . matches ) {
306
+ return ;
307
+ }
308
+
309
+ autoAdvanceInterval = setInterval ( ( ) => {
310
+ moveCarousel ( 1 ) ;
311
+ } , 8000 ) ; // Change slide every 8 seconds
312
+ }
313
+
314
+ function stopAutoAdvance ( ) {
315
+ if ( autoAdvanceInterval ) {
316
+ clearInterval ( autoAdvanceInterval ) ;
317
+ autoAdvanceInterval = null ;
318
+ }
319
+ }
320
+
321
+ // Initialize when DOM is ready
322
+ document . addEventListener ( 'DOMContentLoaded' , ( ) => {
323
+ setTimeout ( initializeCarousel , 100 ) ;
324
+ } ) ;
325
+
326
+ // Also initialize after a short delay in case DOM isn't fully ready
327
+ setTimeout ( ( ) => {
328
+ if ( ! isInitialized ) {
329
+ initializeCarousel ( ) ;
330
+ }
331
+ } , 500 ) ;
332
+
333
+ // Add intersection observer for scroll animations and auto-advance trigger
119
334
if ( 'IntersectionObserver' in window ) {
120
- const upcomingCards = document . querySelectorAll ( '.event-card' ) ;
335
+ const carouselContainer = document . querySelector ( '.events-carousel-container' ) ;
336
+
337
+ if ( carouselContainer ) {
338
+ const carouselObserver = new IntersectionObserver ( ( entries ) => {
339
+ entries . forEach ( ( entry ) => {
340
+ if ( entry . isIntersecting ) {
341
+ // Start auto-advance when carousel becomes visible
342
+ setTimeout ( ( ) => {
343
+ if ( isInitialized ) {
344
+ startAutoAdvance ( ) ;
345
+ }
346
+ } , 2000 ) ;
347
+ } else {
348
+ // Stop auto-advance when carousel is not visible
349
+ stopAutoAdvance ( ) ;
350
+ }
351
+ } ) ;
352
+ } , {
353
+ threshold : 0.3 ,
354
+ rootMargin : '50px'
355
+ } ) ;
356
+
357
+ carouselObserver . observe ( carouselContainer ) ;
358
+ }
359
+
360
+ // Animate individual cards
361
+ const upcomingCards = document . querySelectorAll ( '.events-carousel .event-card' ) ;
121
362
const upcomingObserver = new IntersectionObserver ( ( entries ) => {
122
363
entries . forEach ( ( entry , index ) => {
123
364
if ( entry . isIntersecting ) {
@@ -134,13 +375,112 @@ <h3 class="event-title">
134
375
} ) ;
135
376
136
377
upcomingCards . forEach ( ( card ) => {
137
- if ( ! card . style . opacity ) { // Only apply if not already set by past events script
378
+ if ( ! card . style . opacity ) {
138
379
card . style . opacity = '0' ;
139
380
card . style . transform = 'translateY(30px)' ;
140
381
card . style . transition = 'opacity 0.6s ease, transform 0.6s ease' ;
141
382
upcomingObserver . observe ( card ) ;
142
383
}
143
384
} ) ;
144
385
}
386
+
387
+ // Pause auto-advance on hover and focus
388
+ document . addEventListener ( 'DOMContentLoaded' , ( ) => {
389
+ const carouselContainer = document . querySelector ( '.events-carousel-container' ) ;
390
+ if ( carouselContainer ) {
391
+ carouselContainer . addEventListener ( 'mouseenter' , stopAutoAdvance ) ;
392
+ carouselContainer . addEventListener ( 'mouseleave' , ( ) => {
393
+ setTimeout ( startAutoAdvance , 1000 ) ;
394
+ } ) ;
395
+
396
+ carouselContainer . addEventListener ( 'focusin' , stopAutoAdvance ) ;
397
+ carouselContainer . addEventListener ( 'focusout' , ( ) => {
398
+ setTimeout ( startAutoAdvance , 1000 ) ;
399
+ } ) ;
400
+ }
401
+ } ) ;
402
+
403
+ // Handle touch events for mobile swipe
404
+ let touchStartX = 0 ;
405
+ let touchEndX = 0 ;
406
+ let touchStartY = 0 ;
407
+ let touchEndY = 0 ;
408
+ let isSwiping = false ;
409
+
410
+ document . addEventListener ( 'DOMContentLoaded' , ( ) => {
411
+ const carouselContainer = document . querySelector ( '.events-carousel-container' ) ;
412
+ if ( carouselContainer ) {
413
+ carouselContainer . addEventListener ( 'touchstart' , ( e ) => {
414
+ touchStartX = e . changedTouches [ 0 ] . screenX ;
415
+ touchStartY = e . changedTouches [ 0 ] . screenY ;
416
+ isSwiping = true ;
417
+ } , { passive : true } ) ;
418
+
419
+ carouselContainer . addEventListener ( 'touchmove' , ( e ) => {
420
+ if ( ! isSwiping ) return ;
421
+
422
+ const currentX = e . changedTouches [ 0 ] . screenX ;
423
+ const currentY = e . changedTouches [ 0 ] . screenY ;
424
+ const diffX = Math . abs ( currentX - touchStartX ) ;
425
+ const diffY = Math . abs ( currentY - touchStartY ) ;
426
+
427
+ // If horizontal swipe is more pronounced than vertical, prevent scrolling
428
+ if ( diffX > diffY && diffX > 10 ) {
429
+ e . preventDefault ( ) ;
430
+ }
431
+ } , { passive : false } ) ;
432
+
433
+ carouselContainer . addEventListener ( 'touchend' , ( e ) => {
434
+ if ( ! isSwiping ) return ;
435
+
436
+ touchEndX = e . changedTouches [ 0 ] . screenX ;
437
+ touchEndY = e . changedTouches [ 0 ] . screenY ;
438
+ handleSwipe ( ) ;
439
+ isSwiping = false ;
440
+ } , { passive : true } ) ;
441
+ }
442
+ } ) ;
443
+
444
+ function handleSwipe ( ) {
445
+ const swipeThreshold = 50 ;
446
+ const diffX = touchStartX - touchEndX ;
447
+ const diffY = Math . abs ( touchStartY - touchEndY ) ;
448
+
449
+ // Only trigger swipe if horizontal movement is more significant than vertical
450
+ if ( Math . abs ( diffX ) > swipeThreshold && Math . abs ( diffX ) > diffY ) {
451
+ if ( diffX > 0 ) {
452
+ moveCarousel ( 1 ) ; // Swipe left - next slide
453
+ } else {
454
+ moveCarousel ( - 1 ) ; // Swipe right - previous slide
455
+ }
456
+ }
457
+ }
458
+
459
+ // Handle keyboard navigation
460
+ document . addEventListener ( 'keydown' , ( e ) => {
461
+ const carouselContainer = document . querySelector ( '.events-carousel-container' ) ;
462
+ if ( ! carouselContainer || ! carouselContainer . contains ( document . activeElement ) ) {
463
+ return ;
464
+ }
465
+
466
+ switch ( e . key ) {
467
+ case 'ArrowLeft' :
468
+ e . preventDefault ( ) ;
469
+ moveCarousel ( - 1 ) ;
470
+ break ;
471
+ case 'ArrowRight' :
472
+ e . preventDefault ( ) ;
473
+ moveCarousel ( 1 ) ;
474
+ break ;
475
+ case 'Home' :
476
+ e . preventDefault ( ) ;
477
+ goToSlide ( 0 ) ;
478
+ break ;
479
+ case 'End' :
480
+ e . preventDefault ( ) ;
481
+ goToSlide ( totalSlides - 1 ) ;
482
+ break ;
483
+ }
484
+ } ) ;
145
485
</ script >
146
486
{% endif %}
0 commit comments