185 lines
6.0 KiB
TypeScript
185 lines
6.0 KiB
TypeScript
import { Popup } from 'react-map-gl/mapbox';
|
|
import { useState, useEffect } from 'react';
|
|
import { X } from 'lucide-react';
|
|
import { DistrictFeature } from './layers/district-layer';
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { Skeleton } from "@/app/_components/ui/skeleton";
|
|
import { getCrimeRateInfo } from "@/app/_utils/common";
|
|
import { CrimeIncident } from './markers/crime-marker';
|
|
|
|
|
|
interface MapPopupProps {
|
|
longitude: number
|
|
latitude: number
|
|
onClose: () => void
|
|
children: React.ReactNode
|
|
title?: string
|
|
}
|
|
|
|
export function MapPopup({
|
|
longitude,
|
|
latitude,
|
|
onClose,
|
|
children,
|
|
title
|
|
}: MapPopupProps) {
|
|
return (
|
|
<Popup
|
|
longitude={longitude}
|
|
latitude={latitude}
|
|
closeOnClick={false}
|
|
onClose={onClose}
|
|
anchor="bottom"
|
|
offset={20}
|
|
className="z-10"
|
|
>
|
|
<div className="p-3 min-w-[200px] max-w-[300px]">
|
|
{title && <h3 className="text-sm font-medium border-b pb-1 mb-2 pr-4">{title}</h3>}
|
|
<button
|
|
className="absolute top-2 right-2 p-1 rounded-full hover:bg-gray-100"
|
|
onClick={onClose}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
{children}
|
|
</div>
|
|
</Popup>
|
|
)
|
|
}
|
|
|
|
// Function to fetch district details - this would typically be implemented
|
|
// to fetch more detailed information about a district
|
|
const getDistrictDetails = async (districtId: string, year?: string, month?: string) => {
|
|
// This would be an API call to get district details
|
|
// For now, we'll return mock data
|
|
return {
|
|
category_breakdown: [
|
|
{ category: "Theft", count: 12 },
|
|
{ category: "Assault", count: 5 },
|
|
{ category: "Vandalism", count: 8 }
|
|
]
|
|
};
|
|
};
|
|
|
|
export function DistrictPopup({
|
|
longitude,
|
|
latitude,
|
|
onClose,
|
|
district,
|
|
year,
|
|
month
|
|
}: {
|
|
longitude: number
|
|
latitude: number
|
|
onClose: () => void
|
|
district: DistrictFeature
|
|
year?: string
|
|
month?: string
|
|
}) {
|
|
const { data: districtDetails, isLoading } = useQuery({
|
|
queryKey: ['district-details', district.id, year, month],
|
|
queryFn: () => getDistrictDetails(district.id, year, month),
|
|
enabled: !!district.id
|
|
});
|
|
|
|
const rateInfo = getCrimeRateInfo(district.level);
|
|
|
|
return (
|
|
<MapPopup
|
|
longitude={longitude}
|
|
latitude={latitude}
|
|
onClose={onClose}
|
|
title="District Information"
|
|
>
|
|
<div className="space-y-3">
|
|
<h3 className="font-medium text-base">{district.name}</h3>
|
|
|
|
{district.number_of_crime !== undefined && (
|
|
<div className="text-sm text-gray-600">
|
|
<span className="font-medium">Total Incidents:</span> {district.number_of_crime}
|
|
</div>
|
|
)}
|
|
|
|
{district.level && (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-medium">Crime Rate:</span>
|
|
<span className={`inline-flex items-center rounded-full ${rateInfo.color} px-2.5 py-0.5 text-xs font-medium`}>
|
|
{rateInfo.text}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{year && (
|
|
<div className="text-xs text-gray-500">
|
|
<span className="font-medium">Year:</span> {year}
|
|
{month && <>, Month: {month}</>}
|
|
</div>
|
|
)}
|
|
|
|
{isLoading ? (
|
|
<div className="h-20 flex items-center justify-center">
|
|
<Skeleton className="h-16 w-full" />
|
|
</div>
|
|
) : districtDetails?.category_breakdown && districtDetails.category_breakdown.length > 0 ? (
|
|
<div className="mt-2">
|
|
<h4 className="text-sm font-medium mb-1">Crime Categories:</h4>
|
|
<div className="space-y-1">
|
|
{districtDetails.category_breakdown.map((cat, idx) => (
|
|
<div key={idx} className="flex justify-between text-xs">
|
|
<span>{cat.category}</span>
|
|
<span className="font-medium">{cat.count}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="text-xs text-gray-500">
|
|
<span className="font-medium">District ID:</span> {district.id}
|
|
</div>
|
|
</div>
|
|
</MapPopup>
|
|
)
|
|
}
|
|
|
|
export function CrimePopup({
|
|
longitude,
|
|
latitude,
|
|
onClose,
|
|
crime
|
|
}: {
|
|
longitude: number
|
|
latitude: number
|
|
onClose: () => void
|
|
crime: CrimeIncident
|
|
}) {
|
|
return (
|
|
<MapPopup
|
|
longitude={longitude}
|
|
latitude={latitude}
|
|
onClose={onClose}
|
|
title="Crime Incident"
|
|
>
|
|
<div className="space-y-2">
|
|
<div className="text-sm">
|
|
<span className="font-medium">Type:</span> {crime.category}
|
|
</div>
|
|
{crime.timestamp && (
|
|
<div className="text-sm">
|
|
<span className="font-medium">Date:</span> {new Date(crime.timestamp).toLocaleDateString()}
|
|
</div>
|
|
)}
|
|
{crime.description && (
|
|
<div className="text-sm">
|
|
<span className="font-medium">Description:</span>
|
|
<p className="text-xs mt-1">{crime.description}</p>
|
|
</div>
|
|
)}
|
|
<div className="text-xs text-gray-500 mt-2">
|
|
<span className="font-medium">ID:</span> {crime.id}
|
|
</div>
|
|
</div>
|
|
</MapPopup>
|
|
)
|
|
}
|