Update New Features
This commit is contained in:
349
src/pages/members/EventGallery.js
Normal file
349
src/pages/members/EventGallery.js
Normal file
@@ -0,0 +1,349 @@
|
||||
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-white rounded-2xl border border-[#ddd8eb] 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-[#F8F7FB]">
|
||||
{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-[#ddd8eb]" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute top-3 right-3">
|
||||
<Badge className="bg-[#664fa3] 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-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{event.title}
|
||||
</h3>
|
||||
|
||||
{event.description && (
|
||||
<p className="text-[#664fa3] 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-[#664fa3]">
|
||||
<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-[#664fa3]">
|
||||
<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-white">
|
||||
<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-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Event Gallery
|
||||
</h1>
|
||||
<p className="text-lg text-[#664fa3]" 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-[#664fa3]" 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-[#ddd8eb] mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Event Galleries Yet
|
||||
</h3>
|
||||
<p className="text-[#664fa3]" 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-white">
|
||||
<Navbar />
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
||||
{/* Back Button */}
|
||||
<Button
|
||||
onClick={handleBackToEvents}
|
||||
variant="ghost"
|
||||
className="mb-6 text-[#664fa3] hover:text-[#422268] hover:bg-[#F8F7FB]"
|
||||
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-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{selectedEvent.title}
|
||||
</h1>
|
||||
<div className="flex flex-wrap gap-4 text-[#664fa3]">
|
||||
<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-[#664fa3] 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-[#664fa3]" 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-[#ddd8eb] mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Photos Yet
|
||||
</h3>
|
||||
<p className="text-[#664fa3]" 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">
|
||||
{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;
|
||||
Reference in New Issue
Block a user