165 lines
6.1 KiB
TypeScript
165 lines
6.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useRef } from 'react';
|
|
import mapboxgl from 'mapbox-gl';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { Clock, FileText, MapPin } from 'lucide-react';
|
|
import { Badge } from '@/app/_components/ui/badge';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
import { CustomAnimatedPopup } from '@/app/_utils/map/custom-animated-popup';
|
|
import { incident_logs } from '@prisma/client';
|
|
import { IIncidentLogs } from '@/app/_utils/types/crimes';
|
|
|
|
// export interface ICrimeIncident {
|
|
// id: string;
|
|
// category: string;
|
|
// location: {
|
|
// latitude: number;
|
|
// longitude: number;
|
|
// address: string;
|
|
// district: string;
|
|
// };
|
|
// timestamp: string;
|
|
// description: string;
|
|
// severity: 'high' | 'medium' | 'low';
|
|
// reportedBy: string;
|
|
// }
|
|
|
|
interface RecentCrimesLayerProps {
|
|
map: mapboxgl.Map | null;
|
|
incidents: IIncidentLogs[];
|
|
visible: boolean;
|
|
onIncidentClick?: (incident: IIncidentLogs) => void;
|
|
}
|
|
|
|
export default function RecentCrimesLayer({
|
|
map,
|
|
incidents,
|
|
visible,
|
|
onIncidentClick,
|
|
}: RecentCrimesLayerProps) {
|
|
const markersRef = useRef<Map<string, mapboxgl.Marker>>(new Map());
|
|
|
|
// Filter incidents to only show those from the last 24 hours
|
|
const recentIncidents = incidents.filter(incident => {
|
|
const incidentDate = new Date(incident.timestamp);
|
|
const now = new Date();
|
|
const timeDiff = now.getTime() - incidentDate.getTime();
|
|
const hoursDiff = timeDiff / (1000 * 60 * 60);
|
|
return hoursDiff <= 24;
|
|
});
|
|
|
|
// Create markers for each incident
|
|
useEffect(() => {
|
|
if (!map || !visible) {
|
|
// Remove all markers if layer is not visible
|
|
markersRef.current.forEach(marker => marker.remove());
|
|
return;
|
|
}
|
|
|
|
// Track existing incident IDs to avoid recreating markers
|
|
const existingIds = new Set(Array.from(markersRef.current.keys()));
|
|
|
|
// Add markers for each recent incident
|
|
recentIncidents.forEach(incident => {
|
|
existingIds.delete(incident.id);
|
|
|
|
if (!markersRef.current.has(incident.id)) {
|
|
// Create marker element
|
|
const el = document.createElement('div');
|
|
el.className = 'crime-marker';
|
|
|
|
// Style based on severity
|
|
const colors = {
|
|
high: 'bg-red-500',
|
|
medium: 'bg-amber-500',
|
|
low: 'bg-blue-500'
|
|
};
|
|
|
|
const markerRoot = createRoot(el);
|
|
markerRoot.render(
|
|
<div className={`p-1 rounded-full ${colors[incident.severity]} shadow-lg pulse-animation`}>
|
|
<MapPin className="h-4 w-4 text-white" />
|
|
</div>
|
|
);
|
|
|
|
// Create popup content
|
|
const popupEl = document.createElement('div');
|
|
const popupRoot = createRoot(popupEl);
|
|
|
|
popupRoot.render(
|
|
<div className="p-2 max-w-[250px]">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Badge className={`
|
|
${incident.severity === 'high' ? 'bg-red-500' :
|
|
incident.severity === 'medium' ? 'bg-amber-500' : 'bg-blue-500'}
|
|
text-white`
|
|
}>
|
|
{incident.category}
|
|
</Badge>
|
|
<span className="text-xs flex items-center gap-1 opacity-75">
|
|
<Clock className="h-3 w-3" />
|
|
{formatDistanceToNow(new Date(incident.timestamp), { addSuffix: true })}
|
|
</span>
|
|
</div>
|
|
|
|
<h3 className="font-medium text-sm">{incident.district}</h3>
|
|
<p className="text-xs text-muted-foreground">{incident.address}</p>
|
|
|
|
<p className="text-xs mt-2 line-clamp-3">{incident.description}</p>
|
|
|
|
<div className="flex items-center justify-between mt-2">
|
|
<span className="text-xs opacity-75">ID: {incident.id.substring(0, 8)}...</span>
|
|
<button
|
|
className="text-xs flex items-center gap-1 text-blue-500 hover:underline"
|
|
onClick={() => onIncidentClick?.(incident)}
|
|
>
|
|
<FileText className="h-3 w-3" /> Details
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
// Create popup
|
|
const popup = new CustomAnimatedPopup({
|
|
closeButton: false,
|
|
maxWidth: '300px',
|
|
offset: 15
|
|
}).setDOMContent(popupEl);
|
|
|
|
// Create and add marker
|
|
const marker = new mapboxgl.Marker(el)
|
|
.setLngLat([incident.longitude, incident.latitude])
|
|
.setPopup(popup)
|
|
.addTo(map);
|
|
|
|
// Add click handler for the entire marker
|
|
el.addEventListener('click', () => {
|
|
if (onIncidentClick) {
|
|
onIncidentClick(incident);
|
|
}
|
|
});
|
|
|
|
markersRef.current.set(incident.id, marker);
|
|
}
|
|
});
|
|
|
|
// Remove any markers that are no longer in the recent incidents list
|
|
existingIds.forEach(id => {
|
|
const marker = markersRef.current.get(id);
|
|
if (marker) {
|
|
marker.remove();
|
|
markersRef.current.delete(id);
|
|
}
|
|
});
|
|
|
|
// Clean up on unmount
|
|
return () => {
|
|
markersRef.current.forEach(marker => marker.remove());
|
|
markersRef.current.clear();
|
|
};
|
|
}, [map, recentIncidents, visible, onIncidentClick]);
|
|
|
|
return null; // This is a functional component with no visual rendering
|
|
}
|