import { useState, useEffect } from 'react'; import { useAuth } from '@clerk/clerk-react'; import { MapView, MapFilters } from '@/components/map'; import { ReportDetailModal } from '@/components/report'; import { analyticsService } from '@/services/analyticsService'; import { reportService } from '@/services/reportService'; import { eventService } from '@/services/eventService'; import type { Report, FilterState, HotspotWithSeverity, CleanupEvent, ReportResponseDTO } from '@/types'; /** * Map page - Interactive map displaying litter reports and cleanup events */ export function MapPage() { const { getToken, isSignedIn } = useAuth(); const [selectedReportId, setSelectedReportId] = useState(null); const [selectedReport, setSelectedReport] = useState(null); const [isLoadingReport, setIsLoadingReport] = useState(false); const [filters, setFilters] = useState({}); 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); // Set up token getter for report service useEffect(() => { reportService.setTokenGetter(getToken); }, [getToken]); // Fetch reports from API useEffect(() => { const fetchReports = async () => { try { setIsLoadingReports(true); // 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); // Fall back to sample data on error setReports(sampleReports); } finally { setIsLoadingReports(false); } }; 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) => { setModalImageUrl(url); }; return () => { delete (window as any).openImageModal; }; }, []); // 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', userId: 'user1', latitude: 53.3498, longitude: -6.2603, locationSource: 'gps', photoUrls: ['https://example.com/photo1.jpg'], litterType: 'plastic', quantity: 'moderate', description: 'Plastic bottles and bags near the shore', createdAt: new Date('2024-01-15'), updatedAt: new Date('2024-01-15'), submittedAt: new Date('2024-01-15'), verificationStatus: 'verified', }, { id: '2', userId: 'user2', latitude: 53.2707, longitude: -9.0568, locationSource: 'gps', photoUrls: ['https://example.com/photo2.jpg'], litterType: 'glass', quantity: 'minimal', description: 'Broken glass bottles', createdAt: new Date('2024-01-16'), updatedAt: new Date('2024-01-16'), submittedAt: new Date('2024-01-16'), verificationStatus: 'pending', }, { id: '3', userId: 'user3', latitude: 51.8969, longitude: -8.4863, locationSource: 'manual', photoUrls: ['https://example.com/photo3.jpg'], litterType: 'metal', quantity: 'significant', description: 'Metal cans and containers', createdAt: new Date('2024-01-17'), updatedAt: new Date('2024-01-17'), submittedAt: new Date('2024-01-17'), verificationStatus: 'verified', }, { id: '4', userId: 'user4', latitude: 53.3520, longitude: -6.2650, locationSource: 'gps', photoUrls: ['https://example.com/photo4.jpg'], litterType: 'plastic', quantity: 'severe', description: 'Large amount of plastic waste', createdAt: new Date('2024-01-18'), updatedAt: new Date('2024-01-18'), submittedAt: new Date('2024-01-18'), verificationStatus: 'verified', }, { id: '5', userId: 'user5', latitude: 53.3450, longitude: -6.2580, locationSource: 'gps', photoUrls: ['https://example.com/photo5.jpg'], litterType: 'hazardous', quantity: 'moderate', description: 'Chemical containers', createdAt: new Date('2024-01-19'), updatedAt: new Date('2024-01-19'), submittedAt: new Date('2024-01-19'), verificationStatus: 'disputed', }, { id: '6', userId: 'user6', latitude: 53.3480, longitude: -6.2620, locationSource: 'manual', photoUrls: ['https://example.com/photo6.jpg'], litterType: 'organic', quantity: 'minimal', description: 'Seaweed and organic debris', createdAt: new Date('2024-01-20'), updatedAt: new Date('2024-01-20'), submittedAt: new Date('2024-01-20'), verificationStatus: 'verified', }, ]; const handleMarkerClick = async (reportId: string) => { setSelectedReportId(reportId); setIsLoadingReport(true); try { const report = await reportService.getReport(reportId); setSelectedReport(report); } catch (error) { console.error('Failed to fetch report details:', error); } finally { setIsLoadingReport(false); } }; const handleCloseModal = () => { setSelectedReportId(null); setSelectedReport(null); }; const handleVerify = async (reportId: string, comment?: string) => { try { await reportService.verifyReport(reportId, comment); // Refresh the report details const updatedReport = await reportService.getReport(reportId); setSelectedReport(updatedReport); // Refresh the reports list 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: any) { console.error('Failed to verify report:', error); alert(error.message || 'Failed to verify report. Please try again.'); } }; const handleDispute = async (reportId: string, comment?: string) => { try { await reportService.disputeReport(reportId, comment); // Refresh the report details const updatedReport = await reportService.getReport(reportId); setSelectedReport(updatedReport); // Refresh the reports list 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: any) { console.error('Failed to dispute report:', error); alert(error.message || 'Failed to dispute report. Please try again.'); } }; const handleMarkCleaned = async (reportId: string) => { try { await reportService.markAsCleaned(reportId); // Refresh the report details const updatedReport = await reportService.getReport(reportId); setSelectedReport(updatedReport); // Refresh the reports list 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); alert('Report marked as cleaned up!'); } catch (error: any) { console.error('Failed to mark report as cleaned:', error); alert(error.message || 'Failed to mark report as cleaned. Please try again.'); } }; const handleMapClick = (lat: number, lng: number) => { console.log('Map clicked at:', lat, lng); // In later tasks, this can be used for manual location selection }; const handleToggleClusters = () => { setShowClusters(!showClusters); }; const handleToggleHeatmap = () => { setShowHeatmap(!showHeatmap); }; const handleToggleHotspots = () => { setShowHotspots(!showHotspots); }; const handleToggleEvents = () => { setShowEvents(!showEvents); }; // Fetch hotspots when hotspot view is enabled useEffect(() => { if (showHotspots && hotspots.length === 0) { setIsLoadingHotspots(true); analyticsService.getHotspots() .then((data) => { setHotspots(data); }) .catch((error) => { console.error('Failed to fetch hotspots:', error); // Use sample hotspot data for demonstration setHotspots([ { id: 'hotspot1', latitude: 53.3498, longitude: -6.2603, radius: 500, reportCount: 15, severityScore: 0.75, lastReportDate: new Date('2024-01-20'), calculatedAt: new Date('2024-01-21'), severity: 'high', }, { id: 'hotspot2', latitude: 51.8969, longitude: -8.4863, radius: 450, reportCount: 8, severityScore: 0.45, lastReportDate: new Date('2024-01-19'), calculatedAt: new Date('2024-01-21'), severity: 'medium', }, { id: 'hotspot3', latitude: 53.2707, longitude: -9.0568, radius: 600, reportCount: 22, severityScore: 0.88, lastReportDate: new Date('2024-01-21'), calculatedAt: new Date('2024-01-21'), severity: 'critical', }, ]); }) .finally(() => { setIsLoadingHotspots(false); }); } }, [showHotspots]); return (

Interactive Map

View litter reports and cleanup events across Ireland

{isLoadingReports || isLoadingEvents ? (

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

) : ( )}
{/* Report Detail Modal */} {selectedReport && ( )} {/* Loading indicator for report details */} {isLoadingReport && selectedReportId && (

Loading report details...

)} {/* Image Modal */} {modalImageUrl && (
setModalImageUrl(null)} >
Full size litter photo e.stopPropagation()} />
)}
); }