feat: implement event creation and management interface (task 13)

- Create EventForm component with date/time picker and map-based location selection
- Implement EventList component with status badges and participant counts
- Add EventFilters component for filtering by status and date range
- Create EventDetailModal with registration functionality and event details
- Build eventService for API calls (getEvents, createEvent, register, etc.)
- Update EventsPage with full event management UI
- Support event creation, listing, filtering, and registration
- Requirements: 4.1, 4.2, 4.3, 4.4
This commit is contained in:
2025-11-16 20:06:09 +00:00
parent 64fe312f01
commit e165fea414
10 changed files with 1434 additions and 30 deletions

View File

@@ -1,15 +1,260 @@
/**
* Events page - placeholder for task 13-14
*/
import { useState, useEffect } from 'react';
import { useUser } from '@clerk/clerk-react';
import { EventForm, EventList, EventFilters, EventDetailModal } from '@/components/event';
import type { EventFilterState } from '@/components/event';
import { eventService } from '@/services/eventService';
import type { CleanupEvent } from '@/types';
export function EventsPage() {
const { isSignedIn } = useUser();
const [events, setEvents] = useState<CleanupEvent[]>([]);
const [filteredEvents, setFilteredEvents] = useState<CleanupEvent[]>([]);
const [filters, setFilters] = useState<EventFilterState>({
status: 'all',
});
const [showCreateForm, setShowCreateForm] = useState(false);
const [selectedEvent, setSelectedEvent] = useState<CleanupEvent | null>(null);
const [isRegistered, setIsRegistered] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
// Load events
useEffect(() => {
loadEvents();
}, []);
// Apply filters
useEffect(() => {
applyFilters();
}, [events, filters]);
// Check registration status when event is selected
useEffect(() => {
if (selectedEvent && isSignedIn) {
checkRegistrationStatus(selectedEvent.id);
}
}, [selectedEvent, isSignedIn]);
const loadEvents = async () => {
try {
setIsLoading(true);
setError(null);
const data = await eventService.getEvents();
setEvents(data);
} catch (err) {
setError('Failed to load events. Please try again.');
console.error('Error loading events:', err);
} finally {
setIsLoading(false);
}
};
const applyFilters = () => {
let filtered = [...events];
// Filter by status
if (filters.status !== 'all') {
filtered = filtered.filter(event => event.status === filters.status);
}
// Filter by date range
if (filters.startDate) {
const startDate = new Date(filters.startDate);
filtered = filtered.filter(event => new Date(event.scheduledDate) >= startDate);
}
if (filters.endDate) {
const endDate = new Date(filters.endDate);
endDate.setHours(23, 59, 59, 999);
filtered = filtered.filter(event => new Date(event.scheduledDate) <= endDate);
}
// Sort by date (upcoming first, then by date)
filtered.sort((a, b) => {
const dateA = new Date(a.scheduledDate).getTime();
const dateB = new Date(b.scheduledDate).getTime();
return dateA - dateB;
});
setFilteredEvents(filtered);
};
const checkRegistrationStatus = async (eventId: string) => {
try {
const registered = await eventService.isUserRegistered(eventId);
setIsRegistered(registered);
} catch (err) {
console.error('Error checking registration status:', err);
setIsRegistered(false);
}
};
const handleCreateEvent = async (formData: any) => {
if (!isSignedIn) {
setError('You must be signed in to create an event');
return;
}
try {
setIsSubmitting(true);
setError(null);
// Combine date and time
const scheduledDateTime = new Date(`${formData.scheduledDate}T${formData.scheduledTime}`);
const eventData = {
title: formData.title,
description: formData.description,
latitude: formData.latitude,
longitude: formData.longitude,
locationName: formData.locationName,
scheduledDate: scheduledDateTime,
duration: formData.duration,
};
await eventService.createEvent(eventData);
setShowCreateForm(false);
await loadEvents();
} catch (err: any) {
setError(err.message || 'Failed to create event. Please try again.');
console.error('Error creating event:', err);
} finally {
setIsSubmitting(false);
}
};
const handleRegister = async (eventId: string) => {
if (!isSignedIn) {
setError('You must be signed in to register for events');
return;
}
try {
setIsSubmitting(true);
setError(null);
await eventService.registerForEvent(eventId);
setIsRegistered(true);
await loadEvents();
} catch (err: any) {
setError(err.message || 'Failed to register for event. Please try again.');
console.error('Error registering for event:', err);
} finally {
setIsSubmitting(false);
}
};
const handleUnregister = async () => {
if (!selectedEvent) return;
try {
setIsSubmitting(true);
setError(null);
await eventService.unregisterFromEvent(selectedEvent.id);
setIsRegistered(false);
await loadEvents();
} catch (err: any) {
setError(err.message || 'Failed to unregister from event. Please try again.');
console.error('Error unregistering from event:', err);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Clean-up Events</h2>
<div className="bg-white rounded-lg shadow p-8">
<p className="text-gray-500">
Events management will be implemented in tasks 13-14
</p>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Clean-up Events</h1>
<p className="mt-2 text-sm text-gray-600">
Join community cleanup events or organize your own
</p>
</div>
{isSignedIn && !showCreateForm && (
<button
onClick={() => setShowCreateForm(true)}
className="flex items-center gap-2 bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors font-medium"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Create Event
</button>
)}
</div>
{/* Error Message */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<div className="flex items-start">
<svg className="h-5 w-5 text-red-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
<div className="ml-3">
<p className="text-sm text-red-700">{error}</p>
</div>
<button
onClick={() => setError(null)}
className="ml-auto text-red-400 hover:text-red-500"
>
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
</div>
)}
{/* Create Event Form */}
{showCreateForm && (
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<h2 className="text-xl font-semibold text-gray-900 mb-6">Create New Event</h2>
<EventForm
onSubmit={handleCreateEvent}
onCancel={() => setShowCreateForm(false)}
isSubmitting={isSubmitting}
/>
</div>
)}
{/* Filters */}
{!showCreateForm && (
<EventFilters
filters={filters}
onFiltersChange={setFilters}
/>
)}
{/* Event List */}
{!showCreateForm && (
<EventList
events={filteredEvents}
onEventClick={setSelectedEvent}
onRegister={handleRegister}
isLoading={isLoading}
/>
)}
{/* Event Detail Modal */}
{selectedEvent && (
<EventDetailModal
event={selectedEvent}
isRegistered={isRegistered}
onClose={() => setSelectedEvent(null)}
onRegister={() => handleRegister(selectedEvent.id)}
onUnregister={handleUnregister}
isLoading={isSubmitting}
/>
)}
</div>
);
}