Basic event management

This commit is contained in:
James Pattinson
2025-11-12 18:08:11 +00:00
parent e5fdd0ecb8
commit 107c208746
9 changed files with 791 additions and 79 deletions

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { authService, userService, membershipService, paymentService, User, Membership, Payment } from '../services/membershipService';
import { authService, userService, membershipService, paymentService, eventService, User, Membership, Payment, Event, EventRSVP } from '../services/membershipService';
import MembershipSetup from '../components/MembershipSetup';
import ProfileMenu from '../components/ProfileMenu';
import ProfileEdit from '../components/ProfileEdit';
@@ -21,6 +21,11 @@ const Dashboard: React.FC = () => {
const [showUserDetails, setShowUserDetails] = useState(false);
const [isEditingUser, setIsEditingUser] = useState(false);
const [editFormData, setEditFormData] = useState<Partial<User>>({});
const [upcomingEvents, setUpcomingEvents] = useState<Event[]>([]);
const [allEvents, setAllEvents] = useState<Event[]>([]);
const [eventRSVPs, setEventRSVPs] = useState<EventRSVP[]>([]);
const [eventRSVPCounts, setEventRSVPCounts] = useState<{[eventId: number]: {attending: number, maybe: number, not_attending: number}}>({});
const [rsvpLoading, setRsvpLoading] = useState<{[eventId: number]: boolean}>({});
useEffect(() => {
if (!authService.isAuthenticated()) {
@@ -31,6 +36,16 @@ const Dashboard: React.FC = () => {
loadData();
}, []);
const mergeRSVPStatus = (events: Event[], rsvps: EventRSVP[]): Event[] => {
return events.map(event => {
const rsvp = rsvps.find(r => r.event_id === event.id);
return {
...event,
rsvp_status: rsvp ? rsvp.status : undefined
};
});
};
const loadData = async () => {
try {
const [userData, membershipData, paymentData] = await Promise.all([
@@ -43,16 +58,31 @@ const Dashboard: React.FC = () => {
setMemberships(membershipData);
setPayments(paymentData);
// Load upcoming events and user's RSVPs
const [eventsData, rsvpsData] = await Promise.all([
eventService.getUpcomingEvents(),
eventService.getMyRSVPs()
]);
// Merge RSVP status with events
const eventsWithRSVP = mergeRSVPStatus(eventsData, rsvpsData);
setUpcomingEvents(eventsWithRSVP);
// Load admin data if user is admin
if (userData.role === 'admin' || userData.role === 'super_admin') {
const [allPaymentsData, allMembershipsData, allUsersData] = await Promise.all([
const [allPaymentsData, allMembershipsData, allUsersData, allEventsData] = await Promise.all([
paymentService.getAllPayments(),
membershipService.getAllMemberships(),
userService.getAllUsers()
userService.getAllUsers(),
eventService.getAllEvents()
]);
setAllPayments(allPaymentsData);
setAllMemberships(allMembershipsData);
setAllUsers(allUsersData);
setAllEvents(allEventsData);
// Load RSVP counts for all events
await loadEventRSVPCounts(allEventsData);
}
} catch (error) {
console.error('Failed to load data:', error);
@@ -61,6 +91,26 @@ const Dashboard: React.FC = () => {
}
};
const loadEventRSVPCounts = async (events: Event[]) => {
const counts: {[eventId: number]: {attending: number, maybe: number, not_attending: number}} = {};
for (const event of events) {
try {
const rsvps = await eventService.getEventRSVPs(event.id);
counts[event.id] = {
attending: rsvps.filter(r => r.status === 'attending').length,
maybe: rsvps.filter(r => r.status === 'maybe').length,
not_attending: rsvps.filter(r => r.status === 'not_attending').length
};
} catch (error) {
console.error(`Failed to load RSVPs for event ${event.id}:`, error);
counts[event.id] = { attending: 0, maybe: 0, not_attending: 0 };
}
}
setEventRSVPCounts(counts);
};
const handleMembershipSetup = () => {
setShowMembershipSetup(true);
};
@@ -198,11 +248,73 @@ const Dashboard: React.FC = () => {
}
};
const handleFormChange = (field: keyof User, value: string) => {
setEditFormData(prev => ({
...prev,
[field]: value
}));
const handleRSVP = async (eventId: number, status: 'attending' | 'not_attending' | 'maybe') => {
// Set loading state for this event
setRsvpLoading(prev => ({ ...prev, [eventId]: true }));
// Optimistically update the UI
setUpcomingEvents(prevEvents =>
prevEvents.map(event =>
event.id === eventId
? { ...event, rsvp_status: status }
: event
)
);
try {
await eventService.createOrUpdateRSVP(eventId, { status });
// Reload RSVPs and merge with events to get the latest data
const [eventsData, rsvpsData] = await Promise.all([
eventService.getUpcomingEvents(),
eventService.getMyRSVPs()
]);
const eventsWithRSVP = mergeRSVPStatus(eventsData, rsvpsData);
setUpcomingEvents(eventsWithRSVP);
} catch (error) {
console.error('Failed to update RSVP:', error);
alert('Failed to update RSVP. Please try again.');
// Revert optimistic update on error
const [eventsData, rsvpsData] = await Promise.all([
eventService.getUpcomingEvents(),
eventService.getMyRSVPs()
]);
const eventsWithRSVP = mergeRSVPStatus(eventsData, rsvpsData);
setUpcomingEvents(eventsWithRSVP);
} finally {
// Clear loading state
setRsvpLoading(prev => ({ ...prev, [eventId]: false }));
}
};
const handlePublishEvent = async (eventId: number) => {
try {
await eventService.updateEvent(eventId, { status: 'published' });
// Reload events to reflect the change
const eventsData = await eventService.getAllEvents();
setAllEvents(eventsData);
// Reload RSVP counts
await loadEventRSVPCounts(eventsData);
} catch (error) {
console.error('Failed to publish event:', error);
alert('Failed to publish event. Please try again.');
}
};
const handleCancelEvent = async (eventId: number) => {
if (!confirm('Are you sure you want to cancel this event?')) {
return;
}
try {
await eventService.updateEvent(eventId, { status: 'cancelled' });
// Reload events to reflect the change
const eventsData = await eventService.getAllEvents();
setAllEvents(eventsData);
// Reload RSVP counts
await loadEventRSVPCounts(eventsData);
} catch (error) {
console.error('Failed to cancel event:', error);
alert('Failed to cancel event. Please try again.');
}
};
const formatDate = (dateString: string) => {
@@ -271,10 +383,11 @@ const Dashboard: React.FC = () => {
<div className="card">
<h3 style={{ marginBottom: '16px' }}>Your Membership</h3>
<h4 style={{ color: '#0066cc', marginBottom: '8px' }}>{activeMembership.tier.name}</h4>
<p><strong>Membership Number:</strong> {activeMembership.id}</p>
<p><strong>Status:</strong> <span className={`status-badge ${getStatusClass(activeMembership.status)}`}>{activeMembership.status.toUpperCase()}</span></p>
<p><strong>Annual Fee:</strong> £{activeMembership.tier.annual_fee.toFixed(2)}</p>
<p><strong>Member since:</strong> {formatDate(activeMembership.start_date)}</p>
<p><strong>Renewal Date:</strong> {formatDate(activeMembership.end_date)}</p>
<p><strong>Valid From:</strong> {formatDate(activeMembership.start_date)}</p>
<p><strong>Valid Until:</strong> {formatDate(activeMembership.end_date)}</p>
<p><strong>Auto Renew:</strong> {activeMembership.auto_renew ? 'Yes' : 'No'}</p>
<div style={{ marginTop: '12px', padding: '12px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
<strong>Benefits:</strong>
@@ -332,6 +445,125 @@ const Dashboard: React.FC = () => {
)}
</div>
{/* Upcoming Events */}
<div className="card" style={{ marginTop: '20px' }}>
<h3 style={{ marginBottom: '16px' }}>Upcoming Events</h3>
{upcomingEvents.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{upcomingEvents.map(event => (
<div key={event.id} style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '16px',
backgroundColor: '#f9f9f9'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '12px' }}>
<div>
<h4 style={{ margin: '0 0 4px 0', color: '#0066cc' }}>{event.title}</h4>
<p style={{ margin: '0', fontSize: '14px', color: '#666' }}>
{formatDate(event.event_date)} at {event.event_time}
</p>
{event.location && (
<p style={{ margin: '4px 0 0 0', fontSize: '14px', color: '#666' }}>
📍 {event.location}
</p>
)}
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
className={`btn ${event.rsvp_status === 'attending' ? 'btn-success' : 'btn-outline-secondary'}`}
onClick={() => handleRSVP(event.id, 'attending')}
disabled={rsvpLoading[event.id]}
style={{
fontSize: '12px',
padding: '8px 16px',
opacity: rsvpLoading[event.id] ? 0.6 : event.rsvp_status ? (event.rsvp_status === 'attending' ? 1 : 0.4) : 1,
cursor: rsvpLoading[event.id] ? 'not-allowed' : 'pointer',
fontWeight: event.rsvp_status === 'attending' ? 'bold' : 'normal',
border: event.rsvp_status === 'attending' ? '3px solid #28a745' : '2px solid #adb5bd',
backgroundColor: event.rsvp_status === 'attending' ? '#28a745' : 'transparent',
color: event.rsvp_status === 'attending' ? 'white' : '#6c757d',
transform: event.rsvp_status === 'attending' ? 'scale(1.1)' : event.rsvp_status ? 'scale(0.95)' : 'scale(1)',
boxShadow: event.rsvp_status === 'attending' ? '0 4px 8px rgba(40, 167, 69, 0.3)' : 'none',
transition: 'all 0.3s ease',
filter: event.rsvp_status && event.rsvp_status !== 'attending' ? 'grayscale(50%)' : 'none'
}}
>
{rsvpLoading[event.id] ? '...' : 'Attending'}
</button>
<button
className={`btn ${event.rsvp_status === 'maybe' ? 'btn-warning' : 'btn-outline-secondary'}`}
onClick={() => handleRSVP(event.id, 'maybe')}
disabled={rsvpLoading[event.id]}
style={{
fontSize: '12px',
padding: '8px 16px',
opacity: rsvpLoading[event.id] ? 0.6 : event.rsvp_status ? (event.rsvp_status === 'maybe' ? 1 : 0.4) : 1,
cursor: rsvpLoading[event.id] ? 'not-allowed' : 'pointer',
fontWeight: event.rsvp_status === 'maybe' ? 'bold' : 'normal',
border: event.rsvp_status === 'maybe' ? '3px solid #ffc107' : '2px solid #adb5bd',
backgroundColor: event.rsvp_status === 'maybe' ? '#ffc107' : 'transparent',
color: event.rsvp_status === 'maybe' ? '#212529' : '#6c757d',
transform: event.rsvp_status === 'maybe' ? 'scale(1.1)' : event.rsvp_status ? 'scale(0.95)' : 'scale(1)',
boxShadow: event.rsvp_status === 'maybe' ? '0 4px 8px rgba(255, 193, 7, 0.3)' : 'none',
transition: 'all 0.3s ease',
filter: event.rsvp_status && event.rsvp_status !== 'maybe' ? 'grayscale(50%)' : 'none'
}}
>
{rsvpLoading[event.id] ? '...' : 'Maybe'}
</button>
<button
className={`btn ${event.rsvp_status === 'not_attending' ? 'btn-danger' : 'btn-outline-secondary'}`}
onClick={() => handleRSVP(event.id, 'not_attending')}
disabled={rsvpLoading[event.id]}
style={{
fontSize: '12px',
padding: '8px 16px',
opacity: rsvpLoading[event.id] ? 0.6 : event.rsvp_status ? (event.rsvp_status === 'not_attending' ? 1 : 0.4) : 1,
cursor: rsvpLoading[event.id] ? 'not-allowed' : 'pointer',
fontWeight: event.rsvp_status === 'not_attending' ? 'bold' : 'normal',
border: event.rsvp_status === 'not_attending' ? '3px solid #dc3545' : '2px solid #adb5bd',
backgroundColor: event.rsvp_status === 'not_attending' ? '#dc3545' : 'transparent',
color: event.rsvp_status === 'not_attending' ? 'white' : '#6c757d',
transform: event.rsvp_status === 'not_attending' ? 'scale(1.1)' : event.rsvp_status ? 'scale(0.95)' : 'scale(1)',
boxShadow: event.rsvp_status === 'not_attending' ? '0 4px 8px rgba(220, 53, 69, 0.3)' : 'none',
transition: 'all 0.3s ease',
filter: event.rsvp_status && event.rsvp_status !== 'not_attending' ? 'grayscale(50%)' : 'none'
}}
>
{rsvpLoading[event.id] ? '...' : 'Not Attending'}
</button>
</div>
</div>
{event.description && (
<p style={{ margin: '0', fontSize: '14px', lineHeight: '1.4' }}>
{event.description}
</p>
)}
{event.rsvp_status && (
<div style={{
marginTop: '12px',
padding: '8px 12px',
backgroundColor: event.rsvp_status === 'attending' ? '#d4edda' :
event.rsvp_status === 'maybe' ? '#fff3cd' : '#f8d7da',
border: `1px solid ${event.rsvp_status === 'attending' ? '#c3e6cb' :
event.rsvp_status === 'maybe' ? '#ffeaa7' : '#f5c6cb'}`,
borderRadius: '4px',
fontSize: '12px',
color: event.rsvp_status === 'attending' ? '#155724' :
event.rsvp_status === 'maybe' ? '#856404' : '#721c24'
}}>
<strong>Your RSVP:</strong> <span style={{ textTransform: 'capitalize' }}>{event.rsvp_status.replace('_', ' ')}</span>
</div>
)}
</div>
))}
</div>
) : (
<p style={{ color: '#666' }}>No upcoming events at this time.</p>
)}
</div>
{/* Admin Section */}
{(user?.role === 'admin' || user?.role === 'super_admin') && (
<div className="card" style={{ marginTop: '20px' }}>
@@ -449,6 +681,7 @@ const Dashboard: React.FC = () => {
<tr style={{ borderBottom: '2px solid #ddd' }}>
<th style={{ padding: '12px', textAlign: 'left' }}>Name</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Email</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Membership #</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Role</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Status</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Joined</th>
@@ -456,64 +689,68 @@ const Dashboard: React.FC = () => {
</tr>
</thead>
<tbody>
{filteredUsers.map(u => (
<tr
key={u.id}
style={{ borderBottom: '1px solid #eee', cursor: 'pointer' }}
onClick={() => handleUserClick(u)}
>
<td style={{ padding: '12px' }}>{u.first_name} {u.last_name}</td>
<td style={{ padding: '12px' }}>{u.email}</td>
<td style={{ padding: '12px' }}>
<span style={{
backgroundColor: u.role === 'super_admin' ? '#dc3545' :
u.role === 'admin' ? '#ffc107' : '#28a745',
color: u.role === 'member' ? 'white' : 'black',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 'bold'
}}>
{u.role.toUpperCase()}
</span>
</td>
<td style={{ padding: '12px' }}>
<span className={`status-badge ${u.is_active ? 'status-active' : 'status-expired'}`}>
{u.is_active ? 'ACTIVE' : 'INACTIVE'}
</span>
</td>
<td style={{ padding: '12px' }}>{formatDate(u.created_at)}</td>
<td style={{ padding: '12px' }}>
{u.role === 'member' && (
<button
className="btn btn-primary"
onClick={(e) => {
e.stopPropagation(); // Prevent row click
handleUpdateUserRole(u.id, 'admin');
}}
style={{ fontSize: '12px', padding: '4px 8px', marginRight: '4px' }}
>
Make Admin
</button>
)}
{u.role === 'admin' && u.id !== user?.id && (
<button
className="btn btn-secondary"
onClick={(e) => {
e.stopPropagation(); // Prevent row click
handleUpdateUserRole(u.id, 'member');
}}
style={{ fontSize: '12px', padding: '4px 8px' }}
>
Remove Admin
</button>
)}
{u.role === 'super_admin' && (
<span style={{ fontSize: '12px', color: '#666' }}>Super Admin</span>
)}
</td>
</tr>
))}
{filteredUsers.map(u => {
const userMembership = allMemberships.find(m => m.user_id === u.id && m.status === 'active');
return (
<tr
key={u.id}
style={{ borderBottom: '1px solid #eee', cursor: 'pointer' }}
onClick={() => handleUserClick(u)}
>
<td style={{ padding: '12px' }}>{u.first_name} {u.last_name}</td>
<td style={{ padding: '12px' }}>{u.email}</td>
<td style={{ padding: '12px' }}>{userMembership ? userMembership.id : 'N/A'}</td>
<td style={{ padding: '12px' }}>
<span style={{
backgroundColor: u.role === 'super_admin' ? '#dc3545' :
u.role === 'admin' ? '#ffc107' : '#28a745',
color: u.role === 'member' ? 'white' : 'black',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 'bold'
}}>
{u.role.toUpperCase()}
</span>
</td>
<td style={{ padding: '12px' }}>
<span className={`status-badge ${u.is_active ? 'status-active' : 'status-expired'}`}>
{u.is_active ? 'ACTIVE' : 'INACTIVE'}
</span>
</td>
<td style={{ padding: '12px' }}>{formatDate(u.created_at)}</td>
<td style={{ padding: '12px' }}>
{u.role === 'member' && (
<button
className="btn btn-primary"
onClick={(e) => {
e.stopPropagation(); // Prevent row click
handleUpdateUserRole(u.id, 'admin');
}}
style={{ fontSize: '12px', padding: '4px 8px', marginRight: '4px' }}
>
Make Admin
</button>
)}
{u.role === 'admin' && u.id !== user?.id && (
<button
className="btn btn-secondary"
onClick={(e) => {
e.stopPropagation(); // Prevent row click
handleUpdateUserRole(u.id, 'member');
}}
style={{ fontSize: '12px', padding: '4px 8px' }}
>
Remove Admin
</button>
)}
{u.role === 'super_admin' && (
<span style={{ fontSize: '12px', color: '#666' }}>Super Admin</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
@@ -529,6 +766,110 @@ const Dashboard: React.FC = () => {
/>
)}
{/* Event Management Section for Admins */}
{(user?.role === 'admin' || user?.role === 'super_admin') && (
<div className="card" style={{ marginTop: '20px' }}>
<h3 style={{ marginBottom: '16px' }}>Event Management</h3>
{/* Create New Event Button */}
<div style={{ marginBottom: '16px' }}>
<button
className="btn btn-primary"
onClick={() => {/* TODO: Implement create event modal */}}
style={{ fontSize: '14px', padding: '8px 16px' }}
>
Create New Event
</button>
</div>
{/* Events List */}
<div className="table-container">
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '2px solid #ddd' }}>
<th style={{ padding: '12px', textAlign: 'left' }}>Event</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Date & Time</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Location</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Status</th>
<th style={{ padding: '12px', textAlign: 'left' }}>RSVPs</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Actions</th>
</tr>
</thead>
<tbody>
{allEvents.map(event => (
<tr key={event.id} style={{ borderBottom: '1px solid #eee' }}>
<td style={{ padding: '12px' }}>
<div>
<strong>{event.title}</strong>
{event.description && (
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px', maxWidth: '300px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{event.description}
</div>
)}
</div>
</td>
<td style={{ padding: '12px' }}>
<div>{formatDate(event.event_date)}</div>
<div style={{ fontSize: '12px', color: '#666' }}>{event.event_time}</div>
</td>
<td style={{ padding: '12px' }}>{event.location || 'TBD'}</td>
<td style={{ padding: '12px' }}>
<span className={`status-badge ${event.status === 'published' ? 'status-active' : event.status === 'cancelled' ? 'status-expired' : 'status-pending'}`}>
{event.status.toUpperCase()}
</span>
</td>
<td style={{ padding: '12px' }}>
{eventRSVPCounts[event.id] ? (
<div style={{ fontSize: '12px' }}>
<div>Attending: {eventRSVPCounts[event.id].attending}</div>
<div>Maybe: {eventRSVPCounts[event.id].maybe}</div>
<div>Not: {eventRSVPCounts[event.id].not_attending}</div>
</div>
) : (
<span style={{ fontSize: '12px', color: '#666' }}>Loading...</span>
)}
</td>
<td style={{ padding: '12px' }}>
<div style={{ display: 'flex', gap: '4px' }}>
<button
className="btn btn-secondary"
onClick={() => {/* TODO: Implement edit event */}}
style={{ fontSize: '12px', padding: '4px 8px' }}
>
Edit
</button>
{event.status === 'draft' && (
<button
className="btn btn-primary"
onClick={() => handlePublishEvent(event.id)}
style={{ fontSize: '12px', padding: '4px 8px' }}
>
Publish
</button>
)}
{event.status === 'published' && (
<button
className="btn btn-secondary"
onClick={() => handleCancelEvent(event.id)}
style={{ fontSize: '12px', padding: '4px 8px' }}
>
Cancel
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{allEvents.length === 0 && (
<p style={{ color: '#666', textAlign: 'center', padding: '20px' }}>No events created yet.</p>
)}
</div>
)}
{/* User Details Modal */}
{showUserDetails && selectedUser && (
<div style={{

View File

@@ -14,6 +14,10 @@ export interface LoginData {
password: string;
}
export interface ForgotPasswordData {
email: string;
}
export interface User {
id: number;
email: string;
@@ -62,10 +66,6 @@ export interface Payment {
created_at: string;
}
export interface ForgotPasswordData {
email: string;
}
export interface ResetPasswordData {
token: string;
new_password: string;
@@ -83,6 +83,14 @@ export interface MembershipCreateData {
auto_renew: boolean;
}
export interface MembershipTierUpdateData {
name?: string;
description?: string;
annual_fee?: number;
benefits?: string;
is_active?: boolean;
}
export interface MembershipUpdateData {
status?: string;
start_date?: string;
@@ -111,12 +119,54 @@ export interface MembershipTierCreateData {
benefits?: string;
}
export interface MembershipTierUpdateData {
name?: string;
export interface Event {
id: number;
title: string;
description: string | null;
event_date: string;
event_time: string | null;
location: string | null;
max_attendees: number | null;
status: string;
created_by: number;
created_at: string;
updated_at: string;
rsvp_status?: string; // Current user's RSVP status
}
export interface EventCreateData {
title: string;
description?: string;
annual_fee?: number;
benefits?: string;
is_active?: boolean;
event_date: string;
event_time?: string;
location?: string;
max_attendees?: number;
}
export interface EventUpdateData {
title?: string;
description?: string;
event_date?: string;
event_time?: string;
location?: string;
max_attendees?: number;
status?: string;
}
export interface EventRSVP {
id: number;
event_id: number;
user_id: number;
status: string;
attended: boolean;
notes: string | null;
created_at: string;
updated_at: string;
}
export interface EventRSVPData {
status: string;
notes?: string;
}
export const authService = {
@@ -245,3 +295,45 @@ export const paymentService = {
return response.data;
}
};
export const eventService = {
async getAllEvents(): Promise<Event[]> {
const response = await api.get('/events/');
return response.data;
},
async getUpcomingEvents(): Promise<Event[]> {
const response = await api.get('/events/upcoming');
return response.data;
},
async createEvent(data: EventCreateData): Promise<Event> {
const response = await api.post('/events/', data);
return response.data;
},
async updateEvent(eventId: number, data: EventUpdateData): Promise<Event> {
const response = await api.put(`/events/${eventId}`, data);
return response.data;
},
async deleteEvent(eventId: number): Promise<{ message: string }> {
const response = await api.delete(`/events/${eventId}`);
return response.data;
},
async getEventRSVPs(eventId: number): Promise<EventRSVP[]> {
const response = await api.get(`/events/${eventId}/rsvps`);
return response.data;
},
async createOrUpdateRSVP(eventId: number, data: EventRSVPData): Promise<EventRSVP> {
const response = await api.post(`/events/${eventId}/rsvp`, data);
return response.data;
},
async getMyRSVPs(): Promise<EventRSVP[]> {
const response = await api.get('/events/my-rsvps');
return response.data;
}
};