diff --git a/frontend/public/images/carrousel/2026-01-29 13.09.59.jpg b/frontend/public/images/carrousel/2026-01-29 13.09.59.jpg new file mode 100644 index 0000000..a986426 Binary files /dev/null and b/frontend/public/images/carrousel/2026-01-29 13.09.59.jpg differ diff --git a/frontend/public/images/carrousel/2026-01-29 13.10.12.jpg b/frontend/public/images/carrousel/2026-01-29 13.10.12.jpg new file mode 100644 index 0000000..5ef68d4 Binary files /dev/null and b/frontend/public/images/carrousel/2026-01-29 13.10.12.jpg differ diff --git a/frontend/public/images/carrousel/2026-01-29 13.10.16.jpg b/frontend/public/images/carrousel/2026-01-29 13.10.16.jpg new file mode 100644 index 0000000..7dbb84f Binary files /dev/null and b/frontend/public/images/carrousel/2026-01-29 13.10.16.jpg differ diff --git a/frontend/public/images/carrousel/2026-01-29 13.10.20.jpg b/frontend/public/images/carrousel/2026-01-29 13.10.20.jpg new file mode 100644 index 0000000..7e0563e Binary files /dev/null and b/frontend/public/images/carrousel/2026-01-29 13.10.20.jpg differ diff --git a/frontend/public/images/carrousel/2026-02-01 02.53.00.jpg b/frontend/public/images/carrousel/2026-02-01 02.53.00.jpg new file mode 100644 index 0000000..e0c8e7d Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-01 02.53.00.jpg differ diff --git a/frontend/public/images/carrousel/2026-02-01 02.53.30.jpg b/frontend/public/images/carrousel/2026-02-01 02.53.30.jpg new file mode 100644 index 0000000..5eda3f7 Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-01 02.53.30.jpg differ diff --git a/frontend/public/images/carrousel/2026-02-01 02.53.36.jpg b/frontend/public/images/carrousel/2026-02-01 02.53.36.jpg new file mode 100644 index 0000000..eca6c29 Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-01 02.53.36.jpg differ diff --git a/frontend/public/images/carrousel/2026-02-01 02.53.41.jpg b/frontend/public/images/carrousel/2026-02-01 02.53.41.jpg new file mode 100644 index 0000000..6645467 Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-01 02.53.41.jpg differ diff --git a/frontend/public/images/carrousel/2026-02-01 02.53.45.jpg b/frontend/public/images/carrousel/2026-02-01 02.53.45.jpg new file mode 100644 index 0000000..573dcc6 Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-01 02.53.45.jpg differ diff --git a/frontend/public/images/carrousel/2026-02-01 02.53.48.jpg b/frontend/public/images/carrousel/2026-02-01 02.53.48.jpg new file mode 100644 index 0000000..bd555df Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-01 02.53.48.jpg differ diff --git a/frontend/src/app/(public)/components/MediaCarouselSection.tsx b/frontend/src/app/(public)/components/MediaCarouselSection.tsx new file mode 100644 index 0000000..72c437a --- /dev/null +++ b/frontend/src/app/(public)/components/MediaCarouselSection.tsx @@ -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(null); + const [touchEnd, setTouchEnd] = useState(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 ( +
+
+ {/* Header */} +
+

+ {t('home.carousel.title')} +

+

+ {t('home.carousel.subtitle')} +

+
+ + {/* Carousel Container */} +
+ {/* Main Image Container */} +
+ {images.map((image, index) => ( +
+ {image.alt} +
+ ))} + + {/* Soft gradient overlay for polish */} +
+
+ + {/* Navigation Arrows */} + + + + + {/* Dots Navigation */} +
+ {images.map((_, index) => ( +
+
+ + {/* CTA Section - Outside carousel */} +
+ + + +
+
+
+ ); +} diff --git a/frontend/src/app/(public)/page.tsx b/frontend/src/app/(public)/page.tsx index 805eb35..24ef376 100644 --- a/frontend/src/app/(public)/page.tsx +++ b/frontend/src/app/(public)/page.tsx @@ -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 ( <> + ); diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index acb1813..c9681e7 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -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", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index 24ef7e0..d4b72b9 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -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", diff --git a/frontend/src/lib/carouselImages.ts b/frontend/src/lib/carouselImages.ts new file mode 100644 index 0000000..4900224 --- /dev/null +++ b/frontend/src/lib/carouselImages.ts @@ -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 []; + } +}