Compare commits
4 Commits
7153e55f4d
...
5cbdb8f284
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cbdb8f284 | |||
| 2b8e4e9b31 | |||
| 8e73d8a2a5 | |||
| 91ecbd2443 |
0
EVENT_MAP_FEATURES.md
Normal file
0
EVENT_MAP_FEATURES.md
Normal file
167
MAP_EVENT_INTEGRATION.md
Normal file
167
MAP_EVENT_INTEGRATION.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Map-Event Integration Implementation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Successfully integrated cleanup events with the interactive map, allowing users to view both litter reports and cleanup events on the same map interface.
|
||||||
|
|
||||||
|
## Recent Updates
|
||||||
|
|
||||||
|
### Latest Changes (Commit: 8e73d8a)
|
||||||
|
1. **Event Popup Enhancement**: Added "View Event Details" button that navigates to `/events?id={eventId}`
|
||||||
|
2. **Map Positioning**: Adjusted default center from Dublin (53.3498, -6.2603) to (53.4, -7.5) to show all of Ireland, especially when filter panel is open
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. MapPage Component (`src/pages/MapPage.tsx`)
|
||||||
|
|
||||||
|
**Added:**
|
||||||
|
- Event state management with `useState` for events and loading state
|
||||||
|
- `useEffect` hook to fetch events from the API on component mount
|
||||||
|
- Sample event data as fallback when API is unavailable
|
||||||
|
- Event visibility toggle state (`showEvents`)
|
||||||
|
- Handler function `handleToggleEvents()` to show/hide events
|
||||||
|
- Fixed date filter conversion from Date objects to ISO strings for API compatibility
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- Events are fetched independently from reports
|
||||||
|
- Loading states for both reports and events
|
||||||
|
- Events passed to MapView component only when `showEvents` is true
|
||||||
|
|
||||||
|
### 2. MapView Component (`src/components/map/MapView.tsx`)
|
||||||
|
|
||||||
|
**Enhanced:**
|
||||||
|
- Event marker popups with rich information display:
|
||||||
|
- Event title and description
|
||||||
|
- Location name with map pin emoji
|
||||||
|
- Scheduled date and time (formatted for Irish locale)
|
||||||
|
- Duration in minutes
|
||||||
|
- Participant count
|
||||||
|
- Litter collected (for completed events)
|
||||||
|
- Status badge with color coding
|
||||||
|
- Event markers use calendar emoji (📅)
|
||||||
|
- Different marker colors based on event status:
|
||||||
|
- **Upcoming**: Amber (#F59E0B)
|
||||||
|
- **Completed**: Green (#10B981)
|
||||||
|
- **Cancelled**: Gray (#6B7280)
|
||||||
|
|
||||||
|
**Event Marker Features:**
|
||||||
|
- Always displayed on regular layer (not clustered)
|
||||||
|
- 32x32 pixel markers with white border
|
||||||
|
- Hover effects and click interactions
|
||||||
|
- Detailed popup with formatted information
|
||||||
|
- "View Event Details" button in popup for navigation to event page
|
||||||
|
|
||||||
|
**Map Positioning:**
|
||||||
|
- Default center: (53.4, -7.5) - optimized to show all of Ireland
|
||||||
|
- Zoom level: 7
|
||||||
|
- Ensures Ireland is fully visible even with filter panel open
|
||||||
|
|
||||||
|
### 3. MapFilters Component (`src/components/map/MapFilters.tsx`)
|
||||||
|
|
||||||
|
**Added:**
|
||||||
|
- "Show Events" toggle switch
|
||||||
|
- Props: `onToggleEvents` and `showEvents`
|
||||||
|
- Green color scheme for event toggle (to distinguish from other toggles)
|
||||||
|
- Positioned above the Hotspots toggle
|
||||||
|
|
||||||
|
## Sample Event Data
|
||||||
|
|
||||||
|
Three sample events are provided as fallback:
|
||||||
|
1. **Sandymount Beach Cleanup** (Dublin) - Upcoming
|
||||||
|
2. **Galway Bay Cleanup** (Salthill Beach) - Upcoming
|
||||||
|
3. **Cork Harbor Cleanup** - Completed (with 45.5 kg collected)
|
||||||
|
|
||||||
|
## User Experience
|
||||||
|
|
||||||
|
### Viewing Events on Map
|
||||||
|
1. Navigate to the Map page
|
||||||
|
2. Events are displayed by default with calendar markers
|
||||||
|
3. Click on an event marker to see detailed information
|
||||||
|
4. Toggle "Show Events" in the filters panel to hide/show events
|
||||||
|
|
||||||
|
### Event Information Display
|
||||||
|
Each event popup shows:
|
||||||
|
- 📍 Location name
|
||||||
|
- 📅 Scheduled date and time
|
||||||
|
- ⏱️ Duration
|
||||||
|
- 👥 Number of participants
|
||||||
|
- ♻️ Litter collected (for completed events)
|
||||||
|
- Status badge (color-coded)
|
||||||
|
- Event description
|
||||||
|
- **"View Event Details" button** - navigates to event details page
|
||||||
|
|
||||||
|
## API Integration
|
||||||
|
|
||||||
|
### Event Service
|
||||||
|
Uses `eventService.getEvents()` to fetch events from the backend:
|
||||||
|
- Endpoint: `GET /api/events`
|
||||||
|
- Returns array of `CleanupEvent` objects
|
||||||
|
- Supports optional filters (status, date range, location)
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Falls back to sample data if API call fails
|
||||||
|
- Logs errors to console for debugging
|
||||||
|
- Graceful degradation ensures map remains functional
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Type Safety
|
||||||
|
- Full TypeScript support with `CleanupEvent` interface
|
||||||
|
- Type-safe props and state management
|
||||||
|
- Proper type conversion for date filters
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Events fetched once on component mount
|
||||||
|
- Separate loading states for reports and events
|
||||||
|
- Efficient marker rendering with Leaflet
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
- Toggle switches with proper ARIA labels
|
||||||
|
- Semantic HTML structure
|
||||||
|
- Keyboard navigation support
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential improvements:
|
||||||
|
1. Filter events by status (upcoming/completed/cancelled)
|
||||||
|
2. Click event marker to navigate to event detail page
|
||||||
|
3. Show event radius/coverage area on map
|
||||||
|
4. Link related reports to events
|
||||||
|
5. Real-time event updates with WebSockets
|
||||||
|
6. Event clustering for dense areas
|
||||||
|
7. Calendar view integration
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
To test the implementation:
|
||||||
|
1. Start the backend server: `cd server && npm run dev`
|
||||||
|
2. Start the frontend: `npm run dev`
|
||||||
|
3. Navigate to the Map page
|
||||||
|
4. Verify events appear on the map
|
||||||
|
5. Click event markers to see popups
|
||||||
|
6. Toggle "Show Events" to hide/show markers
|
||||||
|
7. Check that event colors match their status
|
||||||
|
|
||||||
|
## Related Requirements
|
||||||
|
|
||||||
|
This implementation addresses:
|
||||||
|
- **Requirement 4.1**: Display event markers on map
|
||||||
|
- **Requirement 4.2**: Show event details on click
|
||||||
|
- **Requirement 4.3**: Display participant count
|
||||||
|
- **Requirement 2.1**: Interactive map visualization
|
||||||
|
- **Requirement 2.2**: Marker click handlers
|
||||||
|
|
||||||
|
## Commits
|
||||||
|
|
||||||
|
### Initial Implementation (91ecbd2)
|
||||||
|
```
|
||||||
|
Link map items with events - Display cleanup events on interactive map
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enhancement (8e73d8a)
|
||||||
|
```
|
||||||
|
Improve event popup and map positioning
|
||||||
|
|
||||||
|
- Added 'View Event Details' button in event popup
|
||||||
|
- Adjusted map center to better show all of Ireland
|
||||||
|
```
|
||||||
@@ -11,6 +11,8 @@ interface MapFiltersProps {
|
|||||||
onToggleHotspots?: () => void;
|
onToggleHotspots?: () => void;
|
||||||
showHotspots?: boolean;
|
showHotspots?: boolean;
|
||||||
isLoadingHotspots?: boolean;
|
isLoadingHotspots?: boolean;
|
||||||
|
onToggleEvents?: () => void;
|
||||||
|
showEvents?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MapFilters({
|
export function MapFilters({
|
||||||
@@ -23,6 +25,8 @@ export function MapFilters({
|
|||||||
onToggleHotspots,
|
onToggleHotspots,
|
||||||
showHotspots = false,
|
showHotspots = false,
|
||||||
isLoadingHotspots = false,
|
isLoadingHotspots = false,
|
||||||
|
onToggleEvents,
|
||||||
|
showEvents = true,
|
||||||
}: MapFiltersProps) {
|
}: MapFiltersProps) {
|
||||||
const [isExpanded, setIsExpanded] = useState(true);
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
|
|
||||||
@@ -159,6 +163,30 @@ export function MapFilters({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* Hotspots Toggle */}
|
||||||
{onToggleHotspots && (
|
{onToggleHotspots && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function MapView({
|
|||||||
showClusters = true,
|
showClusters = true,
|
||||||
showHeatmap = false,
|
showHeatmap = false,
|
||||||
showHotspots = false,
|
showHotspots = false,
|
||||||
center = [53.3498, -6.2603], // Dublin, Ireland default
|
center = [53.4, -9.0], // Centered on Ireland, minimizing UK visibility
|
||||||
zoom = 7,
|
zoom = 7,
|
||||||
}: MapViewProps) {
|
}: MapViewProps) {
|
||||||
const mapRef = useRef<HTMLDivElement>(null);
|
const mapRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -229,12 +229,60 @@ export function MapView({
|
|||||||
const icon = createEventMarkerIcon(event.status);
|
const icon = createEventMarkerIcon(event.status);
|
||||||
const marker = L.marker([event.latitude, event.longitude], { icon });
|
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(`
|
marker.bindPopup(`
|
||||||
<div class="p-2">
|
<div class="p-3 min-w-[250px]">
|
||||||
<h3 class="font-bold text-sm mb-1">${event.title}</h3>
|
<h3 class="font-bold text-base mb-2">${event.title}</h3>
|
||||||
<p class="text-xs text-gray-600">${event.locationName}</p>
|
<div class="space-y-1 text-sm">
|
||||||
<p class="text-xs text-gray-600">Participants: ${event.participantCount}</p>
|
<p class="flex items-start gap-2">
|
||||||
<p class="text-xs text-gray-600">Status: ${event.status}</p>
|
<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>
|
||||||
|
` : ''}
|
||||||
|
<button
|
||||||
|
onclick="window.location.href='/events?id=${event.id}'"
|
||||||
|
class="mt-3 w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
View Event Details
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { useState, useEffect } from 'react';
|
|||||||
import { MapView, MapFilters } from '@/components/map';
|
import { MapView, MapFilters } from '@/components/map';
|
||||||
import { analyticsService } from '@/services/analyticsService';
|
import { analyticsService } from '@/services/analyticsService';
|
||||||
import { reportService } from '@/services/reportService';
|
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
|
* Map page - Interactive map displaying litter reports and cleanup events
|
||||||
@@ -13,10 +14,13 @@ export function MapPage() {
|
|||||||
const [showClusters, setShowClusters] = useState(true);
|
const [showClusters, setShowClusters] = useState(true);
|
||||||
const [showHeatmap, setShowHeatmap] = useState(false);
|
const [showHeatmap, setShowHeatmap] = useState(false);
|
||||||
const [showHotspots, setShowHotspots] = useState(false);
|
const [showHotspots, setShowHotspots] = useState(false);
|
||||||
|
const [showEvents, setShowEvents] = useState(true);
|
||||||
const [hotspots, setHotspots] = useState<HotspotWithSeverity[]>([]);
|
const [hotspots, setHotspots] = useState<HotspotWithSeverity[]>([]);
|
||||||
const [isLoadingHotspots, setIsLoadingHotspots] = useState(false);
|
const [isLoadingHotspots, setIsLoadingHotspots] = useState(false);
|
||||||
const [reports, setReports] = useState<Report[]>([]);
|
const [reports, setReports] = useState<Report[]>([]);
|
||||||
const [isLoadingReports, setIsLoadingReports] = useState(true);
|
const [isLoadingReports, setIsLoadingReports] = useState(true);
|
||||||
|
const [events, setEvents] = useState<CleanupEvent[]>([]);
|
||||||
|
const [isLoadingEvents, setIsLoadingEvents] = useState(true);
|
||||||
const [modalImageUrl, setModalImageUrl] = useState<string | null>(null);
|
const [modalImageUrl, setModalImageUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
// Fetch reports from API
|
// Fetch reports from API
|
||||||
@@ -24,7 +28,25 @@ export function MapPage() {
|
|||||||
const fetchReports = async () => {
|
const fetchReports = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoadingReports(true);
|
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);
|
setReports(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch reports:', error);
|
console.error('Failed to fetch reports:', error);
|
||||||
@@ -38,6 +60,26 @@ export function MapPage() {
|
|||||||
fetchReports();
|
fetchReports();
|
||||||
}, [filters]);
|
}, [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
|
// Set up global function for opening image modal
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(window as any).openImageModal = (url: string) => {
|
(window as any).openImageModal = (url: string) => {
|
||||||
@@ -49,6 +91,55 @@ export function MapPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Sample data for fallback
|
// 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[] = [
|
const sampleReports: Report[] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -165,6 +256,10 @@ export function MapPage() {
|
|||||||
setShowHotspots(!showHotspots);
|
setShowHotspots(!showHotspots);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleEvents = () => {
|
||||||
|
setShowEvents(!showEvents);
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch hotspots when hotspot view is enabled
|
// Fetch hotspots when hotspot view is enabled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showHotspots && hotspots.length === 0) {
|
if (showHotspots && hotspots.length === 0) {
|
||||||
@@ -229,16 +324,19 @@ export function MapPage() {
|
|||||||
|
|
||||||
<div className="flex-1 p-4">
|
<div className="flex-1 p-4">
|
||||||
<div className="h-full bg-white rounded-lg shadow-lg overflow-hidden relative">
|
<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="absolute inset-0 flex items-center justify-center bg-gray-50">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<MapView
|
<MapView
|
||||||
reports={reports}
|
reports={reports}
|
||||||
|
events={showEvents ? events : []}
|
||||||
hotspots={hotspots}
|
hotspots={hotspots}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
showClusters={showClusters}
|
showClusters={showClusters}
|
||||||
@@ -256,6 +354,8 @@ export function MapPage() {
|
|||||||
showClusters={showClusters}
|
showClusters={showClusters}
|
||||||
onToggleHeatmap={handleToggleHeatmap}
|
onToggleHeatmap={handleToggleHeatmap}
|
||||||
showHeatmap={showHeatmap}
|
showHeatmap={showHeatmap}
|
||||||
|
onToggleEvents={handleToggleEvents}
|
||||||
|
showEvents={showEvents}
|
||||||
onToggleHotspots={handleToggleHotspots}
|
onToggleHotspots={handleToggleHotspots}
|
||||||
showHotspots={showHotspots}
|
showHotspots={showHotspots}
|
||||||
isLoadingHotspots={isLoadingHotspots}
|
isLoadingHotspots={isLoadingHotspots}
|
||||||
|
|||||||
Reference in New Issue
Block a user