Skip to content

Commit 62d0f65

Browse files
authored
Merge pull request #106 from CloudNativeLinz/feature/adding-autumn-editions
Convert upcoming events to carousel with enhanced styling
2 parents 4b91f50 + 96301ab commit 62d0f65

File tree

2 files changed

+550
-10
lines changed

2 files changed

+550
-10
lines changed

_includes/upcoming-events.html

Lines changed: 344 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
{% if upcoming_events %}
1313
## Upcoming events
1414

15-
<div class="events-grid">
15+
<div class="events-carousel-container">
16+
<div class="events-carousel">
17+
<div class="events-carousel-track">
1618
{% for event in site.data.events %}
1719
{% assign event_date = event.date | date: "%Y-%m-%d" %}
1820
{% assign today = site.time | date: "%Y-%m-%d" %}
@@ -112,12 +114,251 @@ <h3 class="event-title">
112114
</article>
113115
{% endif %}
114116
{% 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>
115136
</div>
116137

117138
<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
119334
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');
121362
const upcomingObserver = new IntersectionObserver((entries) => {
122363
entries.forEach((entry, index) => {
123364
if (entry.isIntersecting) {
@@ -134,13 +375,112 @@ <h3 class="event-title">
134375
});
135376

136377
upcomingCards.forEach((card) => {
137-
if (!card.style.opacity) { // Only apply if not already set by past events script
378+
if (!card.style.opacity) {
138379
card.style.opacity = '0';
139380
card.style.transform = 'translateY(30px)';
140381
card.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
141382
upcomingObserver.observe(card);
142383
}
143384
});
144385
}
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+
});
145485
</script>
146486
{% endif %}

0 commit comments

Comments
 (0)