MIF_E31221222/sigap-website/app/_components/map/crime-map.tsx

276 lines
12 KiB
TypeScript

"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/app/_components/ui/card"
import { Skeleton } from "@/app/_components/ui/skeleton"
import DistrictLayer, { type DistrictFeature, ICrimeData } from "./layers/district-layer"
import MapView from "./map"
import { Button } from "@/app/_components/ui/button"
import { AlertCircle } from "lucide-react"
import { getMonthName } from "@/app/_utils/common"
import { useRef, useState, useCallback, useMemo, useEffect } from "react"
import { useFullscreen } from "@/app/_hooks/use-fullscreen"
import { Overlay } from "./overlay"
import MapLegend from "./controls/map-legend"
import { usePrefetchedCrimeData } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_hooks/use-prefetch-crimes"
import { useGetCrimeCategories } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries"
import { ITopTooltipsMapId } from "./controls/map-tooltips"
import MapSelectors from "./controls/map-selector"
import TopNavigation from "./controls/map-navigations"
import CrimeSidebar from "./sidebar/map-sidebar"
import SidebarToggle from "./sidebar/sidebar-toggle"
import { cn } from "@/app/_lib/utils"
import CrimePopup from "./pop-up/crime-popup"
// Updated CrimeIncident type to match the structure in crime_incidents
export interface CrimeIncident {
id: string
timestamp: Date
description: string
status: string
category?: string
type?: string
address?: string
latitude?: number
longitude?: number
}
export default function CrimeMap() {
// State for sidebar
const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
const [selectedIncident, setSelectedIncident] = useState<CrimeIncident | null>(null)
const [showLegend, setShowLegend] = useState<boolean>(true)
const [selectedCategory, setSelectedCategory] = useState<string | "all">("all")
const [activeControl, setActiveControl] = useState<ITopTooltipsMapId>("incidents")
const mapContainerRef = useRef<HTMLDivElement>(null)
// Use the custom fullscreen hook
const { isFullscreen } = useFullscreen(mapContainerRef)
// Toggle sidebar function
const toggleSidebar = useCallback(() => {
setSidebarCollapsed(!sidebarCollapsed)
}, [sidebarCollapsed])
// Use our new prefetched data hook
const {
availableYears,
isYearsLoading,
crimes,
isCrimesLoading,
crimesError,
setSelectedYear,
setSelectedMonth,
selectedYear,
selectedMonth,
} = usePrefetchedCrimeData()
// Extract all unique categories
const { data: categoriesData, isLoading: isCategoryLoading } = useGetCrimeCategories()
// Transform categories data to string array
const categories = useMemo(() =>
categoriesData ? categoriesData.map(category => category.name) : []
, [categoriesData])
// Filter incidents based on selected category
const filteredCrimes = useMemo(() => {
if (!crimes) return []
if (selectedCategory === "all") return crimes
return crimes.map((crime: ICrimeData) => {
const filteredIncidents = crime.crime_incidents.filter(
incident => incident.crime_categories.name === selectedCategory
)
return {
...crime,
crime_incidents: filteredIncidents,
number_of_crime: filteredIncidents.length
}
})
}, [crimes, selectedCategory])
// Extract all incidents from all districts for marker display
const allIncidents = useMemo(() => {
if (!filteredCrimes) return []
return filteredCrimes.flatMap((crime: ICrimeData) =>
crime.crime_incidents.map((incident) => ({
id: incident.id,
timestamp: incident.timestamp,
description: incident.description,
status: incident.status,
category: incident.crime_categories.name,
type: incident.crime_categories.type,
address: incident.locations.address,
latitude: incident.locations.latitude,
longitude: incident.locations.longitude,
}))
)
}, [filteredCrimes])
// Handle district click
const handleDistrictClick = (feature: DistrictFeature) => {
setSelectedDistrict(feature)
}
// Handle incident marker click
const handleIncidentClick = (incident: CrimeIncident) => {
if (!incident.longitude || !incident.latitude) {
console.error("Invalid incident coordinates:", incident);
return;
}
setSelectedIncident(incident);
}
// Set up event listener for incident clicks from the district layer
useEffect(() => {
const handleIncidentClick = (e: CustomEvent) => {
// console.log("Received incident_click event:", e.detail)
if (e.detail) {
setSelectedIncident(e.detail)
}
}
// Add event listener to the map container and document
const mapContainer = mapContainerRef.current
// Listen on both the container and document to ensure we catch the event
document.addEventListener('incident_click', handleIncidentClick as EventListener)
if (mapContainer) {
mapContainer.addEventListener('incident_click', handleIncidentClick as EventListener)
}
return () => {
document.removeEventListener('incident_click', handleIncidentClick as EventListener)
if (mapContainer) {
mapContainer.removeEventListener('incident_click', handleIncidentClick as EventListener)
}
}
}, [])
// Reset filters
const resetFilters = useCallback(() => {
setSelectedYear(2024)
setSelectedMonth("all")
setSelectedCategory("all")
}, [setSelectedYear, setSelectedMonth])
// Determine the title based on filters
const getMapTitle = () => {
let title = `${selectedYear}`
if (selectedMonth !== "all") {
title += ` - ${getMonthName(Number(selectedMonth))}`
}
if (selectedCategory !== "all") {
title += ` - ${selectedCategory}`
}
return title
}
return (
<Card className="w-full p-0 border-none shadow-none h-96">
<CardHeader className="flex flex-row pb-2 pt-0 px-0 items-center justify-between">
<CardTitle>Crime Map {getMapTitle()}</CardTitle>
<MapSelectors
availableYears={availableYears || []}
selectedYear={selectedYear}
setSelectedYear={setSelectedYear}
selectedMonth={selectedMonth}
setSelectedMonth={setSelectedMonth}
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
categories={categories}
isYearsLoading={isYearsLoading}
isCategoryLoading={isCategoryLoading}
/>
</CardHeader>
<CardContent className="p-0">
{isCrimesLoading ? (
<div className="flex items-center justify-center h-96">
<Skeleton className="h-full w-full rounded-md" />
</div>
) : crimesError ? (
<div className="flex flex-col items-center justify-center h-96 gap-4">
<AlertCircle className="h-10 w-10 text-destructive" />
<p className="text-center">Failed to load crime data. Please try again later.</p>
<Button onClick={() => window.location.reload()}>Retry</Button>
</div>
) : (
<div className="relative h-[600px]" ref={mapContainerRef}>
<div className={cn(
"transition-all duration-300 ease-in-out",
!sidebarCollapsed && isFullscreen && "ml-[400px]"
)}>
<MapView mapStyle="mapbox://styles/mapbox/dark-v11" className="h-[600px] w-full rounded-md">
{/* District Layer with crime data */}
<DistrictLayer
onClick={handleDistrictClick}
crimes={filteredCrimes || []}
year={selectedYear.toString()}
month={selectedMonth.toString()}
filterCategory={selectedCategory}
/>
{/* Popup for selected incident */}
{selectedIncident && selectedIncident.longitude !== undefined && selectedIncident.latitude !== undefined && (
<>
{console.log("Rendering CrimePopup with:", selectedIncident)}
<CrimePopup
longitude={selectedIncident.longitude}
latitude={selectedIncident.latitude}
onClose={() => setSelectedIncident(null)}
crime={{
...selectedIncident,
latitude: selectedIncident.latitude,
longitude: selectedIncident.longitude
}}
/>
</>
)}
{/* Components that are only visible in fullscreen mode */}
{isFullscreen && (
<>
<Overlay position="top" className="m-0 bg-transparent shadow-none p-0 border-none">
<div className="flex justify-center">
<TopNavigation
activeControl={activeControl}
onControlChange={setActiveControl}
selectedYear={selectedYear}
setSelectedYear={setSelectedYear}
selectedMonth={selectedMonth}
setSelectedMonth={setSelectedMonth}
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
availableYears={availableYears || []}
categories={categories}
/>
</div>
</Overlay>
{/* Pass selectedCategory, selectedYear, and selectedMonth to the sidebar */}
<CrimeSidebar
defaultCollapsed={sidebarCollapsed}
selectedCategory={selectedCategory}
selectedYear={selectedYear}
selectedMonth={selectedMonth}
/>
<MapLegend position="bottom-right" />
</>
)}
</MapView>
</div>
</div>
)}
</CardContent>
</Card>
)
}