350 lines
13 KiB
JavaScript
350 lines
13 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import api from '../../utils/api';
|
|
import Navbar from '../../components/Navbar';
|
|
import MemberFooter from '../../components/MemberFooter';
|
|
import { Card } from '../../components/ui/card';
|
|
import { Button } from '../../components/ui/button';
|
|
import { Dialog, DialogContent } from '../../components/ui/dialog';
|
|
import { Badge } from '../../components/ui/badge';
|
|
import { Image as ImageIcon, Calendar, MapPin, ArrowLeft, X, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
import { useToast } from '../../hooks/use-toast';
|
|
import moment from 'moment';
|
|
|
|
const EventGallery = () => {
|
|
const { eventId } = useParams();
|
|
const navigate = useNavigate();
|
|
const [events, setEvents] = useState([]);
|
|
const [selectedEvent, setSelectedEvent] = useState(null);
|
|
const [galleryImages, setGalleryImages] = useState([]);
|
|
const [selectedImageIndex, setSelectedImageIndex] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [galleryLoading, setGalleryLoading] = useState(false);
|
|
const { toast } = useToast();
|
|
|
|
useEffect(() => {
|
|
fetchEventsWithGalleries();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (eventId) {
|
|
fetchEventGallery(eventId);
|
|
}
|
|
}, [eventId]);
|
|
|
|
const fetchEventsWithGalleries = async () => {
|
|
try {
|
|
const response = await api.get('/members/gallery');
|
|
setEvents(response.data);
|
|
|
|
// If there's an eventId in URL, find that event
|
|
if (eventId) {
|
|
const event = response.data.find(e => e.id === eventId);
|
|
if (event) {
|
|
setSelectedEvent(event);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch events:', error);
|
|
toast({
|
|
title: "Error",
|
|
description: "Failed to load event galleries. Please try again.",
|
|
variant: "destructive"
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchEventGallery = async (id) => {
|
|
setGalleryLoading(true);
|
|
try {
|
|
const response = await api.get(`/events/${id}/gallery`);
|
|
setGalleryImages(response.data);
|
|
} catch (error) {
|
|
console.error('Failed to fetch gallery:', error);
|
|
toast({
|
|
title: "Error",
|
|
description: "Failed to load gallery images. Please try again.",
|
|
variant: "destructive"
|
|
});
|
|
} finally {
|
|
setGalleryLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleEventClick = (event) => {
|
|
setSelectedEvent(event);
|
|
navigate(`/members/gallery/${event.id}`);
|
|
fetchEventGallery(event.id);
|
|
};
|
|
|
|
const handleBackToEvents = () => {
|
|
setSelectedEvent(null);
|
|
setGalleryImages([]);
|
|
navigate('/members/gallery');
|
|
};
|
|
|
|
const openLightbox = (index) => {
|
|
setSelectedImageIndex(index);
|
|
};
|
|
|
|
const closeLightbox = () => {
|
|
setSelectedImageIndex(null);
|
|
};
|
|
|
|
const nextImage = () => {
|
|
if (selectedImageIndex !== null) {
|
|
setSelectedImageIndex((selectedImageIndex + 1) % galleryImages.length);
|
|
}
|
|
};
|
|
|
|
const previousImage = () => {
|
|
if (selectedImageIndex !== null) {
|
|
setSelectedImageIndex((selectedImageIndex - 1 + galleryImages.length) % galleryImages.length);
|
|
}
|
|
};
|
|
|
|
const EventCard = ({ event }) => (
|
|
<Card
|
|
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-lg hover:-translate-y-1 transition-all cursor-pointer h-full"
|
|
onClick={() => handleEventClick(event)}
|
|
>
|
|
{/* Thumbnail */}
|
|
<div className="relative h-48 mb-4 rounded-xl overflow-hidden bg-[var(--lavender-500)]">
|
|
{event.thumbnail_url ? (
|
|
<img
|
|
src={event.thumbnail_url}
|
|
alt={event.title}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
<ImageIcon className="h-16 w-16 text-[var(--neutral-800)]" />
|
|
</div>
|
|
)}
|
|
<div className="absolute top-3 right-3">
|
|
<Badge className="bg-brand-purple text-white px-3 py-1 rounded-full">
|
|
{event.gallery_count} {event.gallery_count === 1 ? 'photo' : 'photos'}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Event Info */}
|
|
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
{event.title}
|
|
</h3>
|
|
|
|
{event.description && (
|
|
<p className="text-brand-purple mb-3 line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{event.description}
|
|
</p>
|
|
)}
|
|
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex items-center gap-2 text-brand-purple ">
|
|
<Calendar className="h-4 w-4" />
|
|
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{moment(event.start_at).format('MMMM D, YYYY')}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-brand-purple ">
|
|
<MapPin className="h-4 w-4" />
|
|
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</span>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
);
|
|
|
|
// Event Gallery Grid View
|
|
if (!selectedEvent) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-b from-[var(--neutral-100)] to-[var(--neutral-800)]">
|
|
<Navbar />
|
|
|
|
<div className="max-w-7xl mx-auto px-6 py-12">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
Event Gallery
|
|
</h1>
|
|
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
Browse photos from past LOAF events.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Events Grid */}
|
|
{loading ? (
|
|
<div className="text-center py-20">
|
|
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading galleries...</p>
|
|
</div>
|
|
) : events.length > 0 ? (
|
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
{events.map((event) => (
|
|
<EventCard key={event.id} event={event} />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-20">
|
|
<ImageIcon className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
|
|
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
No Event Galleries Yet
|
|
</h3>
|
|
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
Event photos will appear here once admins upload them.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Individual Event Gallery View
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
<Navbar />
|
|
|
|
<div className="max-w-7xl mx-auto px-6 py-12">
|
|
{/* Back Button */}
|
|
<Button
|
|
onClick={handleBackToEvents}
|
|
variant="ghost"
|
|
className="mb-6 text-brand-purple hover:text-[var(--purple-ink)] hover:bg-[var(--lavender-500)]"
|
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
|
>
|
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
Back to All Galleries
|
|
</Button>
|
|
|
|
{/* Event Header */}
|
|
<div className="mb-8">
|
|
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
{selectedEvent.title}
|
|
</h1>
|
|
<div className="flex flex-wrap gap-4 text-brand-purple ">
|
|
<div className="flex items-center gap-2">
|
|
<Calendar className="h-5 w-5" />
|
|
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{moment(selectedEvent.start_at).format('MMMM D, YYYY')}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<MapPin className="h-5 w-5" />
|
|
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{selectedEvent.location}</span>
|
|
</div>
|
|
<Badge className="bg-brand-purple text-white px-3 py-1 rounded-full">
|
|
{selectedEvent.gallery_count} {selectedEvent.gallery_count === 1 ? 'photo' : 'photos'}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Gallery Grid */}
|
|
{galleryLoading ? (
|
|
<div className="text-center py-20">
|
|
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading images...</p>
|
|
</div>
|
|
) : galleryImages.length > 0 ? (
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
{galleryImages.map((image, index) => (
|
|
<div
|
|
key={image.id}
|
|
className="relative aspect-square rounded-xl overflow-hidden cursor-pointer group"
|
|
onClick={() => openLightbox(index)}
|
|
>
|
|
<img
|
|
src={image.image_url}
|
|
alt={image.caption || `Gallery image ${index + 1}`}
|
|
className="w-full h-full object-cover transition-transform group-hover:scale-110"
|
|
/>
|
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center">
|
|
<ImageIcon className="h-8 w-8 text-white opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
</div>
|
|
{image.caption && (
|
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<p className="text-white text-sm line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{image.caption}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-20">
|
|
<ImageIcon className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
|
|
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
No Photos Yet
|
|
</h3>
|
|
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
Photos from this event will appear here once uploaded.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Lightbox Modal */}
|
|
<Dialog open={selectedImageIndex !== null} onOpenChange={closeLightbox}>
|
|
<DialogContent className="max-w-7xl w-full h-[90vh] p-0 bg-black border-0 overflow-y-auto max-h-[90vh]">
|
|
{selectedImageIndex !== null && galleryImages[selectedImageIndex] && (
|
|
<div className="relative w-full h-full flex items-center justify-center">
|
|
{/* Close Button */}
|
|
<Button
|
|
onClick={closeLightbox}
|
|
variant="ghost"
|
|
className="absolute top-4 right-4 z-50 bg-black/50 hover:bg-black/70 text-white rounded-full p-2"
|
|
>
|
|
<X className="h-6 w-6" />
|
|
</Button>
|
|
|
|
{/* Previous Button */}
|
|
{galleryImages.length > 1 && (
|
|
<Button
|
|
onClick={previousImage}
|
|
variant="ghost"
|
|
className="absolute left-4 z-50 bg-black/50 hover:bg-black/70 text-white rounded-full p-3"
|
|
>
|
|
<ChevronLeft className="h-8 w-8" />
|
|
</Button>
|
|
)}
|
|
|
|
{/* Image */}
|
|
<div className="w-full h-full flex flex-col items-center justify-center p-8">
|
|
<img
|
|
src={galleryImages[selectedImageIndex].image_url}
|
|
alt={galleryImages[selectedImageIndex].caption || `Image ${selectedImageIndex + 1}`}
|
|
className="max-w-full max-h-full object-contain"
|
|
/>
|
|
{galleryImages[selectedImageIndex].caption && (
|
|
<div className="mt-4 max-w-2xl">
|
|
<p className="text-white text-center text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{galleryImages[selectedImageIndex].caption}
|
|
</p>
|
|
</div>
|
|
)}
|
|
<div className="mt-2 text-white/70 text-sm">
|
|
{selectedImageIndex + 1} / {galleryImages.length}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Next Button */}
|
|
{galleryImages.length > 1 && (
|
|
<Button
|
|
onClick={nextImage}
|
|
variant="ghost"
|
|
className="absolute right-4 z-50 bg-black/50 hover:bg-black/70 text-white rounded-full p-3"
|
|
>
|
|
<ChevronRight className="h-8 w-8" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
<MemberFooter />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default EventGallery;
|