Add media carousel section to homepage

Display past event photos in an auto-playing carousel between the
"What is Spanglish?" and "Stay Updated" sections. Images are loaded
dynamically from /images/carrousel/ folder with support for jpg, png,
and webp formats.
This commit is contained in:
Michilis
2026-02-01 06:07:58 +00:00
parent 6df3baf0be
commit b0cbaa60f0
15 changed files with 255 additions and 0 deletions

View File

@@ -0,0 +1,199 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { useLanguage } from '@/context/LanguageContext';
import Button from '@/components/ui/Button';
import { ChevronLeftIcon, ChevronRightIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
export interface CarouselImage {
src: string;
alt: string;
}
interface MediaCarouselSectionProps {
images: CarouselImage[];
}
export default function MediaCarouselSection({ images }: MediaCarouselSectionProps) {
const { t } = useLanguage();
const [currentIndex, setCurrentIndex] = useState(0);
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
const [touchStart, setTouchStart] = useState<number | null>(null);
const [touchEnd, setTouchEnd] = useState<number | null>(null);
const goToNext = useCallback(() => {
setCurrentIndex((prev) => (prev + 1) % images.length);
}, [images.length]);
const goToPrevious = useCallback(() => {
setCurrentIndex((prev) => (prev - 1 + images.length) % images.length);
}, [images.length]);
const goToSlide = (index: number) => {
setCurrentIndex(index);
setIsAutoPlaying(false);
// Resume auto-play after 5 seconds of inactivity
setTimeout(() => setIsAutoPlaying(true), 5000);
};
// Auto-play functionality
useEffect(() => {
if (!isAutoPlaying) return;
const interval = setInterval(goToNext, 4000);
return () => clearInterval(interval);
}, [isAutoPlaying, goToNext]);
// Touch handlers for swipe gestures
const handleTouchStart = (e: React.TouchEvent) => {
setTouchEnd(null);
setTouchStart(e.targetTouches[0].clientX);
};
const handleTouchMove = (e: React.TouchEvent) => {
setTouchEnd(e.targetTouches[0].clientX);
};
const handleTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const minSwipeDistance = 50;
if (Math.abs(distance) > minSwipeDistance) {
if (distance > 0) {
goToNext();
} else {
goToPrevious();
}
setIsAutoPlaying(false);
setTimeout(() => setIsAutoPlaying(true), 5000);
}
};
// Keyboard navigation
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowLeft') {
goToPrevious();
setIsAutoPlaying(false);
setTimeout(() => setIsAutoPlaying(true), 5000);
} else if (e.key === 'ArrowRight') {
goToNext();
setIsAutoPlaying(false);
setTimeout(() => setIsAutoPlaying(true), 5000);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [goToNext, goToPrevious]);
// Don't render if no images
if (images.length === 0) {
return null;
}
return (
<section className="section-padding bg-white">
<div className="container-page">
{/* Header */}
<div className="text-center max-w-2xl mx-auto mb-10">
<h2 className="section-title text-primary-dark">
{t('home.carousel.title')}
</h2>
<p className="section-subtitle text-gray-600">
{t('home.carousel.subtitle')}
</p>
</div>
{/* Carousel Container */}
<div
className="relative max-w-4xl mx-auto"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{/* Main Image Container */}
<div className="relative aspect-[16/10] rounded-2xl overflow-hidden bg-gray-100 shadow-lg">
{images.map((image, index) => (
<div
key={image.src}
className={`absolute inset-0 transition-opacity duration-700 ease-in-out ${
index === currentIndex ? 'opacity-100' : 'opacity-0'
}`}
>
<Image
src={image.src}
alt={image.alt}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 900px"
className="object-cover"
priority={index === 0}
loading={index === 0 ? 'eager' : 'lazy'}
/>
</div>
))}
{/* Soft gradient overlay for polish */}
<div className="absolute inset-0 bg-gradient-to-t from-black/10 via-transparent to-transparent pointer-events-none" />
</div>
{/* Navigation Arrows */}
<button
onClick={() => {
goToPrevious();
setIsAutoPlaying(false);
setTimeout(() => setIsAutoPlaying(true), 5000);
}}
className="absolute left-3 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/90 shadow-md flex items-center justify-center text-gray-700 hover:bg-white hover:scale-105 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary-yellow"
aria-label={t('home.carousel.previous')}
>
<ChevronLeftIcon className="w-5 h-5" />
</button>
<button
onClick={() => {
goToNext();
setIsAutoPlaying(false);
setTimeout(() => setIsAutoPlaying(true), 5000);
}}
className="absolute right-3 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/90 shadow-md flex items-center justify-center text-gray-700 hover:bg-white hover:scale-105 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary-yellow"
aria-label={t('home.carousel.next')}
>
<ChevronRightIcon className="w-5 h-5" />
</button>
{/* Dots Navigation */}
<div className="flex justify-center gap-2 mt-6">
{images.map((_, index) => (
<button
key={index}
onClick={() => goToSlide(index)}
className={`w-2.5 h-2.5 rounded-full transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-primary-yellow focus:ring-offset-2 ${
index === currentIndex
? 'bg-primary-yellow w-8'
: 'bg-gray-300 hover:bg-gray-400'
}`}
aria-label={`Go to slide ${index + 1}`}
aria-current={index === currentIndex ? 'true' : 'false'}
/>
))}
</div>
</div>
{/* CTA Section - Outside carousel */}
<div className="text-center mt-10">
<Link href="/events">
<Button variant="outline" size="lg" className="group">
{t('home.carousel.cta')}
<ArrowRightIcon className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
</div>
</div>
</section>
);
}

View File

@@ -1,14 +1,19 @@
import HeroSection from './components/HeroSection';
import NextEventSectionWrapper from './components/NextEventSectionWrapper';
import AboutSection from './components/AboutSection';
import MediaCarouselSection from './components/MediaCarouselSection';
import NewsletterSection from './components/NewsletterSection';
import { getCarouselImages } from '@/lib/carouselImages';
export default function HomePage() {
const carouselImages = getCarouselImages();
return (
<>
<HeroSection />
<NextEventSectionWrapper />
<AboutSection />
<MediaCarouselSection images={carouselImages} />
<NewsletterSection />
</>
);

View File

@@ -53,6 +53,13 @@
"gallery": {
"title": "Our Community"
},
"carousel": {
"title": "Moments from past events",
"subtitle": "A glimpse of our language exchange meetups",
"cta": "See upcoming events",
"previous": "Previous image",
"next": "Next image"
},
"newsletter": {
"title": "Stay Updated",
"description": "Subscribe to get notified about upcoming events",

View File

@@ -53,6 +53,13 @@
"gallery": {
"title": "Nuestra Comunidad"
},
"carousel": {
"title": "Momentos de eventos pasados",
"subtitle": "Un vistazo a nuestros encuentros de intercambio de idiomas",
"cta": "Ver próximos eventos",
"previous": "Imagen anterior",
"next": "Imagen siguiente"
},
"newsletter": {
"title": "Mantente Informado",
"description": "Suscríbete para recibir notificaciones sobre próximos eventos",

View File

@@ -0,0 +1,37 @@
import fs from 'fs';
import path from 'path';
export interface CarouselImage {
src: string;
alt: string;
}
/**
* Reads all images from the carrousel folder at build/request time.
* Supports jpg, jpeg, png, webp, and gif formats.
*/
export function getCarouselImages(): CarouselImage[] {
const carrouselDir = path.join(process.cwd(), 'public', 'images', 'carrousel');
try {
const files = fs.readdirSync(carrouselDir);
// Filter for supported image formats
const supportedExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif'];
const imageFiles = files.filter((file) => {
const ext = path.extname(file).toLowerCase();
return supportedExtensions.includes(ext);
});
// Sort by filename (which includes date) for consistent ordering
imageFiles.sort();
return imageFiles.map((file) => ({
src: `/images/carrousel/${file}`,
alt: 'Spanglish language exchange event moment',
}));
} catch (error) {
console.error('Error reading carrousel directory:', error);
return [];
}
}