185 lines
8.9 KiB
TypeScript
185 lines
8.9 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 } from "./layers/district-layer"
|
|
import MapView from "./map"
|
|
import { Button } from "@/app/_components/ui/button"
|
|
import { AlertCircle, FilterX } from "lucide-react"
|
|
import { getMonthName } from "@/app/_utils/common"
|
|
import { useCrimeMapHandler } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_handlers/crime-map-handlers"
|
|
import { useRef, useState } from "react"
|
|
|
|
import { CrimePopup } from "./pop-up"
|
|
import type { CrimeIncident } from "./markers/crime-marker"
|
|
import YearSelector from "./controls/year-selector"
|
|
import MonthSelector from "./controls/month-selector"
|
|
import { useFullscreen } from "@/app/_hooks/use-fullscreen"
|
|
import { Overlay } from "./overlay"
|
|
import { useGetAvailableYears, useGetCrimeByYearAndMonth } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries"
|
|
import MapLegend from "./controls/map-legend"
|
|
|
|
export default function CrimeMap() {
|
|
// Set default year to 2024 instead of "all"
|
|
const [selectedYear, setSelectedYear] = useState<number>(2024)
|
|
const [selectedMonth, setSelectedMonth] = useState<number | "all">("all")
|
|
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
|
|
const [selectedIncident, setSelectedIncident] = useState<CrimeIncident | null>(null)
|
|
const [showLegend, setShowLegend] = useState<boolean>(true)
|
|
|
|
const mapContainerRef = useRef<HTMLDivElement>(null)
|
|
|
|
// Use the custom fullscreen hook
|
|
const { isFullscreen } = useFullscreen(mapContainerRef)
|
|
|
|
const { data: availableYears, isLoading: isYearsLoading, error: yearsError } = useGetAvailableYears()
|
|
|
|
const { data: crimes, isLoading: isCrimesLoading, error: isCrimesError, refetch: refetchCrimes } = useGetCrimeByYearAndMonth(selectedYear, selectedMonth)
|
|
|
|
// Extract all incidents from all districts for marker display
|
|
const allIncidents =
|
|
crimes?.flatMap((district) =>
|
|
district.incidents.map((incident) => ({
|
|
id: incident.id,
|
|
timestamp: incident.timestamp,
|
|
description: incident.description,
|
|
status: incident.status,
|
|
category: incident.category,
|
|
type: incident.type,
|
|
address: incident.address,
|
|
latitude: incident.latitude,
|
|
longitude: incident.longitude,
|
|
})),
|
|
) || []
|
|
|
|
// Handle district click
|
|
const handleDistrictClick = (feature: DistrictFeature) => {
|
|
setSelectedDistrict(feature)
|
|
}
|
|
|
|
// Handle incident marker click
|
|
const handleIncidentClick = (incident: CrimeIncident) => {
|
|
setSelectedIncident(incident)
|
|
}
|
|
|
|
// Apply filters
|
|
const applyFilters = () => {
|
|
refetchCrimes()
|
|
}
|
|
|
|
// Reset filters
|
|
const resetFilters = () => {
|
|
setSelectedYear(2024)
|
|
setSelectedMonth("all")
|
|
refetchCrimes()
|
|
}
|
|
|
|
// Determine the title based on filters
|
|
const getMapTitle = () => {
|
|
let title = `${selectedYear}`
|
|
if (selectedMonth !== "all") {
|
|
title += ` - ${getMonthName(Number(selectedMonth))}`
|
|
}
|
|
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>
|
|
<div className="flex items-center gap-2">
|
|
{/* Year selector component */}
|
|
<YearSelector
|
|
availableYears={availableYears}
|
|
selectedYear={selectedYear}
|
|
onYearChange={setSelectedYear}
|
|
isLoading={isYearsLoading}
|
|
/>
|
|
|
|
{/* Month selector component */}
|
|
<MonthSelector selectedMonth={selectedMonth} onMonthChange={setSelectedMonth} />
|
|
|
|
<Button variant="ghost" onClick={resetFilters} disabled={selectedYear === 2024 && selectedMonth === "all"}>
|
|
<FilterX className="h-4 w-4" />
|
|
Reset
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
{isCrimesLoading ? (
|
|
<div className="flex items-center justify-center h-96">
|
|
<Skeleton className="h-full w-full rounded-md" />
|
|
</div>
|
|
) : isCrimesError ? (
|
|
<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={() => refetchCrimes()}>Retry</Button>
|
|
</div>
|
|
) : (
|
|
<div className="relative h-[600px]" ref={mapContainerRef}>
|
|
<MapView
|
|
mapStyle="mapbox://styles/mapbox/dark-v11"
|
|
className="h-[600px] w-full rounded-md"
|
|
>
|
|
{/* District Layer with crime data */}
|
|
<DistrictLayer
|
|
onClick={handleDistrictClick}
|
|
crimes={crimes || []}
|
|
year={selectedYear.toString()}
|
|
month={selectedMonth.toString()}
|
|
/>
|
|
|
|
{/* Popup for selected incident */}
|
|
{selectedIncident && (
|
|
<CrimePopup
|
|
longitude={selectedIncident.longitude}
|
|
latitude={selectedIncident.latitude}
|
|
onClose={() => setSelectedIncident(null)}
|
|
crime={selectedIncident}
|
|
/>
|
|
)}
|
|
|
|
{isFullscreen && (
|
|
<>
|
|
<Overlay
|
|
position="top-left"
|
|
className="m-2 bg-transparent shadow-none p-0 border-none"
|
|
>
|
|
<div className="flex items-center gap-2 rounded-md p-0 shadow-lg">
|
|
<YearSelector
|
|
availableYears={availableYears}
|
|
selectedYear={selectedYear}
|
|
onYearChange={setSelectedYear}
|
|
isLoading={isYearsLoading}
|
|
className=" gap-2 text-white"
|
|
/>
|
|
<MonthSelector
|
|
selectedMonth={selectedMonth}
|
|
onMonthChange={setSelectedMonth}
|
|
className=" gap-2 hover:bg-red text-white"
|
|
/>
|
|
<Button
|
|
variant="secondary"
|
|
className=" hover:bg-red text-white"
|
|
onClick={resetFilters}
|
|
disabled={selectedYear === 2024 && selectedMonth === "all"}
|
|
size="sm"
|
|
>
|
|
<FilterX className="h-4 w-4 mr-1" />
|
|
Reset
|
|
</Button>
|
|
</div>
|
|
</Overlay>
|
|
|
|
<MapLegend position="bottom-right" />
|
|
</>
|
|
)}
|
|
</MapView>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|