feat: add map reset functionality and improve incident popup handling

This commit is contained in:
vergiLgood1 2025-05-05 03:42:32 +07:00
parent 6327d7b886
commit c282d958a5
3 changed files with 175 additions and 115 deletions

View File

@ -21,6 +21,7 @@ import { CrimeTimelapse } from "./controls/bottom/crime-timelapse"
import { ITooltips } from "./controls/top/tooltips" import { ITooltips } from "./controls/top/tooltips"
import CrimeSidebar from "./controls/left/sidebar/map-sidebar" import CrimeSidebar from "./controls/left/sidebar/map-sidebar"
import Tooltips from "./controls/top/tooltips" import Tooltips from "./controls/top/tooltips"
import { BASE_BEARING, BASE_PITCH, BASE_ZOOM } from "@/app/_utils/const/map"
// Updated CrimeIncident type to match the structure in crime_incidents // Updated CrimeIncident type to match the structure in crime_incidents
interface ICrimeIncident { interface ICrimeIncident {
@ -225,6 +226,57 @@ export default function CrimeMap() {
}; };
}, []); }, []);
// Add event listener for map reset
useEffect(() => {
const handleMapReset = (e: CustomEvent) => {
const { duration } = e.detail || { duration: 1500 };
// Find the map instance
const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map');
if (!mapInstance) {
console.error("Map instance not found");
return;
}
// Create and dispatch the reset event that MapView will listen for
const mapboxEvent = new CustomEvent('mapbox_fly', {
detail: {
duration: duration,
resetCamera: true
},
bubbles: true
});
mapInstance.dispatchEvent(mapboxEvent);
};
// Add event listener
document.addEventListener('mapbox_reset', handleMapReset as EventListener);
return () => {
document.removeEventListener('mapbox_reset', handleMapReset as EventListener);
};
}, []);
// Update the popup close handler to reset the map view
const handlePopupClose = () => {
setSelectedIncident(null);
// Dispatch map reset event to reset zoom, pitch, and bearing
const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map');
if (mapInstance) {
const resetEvent = new CustomEvent('mapbox_reset', {
detail: {
duration: 1500,
},
bubbles: true
});
mapInstance.dispatchEvent(resetEvent);
}
}
// Handle year-month timeline change // Handle year-month timeline change
const handleTimelineChange = useCallback((year: number, month: number, progress: number) => { const handleTimelineChange = useCallback((year: number, month: number, progress: number) => {
setSelectedYear(year) setSelectedYear(year)
@ -262,11 +314,6 @@ export default function CrimeMap() {
return title return title
} }
// Toggle sidebar function
const toggleSidebar = useCallback(() => {
setSidebarCollapsed(!sidebarCollapsed)
}, [sidebarCollapsed])
// Handle control changes from the top controls component // Handle control changes from the top controls component
const handleControlChange = (controlId: ITooltips) => { const handleControlChange = (controlId: ITooltips) => {
setActiveControl(controlId) setActiveControl(controlId)
@ -327,7 +374,7 @@ export default function CrimeMap() {
<CrimePopup <CrimePopup
longitude={selectedIncident.longitude} longitude={selectedIncident.longitude}
latitude={selectedIncident.latitude} latitude={selectedIncident.latitude}
onClose={() => setSelectedIncident(null)} onClose={handlePopupClose}
incident={selectedIncident} incident={selectedIncident}
/> />
@ -362,7 +409,7 @@ export default function CrimeMap() {
selectedMonth={selectedMonth} selectedMonth={selectedMonth}
/> />
<div className="absolute bottom-20 right-0 z-10 p-2"> <div className="absolute bottom-20 right-0 z-10 p-2">
<MapLegend position="bottom-right" /> <MapLegend position="bottom-right" />
</div> </div>
</> </>

View File

@ -88,16 +88,29 @@ export default function MapView({
const handleMapFly = (e: CustomEvent) => { const handleMapFly = (e: CustomEvent) => {
if (!e.detail || !mapRef.current) return; if (!e.detail || !mapRef.current) return;
const { center, zoom, bearing, pitch, duration } = e.detail; const { center, zoom, bearing, pitch, duration, resetCamera } = e.detail;
mapRef.current.flyTo({ if (resetCamera) {
center, // Reset to default view
zoom, mapRef.current.flyTo({
bearing, center: [BASE_LONGITUDE, BASE_LATITUDE],
pitch, zoom: BASE_ZOOM,
duration, bearing: BASE_BEARING,
essential: true pitch: BASE_PITCH,
}); duration,
essential: true
});
} else {
// Fly to specific location
mapRef.current.flyTo({
center,
zoom,
bearing,
pitch,
duration,
essential: true
});
}
}; };
mapElement.addEventListener('mapbox_fly', handleMapFly as EventListener); mapElement.addEventListener('mapbox_fly', handleMapFly as EventListener);

View File

@ -27,31 +27,31 @@ interface IncidentPopupProps {
export default function IncidentPopup({ longitude, latitude, onClose, incident }: IncidentPopupProps) { export default function IncidentPopup({ longitude, latitude, onClose, incident }: IncidentPopupProps) {
const formatDate = (date?: Date) => { const formatDate = (date?: Date) => {
if (!date) return "Unknown date" if (!date) return "Unknown date"
return new Date(date).toLocaleDateString() return new Date(date).toLocaleDateString()
} }
const formatTime = (date?: Date) => { const formatTime = (date?: Date) => {
if (!date) return "Unknown time" if (!date) return "Unknown time"
return new Date(date).toLocaleTimeString() return new Date(date).toLocaleTimeString()
} }
const getStatusBadge = (status?: string) => { const getStatusBadge = (status?: string) => {
if (!status) return <Badge variant="outline">Unknown</Badge> if (!status) return <Badge variant="outline">Unknown</Badge>
const statusLower = status.toLowerCase() const statusLower = status.toLowerCase()
if (statusLower.includes("resolv") || statusLower.includes("closed")) { if (statusLower.includes("resolv") || statusLower.includes("closed")) {
return <Badge className="bg-emerald-600 text-white">Resolved</Badge> return <Badge className="bg-emerald-600 text-white">Resolved</Badge>
} }
if (statusLower.includes("progress") || statusLower.includes("invest")) { if (statusLower.includes("progress") || statusLower.includes("invest")) {
return <Badge className="bg-amber-500 text-white">In Progress</Badge> return <Badge className="bg-amber-500 text-white">In Progress</Badge>
} }
if (statusLower.includes("open") || statusLower.includes("new")) { if (statusLower.includes("open") || statusLower.includes("new")) {
return <Badge className="bg-blue-600 text-white">Open</Badge> return <Badge className="bg-blue-600 text-white">Open</Badge>
} }
return <Badge variant="outline">{status}</Badge> return <Badge variant="outline">{status}</Badge>
} }
// Determine border color based on status // Determine border color based on status
const getBorderColor = (status?: string) => { const getBorderColor = (status?: string) => {
@ -75,108 +75,108 @@ export default function IncidentPopup({ longitude, latitude, onClose, incident }
<Popup <Popup
longitude={longitude} longitude={longitude}
latitude={latitude} latitude={latitude}
closeButton={false} // Hide default close button closeButton={false}
closeOnClick={false} closeOnClick={false}
onClose={onClose} onClose={onClose}
anchor="top" anchor="top"
maxWidth="320px" maxWidth="320px"
className="incident-popup z-50" className="incident-popup z-50"
> >
<Card <Card
className={`bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 ${getBorderColor(incident.status)}`} className={`bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 ${getBorderColor(incident.status)}`}
> >
<div className="p-4 relative"> <div className="p-4 relative">
{/* Custom close button */} {/* Custom close button */}
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="absolute top-2 right-2 h-6 w-6 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700" className="absolute top-2 right-2 h-6 w-6 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
onClick={onClose} onClick={onClose}
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</Button> </Button>
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="font-bold text-base flex items-center gap-1.5"> <h3 className="font-bold text-base flex items-center gap-1.5">
<AlertTriangle className="h-4 w-4 text-red-500" /> <AlertTriangle className="h-4 w-4 text-red-500" />
{incident.category || "Unknown Incident"} {incident.category || "Unknown Incident"}
</h3> </h3>
{getStatusBadge(incident.status)} {getStatusBadge(incident.status)}
</div> </div>
{incident.description && ( {incident.description && (
<div className="mb-3 bg-slate-50 dark:bg-slate-900/40 p-3 rounded-lg"> <div className="mb-3 bg-slate-50 dark:bg-slate-900/40 p-3 rounded-lg">
<p className="text-sm"> <p className="text-sm">
<FileText className="inline-block h-3.5 w-3.5 mr-1.5 align-text-top text-slate-500" /> <FileText className="inline-block h-3.5 w-3.5 mr-1.5 align-text-top text-slate-500" />
{incident.description} {incident.description}
</p> </p>
</div> </div>
)} )}
<Separator className="my-3" /> <Separator className="my-3" />
{/* Improved section headers */} {/* Improved section headers */}
<div className="grid grid-cols-2 gap-2 text-sm"> <div className="grid grid-cols-2 gap-2 text-sm">
{incident.district && ( {incident.district && (
<div className="col-span-2"> <div className="col-span-2">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">District</p> <p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">District</p>
<p className="flex items-center"> <p className="flex items-center">
<Bookmark className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" /> <Bookmark className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" />
<span className="font-medium">{incident.district}</span> <span className="font-medium">{incident.district}</span>
</p> </p>
</div> </div>
)} )}
{incident.address && ( {incident.address && (
<div className="col-span-2"> <div className="col-span-2">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Location</p> <p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Location</p>
<p className="flex items-center"> <p className="flex items-center">
<MapPin className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-red-500" /> <MapPin className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-red-500" />
<span className="font-medium">{incident.address}</span> <span className="font-medium">{incident.address}</span>
</p> </p>
</div> </div>
)} )}
{incident.timestamp && ( {incident.timestamp && (
<> <>
<div> <div>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p> <p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p>
<p className="flex items-center"> <p className="flex items-center">
<Calendar className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-blue-500" /> <Calendar className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-blue-500" />
<span className="font-medium">{formatDate(incident.timestamp)}</span> <span className="font-medium">{formatDate(incident.timestamp)}</span>
</p> </p>
</div> </div>
<div> <div>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Time</p> <p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Time</p>
<p className="flex items-center"> <p className="flex items-center">
<Clock className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-amber-500" /> <Clock className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-amber-500" />
<span className="font-medium">{formatTime(incident.timestamp)}</span> <span className="font-medium">{formatTime(incident.timestamp)}</span>
</p> </p>
</div> </div>
</> </>
)} )}
{incident.type_category && ( {incident.type_category && (
<div className="col-span-2"> <div className="col-span-2">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Type</p> <p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Type</p>
<p className="flex items-center"> <p className="flex items-center">
<Tag className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" /> <Tag className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" />
<span className="font-medium">{incident.type_category}</span> <span className="font-medium">{incident.type_category}</span>
</p> </p>
</div> </div>
)} )}
</div> </div>
<div className="mt-3 pt-3 border-t border-border"> <div className="mt-3 pt-3 border-t border-border">
<p className="text-xs text-muted-foreground flex items-center"> <p className="text-xs text-muted-foreground flex items-center">
<Navigation className="inline-block h-3 w-3 mr-1 shrink-0" /> <Navigation className="inline-block h-3 w-3 mr-1 shrink-0" />
Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)} Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)}
</p> </p>
<p className="text-xs text-muted-foreground mt-1">ID: {incident.id}</p> <p className="text-xs text-muted-foreground mt-1">ID: {incident.id}</p>
</div> </div>
</div> </div>
</Card> </Card>
</Popup> </Popup>
) )
} }