From 91ecbd2443934a3e45fc9d1290373735ac7b0ece Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Sun, 16 Nov 2025 21:05:23 +0000 Subject: [PATCH] Link map items with events - Display cleanup events on interactive map - Added event fetching in MapPage component - Events now display on map with custom markers (calendar emoji) - Enhanced event popup with detailed information (date, time, participants, litter collected) - Added event visibility toggle in MapFilters - Events use different colors based on status (upcoming=amber, completed=green, cancelled=gray) - Sample event data provided as fallback - Fixed date filter type conversion for API calls --- src/components/map/MapFilters.tsx | 28 ++++++++ src/components/map/MapView.tsx | 52 ++++++++++++-- src/pages/MapPage.tsx | 108 ++++++++++++++++++++++++++++-- 3 files changed, 179 insertions(+), 9 deletions(-) diff --git a/src/components/map/MapFilters.tsx b/src/components/map/MapFilters.tsx index 2c28a72..fb06744 100644 --- a/src/components/map/MapFilters.tsx +++ b/src/components/map/MapFilters.tsx @@ -11,6 +11,8 @@ interface MapFiltersProps { onToggleHotspots?: () => void; showHotspots?: boolean; isLoadingHotspots?: boolean; + onToggleEvents?: () => void; + showEvents?: boolean; } export function MapFilters({ @@ -23,6 +25,8 @@ export function MapFilters({ onToggleHotspots, showHotspots = false, isLoadingHotspots = false, + onToggleEvents, + showEvents = true, }: MapFiltersProps) { const [isExpanded, setIsExpanded] = useState(true); @@ -159,6 +163,30 @@ export function MapFilters({ + {/* Events Toggle */} + {onToggleEvents && ( +
+ + +
+ )} + {/* Hotspots Toggle */} {onToggleHotspots && (
diff --git a/src/components/map/MapView.tsx b/src/components/map/MapView.tsx index ffae70f..812c19d 100644 --- a/src/components/map/MapView.tsx +++ b/src/components/map/MapView.tsx @@ -229,12 +229,54 @@ export function MapView({ const icon = createEventMarkerIcon(event.status); const marker = L.marker([event.latitude, event.longitude], { icon }); + const scheduledDate = new Date(event.scheduledDate).toLocaleDateString('en-IE', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + marker.bindPopup(` -
-

${event.title}

-

${event.locationName}

-

Participants: ${event.participantCount}

-

Status: ${event.status}

+
+

${event.title}

+
+

+ 📍 + ${event.locationName} +

+

+ 📅 + ${scheduledDate} +

+

+ ⏱️ + ${event.duration} minutes +

+

+ 👥 + ${event.participantCount} participant${event.participantCount !== 1 ? 's' : ''} +

+ ${event.status === 'completed' && event.litterCollected ? ` +

+ ♻️ + ${event.litterCollected} kg collected +

+ ` : ''} +

+ + ${event.status.charAt(0).toUpperCase() + event.status.slice(1)} + +

+
+ ${event.description ? ` +

${event.description}

+ ` : ''}
`); diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index f2c0063..158a763 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -2,7 +2,8 @@ import { useState, useEffect } from 'react'; import { MapView, MapFilters } from '@/components/map'; import { analyticsService } from '@/services/analyticsService'; import { reportService } from '@/services/reportService'; -import type { Report, FilterState, HotspotWithSeverity } from '@/types'; +import { eventService } from '@/services/eventService'; +import type { Report, FilterState, HotspotWithSeverity, CleanupEvent } from '@/types'; /** * Map page - Interactive map displaying litter reports and cleanup events @@ -13,10 +14,13 @@ export function MapPage() { const [showClusters, setShowClusters] = useState(true); const [showHeatmap, setShowHeatmap] = useState(false); const [showHotspots, setShowHotspots] = useState(false); + const [showEvents, setShowEvents] = useState(true); const [hotspots, setHotspots] = useState([]); const [isLoadingHotspots, setIsLoadingHotspots] = useState(false); const [reports, setReports] = useState([]); const [isLoadingReports, setIsLoadingReports] = useState(true); + const [events, setEvents] = useState([]); + const [isLoadingEvents, setIsLoadingEvents] = useState(true); const [modalImageUrl, setModalImageUrl] = useState(null); // Fetch reports from API @@ -24,7 +28,25 @@ export function MapPage() { const fetchReports = async () => { try { setIsLoadingReports(true); - const data = await reportService.getReports(filters); + + // Convert FilterState to API format (Date objects to ISO strings) + const apiFilters: Parameters[0] = filters.dateRange + ? { + dateRange: { + start: filters.dateRange.start.toISOString(), + end: filters.dateRange.end.toISOString(), + }, + litterTypes: filters.litterTypes, + quantities: filters.quantities, + verificationStatus: filters.verificationStatus, + } + : { + litterTypes: filters.litterTypes, + quantities: filters.quantities, + verificationStatus: filters.verificationStatus, + }; + + const data = await reportService.getReports(apiFilters); setReports(data); } catch (error) { console.error('Failed to fetch reports:', error); @@ -38,6 +60,26 @@ export function MapPage() { fetchReports(); }, [filters]); + // Fetch events from API + useEffect(() => { + const fetchEvents = async () => { + try { + setIsLoadingEvents(true); + // Fetch upcoming and completed events to show on map + const data = await eventService.getEvents(); + setEvents(data); + } catch (error) { + console.error('Failed to fetch events:', error); + // Fall back to sample data on error + setEvents(sampleEvents); + } finally { + setIsLoadingEvents(false); + } + }; + + fetchEvents(); + }, []); + // Set up global function for opening image modal useEffect(() => { (window as any).openImageModal = (url: string) => { @@ -49,6 +91,55 @@ export function MapPage() { }, []); // Sample data for fallback + const sampleEvents: CleanupEvent[] = [ + { + id: 'event1', + organizerId: 'user1', + title: 'Sandymount Beach Cleanup', + description: 'Join us for a community cleanup at Sandymount Beach', + latitude: 53.3333, + longitude: -6.2167, + locationName: 'Sandymount Beach, Dublin', + scheduledDate: new Date('2024-02-15T10:00:00'), + duration: 120, + status: 'upcoming', + participantCount: 15, + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-10'), + }, + { + id: 'event2', + organizerId: 'user2', + title: 'Galway Bay Cleanup', + description: 'Monthly cleanup event at Salthill Beach', + latitude: 53.2577, + longitude: -9.0810, + locationName: 'Salthill Beach, Galway', + scheduledDate: new Date('2024-02-20T14:00:00'), + duration: 90, + status: 'upcoming', + participantCount: 8, + createdAt: new Date('2024-01-12'), + updatedAt: new Date('2024-01-12'), + }, + { + id: 'event3', + organizerId: 'user3', + title: 'Cork Harbor Cleanup - Completed', + description: 'Successful cleanup event with great turnout', + latitude: 51.8503, + longitude: -8.2944, + locationName: 'Cork Harbor', + scheduledDate: new Date('2024-01-10T11:00:00'), + duration: 150, + status: 'completed', + participantCount: 25, + litterCollected: 45.5, + createdAt: new Date('2024-01-05'), + updatedAt: new Date('2024-01-10'), + }, + ]; + const sampleReports: Report[] = [ { id: '1', @@ -165,6 +256,10 @@ export function MapPage() { setShowHotspots(!showHotspots); }; + const handleToggleEvents = () => { + setShowEvents(!showEvents); + }; + // Fetch hotspots when hotspot view is enabled useEffect(() => { if (showHotspots && hotspots.length === 0) { @@ -229,16 +324,19 @@ export function MapPage() {
- {isLoadingReports ? ( + {isLoadingReports || isLoadingEvents ? (
-

Loading reports...

+

+ Loading {isLoadingReports && isLoadingEvents ? 'map data' : isLoadingReports ? 'reports' : 'events'}... +

) : (