Update New Features

This commit is contained in:
Koncept Kit
2025-12-10 17:52:47 +07:00
parent 1f27c3224b
commit 36017e8693
39 changed files with 4977 additions and 196 deletions

View 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;