Compare commits

...

4 Commits

Author SHA1 Message Date
5cbdb8f284 Shift map center further west to focus on Ireland
- Moved center from (53.4, -8.0) to (53.4, -9.0)
- Minimizes UK visibility and focuses view on Ireland
- Provides optimal viewing with filter panel open
2025-11-16 21:09:24 +00:00
2b8e4e9b31 Adjust map center further west to -8.0 longitude
- Moved center from (53.4, -7.5) to (53.4, -8.0)
- Provides more clearance for filter panel on the left
- Ensures all of Ireland is clearly visible
2025-11-16 21:08:14 +00:00
8e73d8a2a5 Improve event popup and map positioning
- Added 'View Event Details' button in event popup that navigates to /events?id={eventId}
- Adjusted map center from Dublin (53.3498, -6.2603) to (53.4, -7.5) to better show all of Ireland
- New center point ensures Ireland is fully visible even when filter panel is open
- Button has blue styling with hover effect for better UX
2025-11-16 21:07:10 +00:00
91ecbd2443 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
2025-11-16 21:05:23 +00:00
5 changed files with 353 additions and 10 deletions

0
EVENT_MAP_FEATURES.md Normal file
View File

167
MAP_EVENT_INTEGRATION.md Normal file
View 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
```

View File

@@ -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">

View File

@@ -43,7 +43,7 @@ export function MapView({
showClusters = true,
showHeatmap = false,
showHotspots = false,
center = [53.3498, -6.2603], // Dublin, Ireland default
center = [53.4, -9.0], // Centered on Ireland, minimizing UK visibility
zoom = 7,
}: MapViewProps) {
const mapRef = useRef<HTMLDivElement>(null);
@@ -229,12 +229,60 @@ 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>
` : ''}
<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>
`);

View File

@@ -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}