"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-old" 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 { useGetAvailableYears, useGetCrimeCategories, useGetCrimes } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries" import { ITopTooltipsMapId } from "./controls/map-tooltips" import MapSelectors from "./controls/map-selector" 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" import { $Enums, crime_categories, crime_incidents, crimes, demographics, districts, geographics, locations } from "@prisma/client" import { CrimeTimelapse } from "./controls/crime-timelapse" import TopControl from "./controls/top-controls" // Updated CrimeIncident type to match the structure in crime_incidents 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(null) const [selectedIncident, setSelectedIncident] = useState(null) const [showLegend, setShowLegend] = useState(true) const [selectedCategory, setSelectedCategory] = useState("all") const [selectedYear, setSelectedYear] = useState(2024) const [selectedMonth, setSelectedMonth] = useState("all") const [activeControl, setActiveControl] = useState("incidents") const [yearProgress, setYearProgress] = useState(0) const [isTimelapsePlaying, setisTimelapsePlaying] = useState(false) const [isSearchActive, setIsSearchActive] = useState(false) const mapContainerRef = useRef(null) // Use the custom fullscreen hook const { isFullscreen } = useFullscreen(mapContainerRef) // Get available years const { data: availableYears, isLoading: isYearsLoading, error: yearsError } = useGetAvailableYears() // 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]) // Get all crime data in a single request const { data: crimes, isLoading: isCrimesLoading, error: crimesError } = useGetCrimes() // Filter crimes based on selected year and month const filteredByYearAndMonth = useMemo(() => { if (!crimes) return [] return crimes.filter((crime) => { const yearMatch = crime.year === selectedYear if (selectedMonth === "all") { return yearMatch } else { return yearMatch && crime.month === selectedMonth } }) }, [crimes, selectedYear, selectedMonth]) // Filter incidents based on selected category const filteredCrimes = useMemo(() => { if (!filteredByYearAndMonth) return [] if (selectedCategory === "all") return filteredByYearAndMonth return filteredByYearAndMonth.map((crime) => { const filteredIncidents = crime.crime_incidents.filter( incident => incident.crime_categories.name === selectedCategory ) return { ...crime, crime_incidents: filteredIncidents, number_of_crime: filteredIncidents.length } }) }, [filteredByYearAndMonth, selectedCategory]) // Handle incident marker click // const handleIncidentClick = (incident: CrimeIncident) => { // console.log("Incident clicked directly:", incident); // if (!incident.longitude || !incident.latitude) { // console.error("Invalid incident coordinates:", incident); // return; // } // // When an incident is clicked, clear any selected district // setSelectedDistrict(null); // // Set the selected incident // setSelectedIncident(incident); // } // Set up event listener for incident clicks from the district layer useEffect(() => { const handleIncidentClickEvent = (e: CustomEvent) => { console.log("Received incident_click event:", e.detail); if (e.detail) { if (!e.detail.longitude || !e.detail.latitude) { console.error("Invalid incident coordinates in event:", e.detail); return; } // When an incident is clicked, clear any selected district setSelectedDistrict(null); // Set the selected incident setSelectedIncident(e.detail); } } // Add event listener to the map container and document const mapContainer = mapContainerRef.current // Clean up previous listeners to prevent duplicates document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); if (mapContainer) { mapContainer.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); } // Listen on both the container and document to ensure we catch the event document.addEventListener('incident_click', handleIncidentClickEvent as EventListener); if (mapContainer) { mapContainer.addEventListener('incident_click', handleIncidentClickEvent as EventListener); } return () => { document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); if (mapContainer) { mapContainer.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); } } }, []); // Set up event listener for fly-to-incident events from search useEffect(() => { const handleFlyToIncident = (e: CustomEvent) => { if (!e.detail || !e.detail.longitude || !e.detail.latitude) { console.error("Invalid fly-to coordinates:", e.detail); return; } // Handle the fly-to event by dispatching to the map const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map'); if (mapInstance) { // Clear any existing selections first setSelectedIncident(null); setSelectedDistrict(null); // Create an incident object to highlight const incidentToHighlight: CrimeIncident = { id: e.detail.id as string, latitude: e.detail.latitude as number, longitude: e.detail.longitude as number, timestamp: new Date(), description: e.detail.description || "", status: e.detail.status }; // First fly to the location const flyEvent = new CustomEvent('mapbox_fly_to', { detail: { longitude: e.detail.longitude, latitude: e.detail.latitude, zoom: e.detail.zoom || 15, bearing: 0, pitch: 45, duration: 2000, }, bubbles: true }); mapInstance.dispatchEvent(flyEvent); // After flying, select the incident with a slight delay setTimeout(() => { setSelectedIncident(incidentToHighlight); }, 2000); } } // Add event listener document.addEventListener('fly_to_incident', handleFlyToIncident as EventListener); return () => { document.removeEventListener('fly_to_incident', handleFlyToIncident as EventListener); } }, []); // Handle district click // const handleDistrictClick = (feature: DistrictFeature) => { // console.log("District clicked in CrimeMap:", feature.name); // // When a district is clicked, clear any selected incident // setSelectedIncident(null); // // Set the selected district (for the sidebar or other components) // setSelectedDistrict(feature); // } // Handle year-month timeline change const handleTimelineChange = useCallback((year: number, month: number, progress: number) => { setSelectedYear(year) setSelectedMonth(month) setYearProgress(progress) }, []) // Handle timeline playing state change const handleTimelinePlayingChange = useCallback((playing: boolean) => { setisTimelapsePlaying(playing) // When timelapse starts, close any open popups/details if (playing) { setSelectedIncident(null) setSelectedDistrict(null) } }, []) // Reset filters const resetFilters = useCallback(() => { setSelectedYear(2024) setSelectedMonth("all") setSelectedCategory("all") }, []) // 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 } // Toggle sidebar function const toggleSidebar = useCallback(() => { setSidebarCollapsed(!sidebarCollapsed) }, [sidebarCollapsed]) // Handle control changes from the top controls component const handleControlChange = (controlId: ITopTooltipsMapId) => { setActiveControl(controlId) // Toggle search state when search control is clicked if (controlId === "search") { setIsSearchActive(prev => !prev) } } return ( Crime Map {getMapTitle()} {isCrimesLoading ? (
) : crimesError ? (

Failed to load crime data. Please try again later.

) : (
{/* District Layer with crime data - don't pass onClick if we want internal popup */} {/* Popup for selected incident */} {selectedIncident && selectedIncident.latitude && selectedIncident.longitude && ( <> setSelectedIncident(null)} crime={selectedIncident} /> )} {/* Components that are only visible in fullscreen mode */} {isFullscreen && ( <>
{/* Pass selectedCategory, selectedYear, and selectedMonth to the sidebar */}
)} {isFullscreen && (
)}
)}
) }