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
This commit is contained in:
@@ -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({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Events Toggle */}
|
||||
{onToggleEvents && (
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="events-toggle" className="text-sm font-medium text-gray-700">
|
||||
Show Events
|
||||
</label>
|
||||
<button
|
||||
id="events-toggle"
|
||||
onClick={onToggleEvents}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
showEvents ? 'bg-green-600' : 'bg-gray-200'
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={showEvents}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
showEvents ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hotspots Toggle */}
|
||||
{onToggleHotspots && (
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -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(`
|
||||
<div class="p-2">
|
||||
<h3 class="font-bold text-sm mb-1">${event.title}</h3>
|
||||
<p class="text-xs text-gray-600">${event.locationName}</p>
|
||||
<p class="text-xs text-gray-600">Participants: ${event.participantCount}</p>
|
||||
<p class="text-xs text-gray-600">Status: ${event.status}</p>
|
||||
<div class="p-3 min-w-[250px]">
|
||||
<h3 class="font-bold text-base mb-2">${event.title}</h3>
|
||||
<div class="space-y-1 text-sm">
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="text-gray-500">📍</span>
|
||||
<span>${event.locationName}</span>
|
||||
</p>
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="text-gray-500">📅</span>
|
||||
<span>${scheduledDate}</span>
|
||||
</p>
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="text-gray-500">⏱️</span>
|
||||
<span>${event.duration} minutes</span>
|
||||
</p>
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="text-gray-500">👥</span>
|
||||
<span>${event.participantCount} participant${event.participantCount !== 1 ? 's' : ''}</span>
|
||||
</p>
|
||||
${event.status === 'completed' && event.litterCollected ? `
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="text-gray-500">♻️</span>
|
||||
<span>${event.litterCollected} kg collected</span>
|
||||
</p>
|
||||
` : ''}
|
||||
<p class="mt-2 pt-2 border-t">
|
||||
<span class="inline-block px-2 py-1 text-xs font-semibold rounded ${
|
||||
event.status === 'upcoming' ? 'bg-amber-100 text-amber-800' :
|
||||
event.status === 'completed' ? 'bg-green-100 text-green-800' :
|
||||
'bg-gray-100 text-gray-800'
|
||||
}">
|
||||
${event.status.charAt(0).toUpperCase() + event.status.slice(1)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
${event.description ? `
|
||||
<p class="text-xs text-gray-600 mt-2 pt-2 border-t">${event.description}</p>
|
||||
` : ''}
|
||||
</div>
|
||||
`);
|
||||
|
||||
|
||||
@@ -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<HotspotWithSeverity[]>([]);
|
||||
const [isLoadingHotspots, setIsLoadingHotspots] = useState(false);
|
||||
const [reports, setReports] = useState<Report[]>([]);
|
||||
const [isLoadingReports, setIsLoadingReports] = useState(true);
|
||||
const [events, setEvents] = useState<CleanupEvent[]>([]);
|
||||
const [isLoadingEvents, setIsLoadingEvents] = useState(true);
|
||||
const [modalImageUrl, setModalImageUrl] = useState<string | null>(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<typeof reportService.getReports>[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() {
|
||||
|
||||
<div className="flex-1 p-4">
|
||||
<div className="h-full bg-white rounded-lg shadow-lg overflow-hidden relative">
|
||||
{isLoadingReports ? (
|
||||
{isLoadingReports || isLoadingEvents ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading reports...</p>
|
||||
<p className="mt-4 text-gray-600">
|
||||
Loading {isLoadingReports && isLoadingEvents ? 'map data' : isLoadingReports ? 'reports' : 'events'}...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<MapView
|
||||
reports={reports}
|
||||
events={showEvents ? events : []}
|
||||
hotspots={hotspots}
|
||||
filters={filters}
|
||||
showClusters={showClusters}
|
||||
@@ -256,6 +354,8 @@ export function MapPage() {
|
||||
showClusters={showClusters}
|
||||
onToggleHeatmap={handleToggleHeatmap}
|
||||
showHeatmap={showHeatmap}
|
||||
onToggleEvents={handleToggleEvents}
|
||||
showEvents={showEvents}
|
||||
onToggleHotspots={handleToggleHotspots}
|
||||
showHotspots={showHotspots}
|
||||
isLoadingHotspots={isLoadingHotspots}
|
||||
|
||||
Reference in New Issue
Block a user