Files
membership-fe/src/pages/members/MemberCalendar.js

337 lines
13 KiB
JavaScript

import React, { useState, useEffect, useMemo } from 'react';
import { Calendar, momentLocalizer } from 'react-big-calendar';
import moment from 'moment';
import 'react-big-calendar/lib/css/react-big-calendar.css';
import './MemberCalendar.css';
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 { Badge } from '../../components/ui/badge';
import AddToCalendarButton from '../../components/AddToCalendarButton';
import { toast } from 'sonner';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '../../components/ui/dialog';
import { Calendar as CalendarIcon, MapPin, Users, Clock, X, Check, HelpCircle } from 'lucide-react';
const localizer = momentLocalizer(moment);
export default function MemberCalendar() {
const [events, setEvents] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedEvent, setSelectedEvent] = useState(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [rsvpLoading, setRsvpLoading] = useState(false);
const [currentDate, setCurrentDate] = useState(new Date());
const [currentView, setCurrentView] = useState('month');
useEffect(() => {
fetchEvents();
}, []);
const fetchEvents = async () => {
try {
const response = await api.get('/events');
setEvents(response.data);
} catch (error) {
toast.error('Failed to load events');
} finally {
setLoading(false);
}
};
// Transform events for react-big-calendar
const calendarEvents = useMemo(() => {
return events.map(event => ({
id: event.id,
title: event.title,
start: new Date(event.start_at),
end: new Date(event.end_at),
resource: event,
}));
}, [events]);
const handleSelectEvent = (event) => {
setSelectedEvent(event.resource);
setIsDialogOpen(true);
};
const handleNavigate = (newDate) => {
setCurrentDate(newDate);
};
const handleViewChange = (newView) => {
setCurrentView(newView);
};
const handleRSVP = async (status) => {
if (!selectedEvent) return;
setRsvpLoading(true);
try {
await api.post(`/events/${selectedEvent.id}/rsvp`, { rsvp_status: status });
toast.success(`RSVP updated to: ${status}`);
await fetchEvents();
const updatedEvent = events.find(e => e.id === selectedEvent.id);
if (updatedEvent) {
setSelectedEvent({ ...updatedEvent, user_rsvp_status: status });
}
} catch (error) {
toast.error('Failed to update RSVP');
} finally {
setRsvpLoading(false);
}
};
const eventStyleGetter = (event) => {
const rsvpStatus = event.resource?.user_rsvp_status;
let backgroundColor = '#DDD8EB';
let borderColor = '#664fa3';
if (rsvpStatus === 'yes') {
backgroundColor = '#81B29A';
borderColor = '#66927e';
} else if (rsvpStatus === 'no') {
backgroundColor = '#9ca3af';
borderColor = '#6b7280';
} else if (rsvpStatus === 'maybe') {
backgroundColor = '#fb923c';
borderColor = '#ea580c';
}
return {
style: {
backgroundColor,
borderColor,
borderWidth: '2px',
borderStyle: 'solid',
borderRadius: '6px',
color: 'white',
fontWeight: '500',
fontSize: '0.875rem',
padding: '2px 6px',
}
};
};
if (loading) {
return (
<div className="min-h-screen bg-background">
<Navbar />
<div className="flex items-center justify-center min-h-[60vh]">
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Loading calendar...
</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-background">
<Navbar />
<div className="max-w-7xl mx-auto px-6 py-12">
<div className="mb-8">
<h1 className="text-4xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Event Calendar
</h1>
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
View and manage your event RSVPs. Click on any event to see details and update your RSVP.
</p>
<div className="flex gap-3 flex-wrap items-center">
<AddToCalendarButton
showSubscribe={true}
variant="default"
/>
<div className="flex gap-4 ml-auto">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-[var(--green-light)]"></div>
<span className="text-sm text-brand-purple ">Going</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-[var(--orange-400)]"></div>
<span className="text-sm text-brand-purple ">Maybe</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-[var(--slate-400)]"></div>
<span className="text-sm text-brand-purple ">Not Going</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-[var(--neutral-800)]"></div>
<span className="text-sm text-brand-purple ">No RSVP</span>
</div>
</div>
</div>
</div>
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
<Calendar
localizer={localizer}
events={calendarEvents}
startAccessor="start"
endAccessor="end"
style={{ height: 700 }}
date={currentDate}
view={currentView}
onNavigate={handleNavigate}
onView={handleViewChange}
onSelectEvent={handleSelectEvent}
eventPropGetter={eventStyleGetter}
views={['month', 'week', 'day', 'agenda']}
popup
className="member-calendar"
/>
</Card>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
{selectedEvent && (
<>
<DialogHeader>
<div className="flex items-center gap-3 mb-2">
<div className="bg-[var(--neutral-800)]/20 p-3 rounded-lg">
<CalendarIcon className="h-6 w-6 text-brand-purple " />
</div>
{selectedEvent.user_rsvp_status && (
<Badge
className={`px-3 py-1 rounded-full text-sm ${selectedEvent.user_rsvp_status === 'yes'
? 'bg-[var(--green-light)] text-white'
: selectedEvent.user_rsvp_status === 'no'
? 'bg-gray-400 text-white'
: 'bg-orange-400 text-white'
}`}
>
{selectedEvent.user_rsvp_status === 'yes' && 'Going'}
{selectedEvent.user_rsvp_status === 'no' && 'Not Going'}
{selectedEvent.user_rsvp_status === 'maybe' && 'Maybe'}
</Badge>
)}
</div>
<DialogTitle className="text-2xl text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{selectedEvent.title}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 mt-4">
<div className="space-y-3">
<div className="flex items-center gap-3 text-brand-purple ">
<CalendarIcon className="h-5 w-5" />
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{new Date(selectedEvent.start_at).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</span>
</div>
<div className="flex items-center gap-3 text-brand-purple ">
<Clock className="h-5 w-5" />
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{new Date(selectedEvent.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - {new Date(selectedEvent.end_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
<div className="flex items-center gap-3 text-brand-purple ">
<MapPin className="h-5 w-5" />
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{selectedEvent.location}</span>
</div>
<div className="flex items-center gap-3 text-brand-purple ">
<Users className="h-5 w-5" />
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{selectedEvent.rsvp_count || 0} {selectedEvent.rsvp_count === 1 ? 'person' : 'people'} attending
{selectedEvent.capacity && ` (Capacity: ${selectedEvent.capacity})`}
</span>
</div>
</div>
{selectedEvent.description && (
<div className="pt-4 border-t border-[var(--neutral-800)]">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
About This Event
</h3>
<p className="text-brand-purple leading-relaxed whitespace-pre-line" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{selectedEvent.description}
</p>
</div>
)}
<div className="pt-4 border-t border-[var(--neutral-800)]">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Your RSVP
</h3>
<div className="flex gap-3 flex-wrap">
<Button
onClick={() => handleRSVP('yes')}
disabled={rsvpLoading}
size="sm"
className={`rounded-full px-6 flex items-center gap-2 ${selectedEvent.user_rsvp_status === 'yes'
? 'bg-[var(--green-light)] text-white hover:bg-[var(--green-muted)]'
: 'bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-[var(--neutral-400:)]'
}`}
>
<Check className="h-4 w-4" />
I'm Going
</Button>
<Button
onClick={() => handleRSVP('maybe')}
disabled={rsvpLoading}
size="sm"
variant="outline"
className={`rounded-full px-6 flex items-center gap-2 border-2 ${selectedEvent.user_rsvp_status === 'maybe'
? 'border-orange-400 bg-orange-100 text-orange-700'
: 'border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)]'
}`}
>
<HelpCircle className="h-4 w-4" />
Maybe
</Button>
<Button
onClick={() => handleRSVP('no')}
disabled={rsvpLoading}
size="sm"
variant="outline"
className={`rounded-full px-6 flex items-center gap-2 border-2 ${selectedEvent.user_rsvp_status === 'no'
? 'border-gray-400 bg-gray-100 text-gray-700'
: 'border-gray-400 text-gray-600 hover:bg-gray-50'
}`}
>
<X className="h-4 w-4" />
Can't Attend
</Button>
</div>
</div>
<div className="pt-4 border-t border-[var(--neutral-800)]">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Add to Your Calendar
</h3>
<AddToCalendarButton
event={selectedEvent}
showSubscribe={false}
variant="outline"
size="sm"
/>
</div>
</div>
</>
)}
</DialogContent>
</Dialog>
</div>
<MemberFooter />
</div>
);
}