refactor: update crime-related interfaces for consistency and clarity across components
This commit is contained in:
parent
2aa2e609f1
commit
a2f51e6837
|
@ -13,27 +13,29 @@ import { ITooltips } from "./tooltips"
|
||||||
import { $Enums } from "@prisma/client"
|
import { $Enums } from "@prisma/client"
|
||||||
|
|
||||||
// Define types based on the crime data structure
|
// Define types based on the crime data structure
|
||||||
interface CrimeIncident {
|
interface ICrimeIncident {
|
||||||
id: string
|
id: string
|
||||||
timestamp: Date
|
timestamp: Date
|
||||||
description: string
|
description: string
|
||||||
status: string
|
status: string
|
||||||
latitude?: number
|
locations: {
|
||||||
longitude?: number
|
address: string;
|
||||||
address?: string
|
longitude: number;
|
||||||
crime_categories?: {
|
latitude: number;
|
||||||
|
},
|
||||||
|
crime_categories: {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Crime {
|
interface ICrime {
|
||||||
id: string
|
id: string
|
||||||
district_id: string
|
district_id: string
|
||||||
month: number
|
month: number
|
||||||
year: number
|
year: number
|
||||||
crime_incidents: CrimeIncident[]
|
crime_incidents: ICrimeIncident[]
|
||||||
district: {
|
districts: {
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,21 +73,21 @@ const ACTIONS = [
|
||||||
placeholder: "Enter crime description",
|
placeholder: "Enter crime description",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "address",
|
id: "locations.address",
|
||||||
label: "Search by Address",
|
label: "Search by locations.Address",
|
||||||
icon: <FolderOpen className="h-4 w-4 text-amber-500" />,
|
icon: <FolderOpen className="h-4 w-4 text-amber-500" />,
|
||||||
description: "e.g., Jalan Sudirman",
|
description: "e.g., Jalan Sudirman",
|
||||||
category: "Search",
|
category: "Search",
|
||||||
prefix: "",
|
prefix: "",
|
||||||
regex: /.+/,
|
regex: /.+/,
|
||||||
placeholder: "Enter location or address",
|
placeholder: "Enter location or locations.address",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
interface SearchTooltipProps {
|
interface SearchTooltipProps {
|
||||||
onControlChange?: (controlId: ITooltips) => void
|
onControlChange?: (controlId: ITooltips) => void
|
||||||
activeControl?: string
|
activeControl?: string
|
||||||
crimes?: Crime[]
|
crimes?: ICrime[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SearchTooltip({ onControlChange, activeControl, crimes = [] }: SearchTooltipProps) {
|
export default function SearchTooltip({ onControlChange, activeControl, crimes = [] }: SearchTooltipProps) {
|
||||||
|
@ -93,9 +95,9 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [selectedSearchType, setSelectedSearchType] = useState<string | null>(null)
|
const [selectedSearchType, setSelectedSearchType] = useState<string | null>(null)
|
||||||
const [searchValue, setSearchValue] = useState("")
|
const [searchValue, setSearchValue] = useState("")
|
||||||
const [suggestions, setSuggestions] = useState<CrimeIncident[]>([])
|
const [suggestions, setSuggestions] = useState<ICrimeIncident[]>([])
|
||||||
const [isInputValid, setIsInputValid] = useState(true)
|
const [isInputValid, setIsInputValid] = useState(true)
|
||||||
const [selectedSuggestion, setSelectedSuggestion] = useState<CrimeIncident | null>(null)
|
const [selectedSuggestion, setSelectedSuggestion] = useState<ICrimeIncident | null>(null)
|
||||||
const [showInfoBox, setShowInfoBox] = useState(false)
|
const [showInfoBox, setShowInfoBox] = useState(false)
|
||||||
|
|
||||||
// Limit results to prevent performance issues
|
// Limit results to prevent performance issues
|
||||||
|
@ -105,7 +107,7 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
|
||||||
const allIncidents = crimes.flatMap(crime =>
|
const allIncidents = crimes.flatMap(crime =>
|
||||||
crime.crime_incidents.map(incident => ({
|
crime.crime_incidents.map(incident => ({
|
||||||
...incident,
|
...incident,
|
||||||
district: crime.district?.name || '',
|
district: crime.districts?.name || '',
|
||||||
year: crime.year,
|
year: crime.year,
|
||||||
month: crime.month
|
month: crime.month
|
||||||
}))
|
}))
|
||||||
|
@ -129,11 +131,11 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
|
||||||
setIsInputValid(true);
|
setIsInputValid(true);
|
||||||
|
|
||||||
// Initial suggestions based on the selected search type
|
// Initial suggestions based on the selected search type
|
||||||
let initialSuggestions: CrimeIncident[] = [];
|
let initialSuggestions: ICrimeIncident[] = [];
|
||||||
|
|
||||||
if (actionId === 'incident_id') {
|
if (actionId === 'incident_id') {
|
||||||
initialSuggestions = allIncidents.slice(0, MAX_RESULTS); // Limit to 50 results initially
|
initialSuggestions = allIncidents.slice(0, MAX_RESULTS); // Limit to 50 results initially
|
||||||
} else if (actionId === 'description' || actionId === 'address') {
|
} else if (actionId === 'description' || actionId === 'locations.address') {
|
||||||
initialSuggestions = allIncidents.slice(0, MAX_RESULTS);
|
initialSuggestions = allIncidents.slice(0, MAX_RESULTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,8 +156,8 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter suggestions based on search type and search text
|
// Filter suggestions based on search type and search text
|
||||||
const filterSuggestions = (searchType: string, searchText: string): CrimeIncident[] => {
|
const filterSuggestions = (searchType: string, searchText: string): ICrimeIncident[] => {
|
||||||
let filtered: CrimeIncident[] = [];
|
let filtered: ICrimeIncident[] = [];
|
||||||
|
|
||||||
if (searchType === 'incident_id') {
|
if (searchType === 'incident_id') {
|
||||||
if (!searchText || searchText === 'CI-') {
|
if (!searchText || searchText === 'CI-') {
|
||||||
|
@ -175,26 +177,26 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
|
||||||
).slice(0, MAX_RESULTS);
|
).slice(0, MAX_RESULTS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (searchType === 'address') {
|
else if (searchType === 'locations.address') {
|
||||||
if (!searchText) {
|
if (!searchText) {
|
||||||
filtered = allIncidents.slice(0, MAX_RESULTS);
|
filtered = allIncidents.slice(0, MAX_RESULTS);
|
||||||
} else {
|
} else {
|
||||||
filtered = allIncidents.filter(item =>
|
filtered = allIncidents.filter(item =>
|
||||||
item.address && item.address.toLowerCase().includes(searchText.toLowerCase())
|
item.locations.address && item.locations.address.toLowerCase().includes(searchText.toLowerCase())
|
||||||
).slice(0, MAX_RESULTS);
|
).slice(0, MAX_RESULTS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (searchType === 'coordinates') {
|
else if (searchType === 'coordinates') {
|
||||||
if (!searchText) {
|
if (!searchText) {
|
||||||
filtered = allIncidents.filter(item => item.latitude !== undefined && item.longitude !== undefined)
|
filtered = allIncidents.filter(item => item.locations.latitude !== undefined && item.locations.longitude !== undefined)
|
||||||
.slice(0, MAX_RESULTS);
|
.slice(0, MAX_RESULTS);
|
||||||
} else {
|
} else {
|
||||||
// For coordinates, we'd typically do a proximity search
|
// For coordinates, we'd typically do a proximity search
|
||||||
// This is a simple implementation for demo purposes
|
// This is a simple implementation for demo purposes
|
||||||
filtered = allIncidents.filter(item =>
|
filtered = allIncidents.filter(item =>
|
||||||
item.latitude !== undefined &&
|
item.locations.latitude !== undefined &&
|
||||||
item.longitude !== undefined &&
|
item.locations.longitude !== undefined &&
|
||||||
`${item.latitude}, ${item.longitude}`.includes(searchText)
|
`${item.locations.latitude}, ${item.locations.longitude}`.includes(searchText)
|
||||||
).slice(0, MAX_RESULTS);
|
).slice(0, MAX_RESULTS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,7 +246,7 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSuggestionSelect = (incident: CrimeIncident) => {
|
const handleSuggestionSelect = (incident: ICrimeIncident) => {
|
||||||
setSearchValue(incident.id);
|
setSearchValue(incident.id);
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setSelectedSuggestion(incident);
|
setSelectedSuggestion(incident);
|
||||||
|
@ -252,12 +254,12 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFlyToIncident = () => {
|
const handleFlyToIncident = () => {
|
||||||
if (!selectedSuggestion || !selectedSuggestion.latitude || !selectedSuggestion.longitude) return;
|
if (!selectedSuggestion || !selectedSuggestion.locations.latitude || !selectedSuggestion.locations.longitude) return;
|
||||||
|
|
||||||
const flyToEvent = new CustomEvent('fly_to_incident', {
|
const flyToEvent = new CustomEvent('incident_click', {
|
||||||
detail: {
|
detail: {
|
||||||
longitude: selectedSuggestion.longitude,
|
longitude: selectedSuggestion.locations.longitude,
|
||||||
latitude: selectedSuggestion.latitude,
|
latitude: selectedSuggestion.locations.latitude,
|
||||||
id: selectedSuggestion.id,
|
id: selectedSuggestion.id,
|
||||||
zoom: 15,
|
zoom: 15,
|
||||||
description: selectedSuggestion.description,
|
description: selectedSuggestion.description,
|
||||||
|
@ -294,7 +296,7 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format date for display
|
// Format date for display
|
||||||
const formatIncidentDate = (incident: CrimeIncident) => {
|
const formatIncidentDate = (incident: ICrimeIncident) => {
|
||||||
try {
|
try {
|
||||||
if (incident.timestamp) {
|
if (incident.timestamp) {
|
||||||
return format(new Date(incident.timestamp), 'PPP p');
|
return format(new Date(incident.timestamp), 'PPP p');
|
||||||
|
@ -410,11 +412,11 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
|
||||||
</span>
|
</span>
|
||||||
) : selectedSearchType === 'coordinates' ? (
|
) : selectedSearchType === 'coordinates' ? (
|
||||||
<span className="text-muted-foreground text-sm truncate max-w-[300px]">
|
<span className="text-muted-foreground text-sm truncate max-w-[300px]">
|
||||||
{incident.latitude}, {incident.longitude} - {incident.description}
|
{incident.locations.latitude}, {incident.locations.longitude} - {incident.description}
|
||||||
</span>
|
</span>
|
||||||
) : selectedSearchType === 'address' ? (
|
) : selectedSearchType === 'locations.address' ? (
|
||||||
<span className="text-muted-foreground text-sm truncate max-w-[300px]">
|
<span className="text-muted-foreground text-sm truncate max-w-[300px]">
|
||||||
{incident.address || 'N/A'}
|
{incident.locations.address || 'N/A'}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground text-sm truncate max-w-[300px]">
|
<span className="text-muted-foreground text-sm truncate max-w-[300px]">
|
||||||
|
@ -485,10 +487,10 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedSuggestion.address && (
|
{selectedSuggestion.locations.address && (
|
||||||
<div className="grid grid-cols-[20px_1fr] gap-2 items-start">
|
<div className="grid grid-cols-[20px_1fr] gap-2 items-start">
|
||||||
<MapPin className="h-4 w-4 mt-1 text-muted-foreground" />
|
<MapPin className="h-4 w-4 mt-1 text-muted-foreground" />
|
||||||
<p className="text-sm">{selectedSuggestion.address}</p>
|
<p className="text-sm">{selectedSuggestion.locations.address}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -515,7 +517,7 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleFlyToIncident}
|
onClick={handleFlyToIncident}
|
||||||
disabled={!selectedSuggestion.latitude || !selectedSuggestion.longitude}
|
disabled={!selectedSuggestion.locations.latitude || !selectedSuggestion.locations.longitude}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span>Fly to Incident</span>
|
<span>Fly to Incident</span>
|
||||||
|
|
|
@ -23,14 +23,15 @@ import CrimeSidebar from "./controls/left/sidebar/map-sidebar"
|
||||||
import Tooltips from "./controls/top/tooltips"
|
import Tooltips from "./controls/top/tooltips"
|
||||||
|
|
||||||
// Updated CrimeIncident type to match the structure in crime_incidents
|
// Updated CrimeIncident type to match the structure in crime_incidents
|
||||||
interface CrimeIncident {
|
interface ICrimeIncident {
|
||||||
id: string
|
id: string
|
||||||
timestamp: Date
|
district?: string
|
||||||
description: string
|
|
||||||
status: string
|
|
||||||
category?: string
|
category?: string
|
||||||
type?: string
|
type_category?: string | null
|
||||||
address?: string
|
description?: string
|
||||||
|
status: string
|
||||||
|
address?: string | null
|
||||||
|
timestamp?: Date
|
||||||
latitude?: number
|
latitude?: number
|
||||||
longitude?: number
|
longitude?: number
|
||||||
}
|
}
|
||||||
|
@ -39,7 +40,7 @@ export default function CrimeMap() {
|
||||||
// State for sidebar
|
// State for sidebar
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
|
||||||
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
|
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
|
||||||
const [selectedIncident, setSelectedIncident] = useState<CrimeIncident | null>(null)
|
const [selectedIncident, setSelectedIncident] = useState<ICrimeIncident | null>(null)
|
||||||
const [showLegend, setShowLegend] = useState<boolean>(true)
|
const [showLegend, setShowLegend] = useState<boolean>(true)
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string | "all">("all")
|
const [selectedCategory, setSelectedCategory] = useState<string | "all">("all")
|
||||||
const [selectedYear, setSelectedYear] = useState<number>(2024)
|
const [selectedYear, setSelectedYear] = useState<number>(2024)
|
||||||
|
@ -113,22 +114,55 @@ export default function CrimeMap() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleIncidentClickEvent = (e: CustomEvent) => {
|
const handleIncidentClickEvent = (e: CustomEvent) => {
|
||||||
console.log("Received incident_click event:", e.detail);
|
console.log("Received incident_click event:", e.detail);
|
||||||
if (e.detail) {
|
if (!e.detail || !e.detail.id) {
|
||||||
if (!e.detail.longitude || !e.detail.latitude) {
|
console.error("Invalid incident data in event:", e.detail);
|
||||||
console.error("Invalid incident coordinates in event:", e.detail);
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When an incident is clicked, clear any selected district
|
|
||||||
setSelectedDistrict(null);
|
|
||||||
|
|
||||||
// Set the selected incident
|
|
||||||
setSelectedIncident(e.detail);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Find the incident in filtered crimes data using the id from the event
|
||||||
|
let foundIncident: ICrimeIncident | undefined;
|
||||||
|
|
||||||
|
// Search through all crimes and their incidents to find matching incident
|
||||||
|
filteredCrimes.forEach(crime => {
|
||||||
|
crime.crime_incidents.forEach(incident => {
|
||||||
|
if (incident.id === e.detail.id) {
|
||||||
|
// Map the found incident to ICrimeIncident type
|
||||||
|
foundIncident = {
|
||||||
|
id: incident.id,
|
||||||
|
district: crime.districts.name,
|
||||||
|
description: incident.description,
|
||||||
|
status: incident.status || "unknown",
|
||||||
|
timestamp: incident.timestamp,
|
||||||
|
category: incident.crime_categories.name,
|
||||||
|
type_category: incident.crime_categories.type,
|
||||||
|
address: incident.locations.address,
|
||||||
|
latitude: incident.locations.latitude,
|
||||||
|
longitude: incident.locations.longitude,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!foundIncident) {
|
||||||
|
console.error("Could not find incident with ID:", e.detail.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the coordinates
|
||||||
|
if (!foundIncident.latitude || !foundIncident.longitude) {
|
||||||
|
console.error("Invalid incident coordinates:", foundIncident);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When an incident is clicked, clear any selected district
|
||||||
|
setSelectedDistrict(null);
|
||||||
|
|
||||||
|
// Set the selected incident
|
||||||
|
setSelectedIncident(foundIncident);
|
||||||
|
};
|
||||||
|
|
||||||
// Add event listener to the map container and document
|
// Add event listener to the map container and document
|
||||||
const mapContainer = mapContainerRef.current
|
const mapContainer = mapContainerRef.current;
|
||||||
|
|
||||||
// Clean up previous listeners to prevent duplicates
|
// Clean up previous listeners to prevent duplicates
|
||||||
document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
|
document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
|
||||||
|
@ -145,67 +179,11 @@ export default function CrimeMap() {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
|
document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
|
||||||
|
|
||||||
if (mapContainer) {
|
if (mapContainer) {
|
||||||
mapContainer.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
|
mapContainer.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, []);
|
}, [filteredCrimes]);
|
||||||
|
|
||||||
// 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 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) => {
|
||||||
|
@ -310,7 +288,7 @@ export default function CrimeMap() {
|
||||||
longitude={selectedIncident.longitude}
|
longitude={selectedIncident.longitude}
|
||||||
latitude={selectedIncident.latitude}
|
latitude={selectedIncident.latitude}
|
||||||
onClose={() => setSelectedIncident(null)}
|
onClose={() => setSelectedIncident(null)}
|
||||||
crime={selectedIncident}
|
incident={selectedIncident}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -7,25 +7,25 @@ import { Separator } from "@/app/_components/ui/separator"
|
||||||
import { Button } from "@/app/_components/ui/button"
|
import { Button } from "@/app/_components/ui/button"
|
||||||
import { MapPin, AlertTriangle, Calendar, Clock, Tag, Bookmark, FileText, Navigation, X } from "lucide-react"
|
import { MapPin, AlertTriangle, Calendar, Clock, Tag, Bookmark, FileText, Navigation, X } from "lucide-react"
|
||||||
|
|
||||||
interface CrimePopupProps {
|
interface IncidentPopupProps {
|
||||||
longitude: number
|
longitude: number
|
||||||
latitude: number
|
latitude: number
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
crime: {
|
incident: {
|
||||||
id: string
|
id: string
|
||||||
district?: string
|
district?: string
|
||||||
category?: string
|
category?: string
|
||||||
type?: string
|
type_category?: string | null
|
||||||
description?: string
|
description?: string
|
||||||
status?: string
|
status?: string
|
||||||
address?: string
|
address?: string | null
|
||||||
timestamp?: Date
|
timestamp?: Date
|
||||||
latitude?: number
|
latitude?: number
|
||||||
longitude?: number
|
longitude?: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CrimePopup({ longitude, latitude, onClose, crime }: CrimePopupProps) {
|
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()
|
||||||
|
@ -80,10 +80,10 @@ export default function CrimePopup({ longitude, latitude, onClose, crime }: Crim
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
anchor="top"
|
anchor="top"
|
||||||
maxWidth="320px"
|
maxWidth="320px"
|
||||||
className="crime-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(crime.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 */}
|
||||||
|
@ -100,16 +100,16 @@ export default function CrimePopup({ longitude, latitude, onClose, crime }: Crim
|
||||||
<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" />
|
||||||
{crime.category || "Unknown Incident"}
|
{incident.category || "Unknown Incident"}
|
||||||
</h3>
|
</h3>
|
||||||
{getStatusBadge(crime.status)}
|
{getStatusBadge(incident.status)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{crime.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" />
|
||||||
{crime.description}
|
{incident.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -118,51 +118,51 @@ export default function CrimePopup({ longitude, latitude, onClose, crime }: Crim
|
||||||
|
|
||||||
{/* 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">
|
||||||
{crime.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">{crime.district}</span>
|
<span className="font-medium">{incident.district}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{crime.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">{crime.address}</span>
|
<span className="font-medium">{incident.address}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{crime.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(crime.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(crime.timestamp)}</span>
|
<span className="font-medium">{formatTime(incident.timestamp)}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{crime.type && (
|
{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">{crime.type}</span>
|
<span className="font-medium">{incident.type_category}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -173,7 +173,7 @@ export default function CrimePopup({ longitude, latitude, onClose, crime }: Crim
|
||||||
<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: {crime.id}</p>
|
<p className="text-xs text-muted-foreground mt-1">ID: {incident.id}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
Loading…
Reference in New Issue