MIF_E31221222/sigap-website/app/_components/map/layers/recent-crimes-layer.tsx

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
}