MIF_E31221222/sigap-website/app/_components/map/pop-up/district-popup.tsx

343 lines
18 KiB
TypeScript

"use client"
import { useState, useMemo, useEffect } from "react"
import { Popup } from "react-map-gl/mapbox"
import { Badge } from "@/app/_components/ui/badge"
import { Card } from "@/app/_components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/tabs"
import { Button } from "@/app/_components/ui/button"
import { getMonthName } from "@/app/_utils/common"
import { BarChart, Users, Home, AlertTriangle, ChevronRight, Building, Calendar, X } from 'lucide-react'
import type { DistrictFeature } from "../layers/district-layer"
// Helper function to format numbers
function formatNumber(num?: number): string {
if (num === undefined || num === null) return "N/A"
if (num >= 1_000_000) {
return (num / 1_000_000).toFixed(1) + "M"
}
if (num >= 1_000) {
return (num / 1_000).toFixed(1) + "K"
}
return num.toLocaleString()
}
interface DistrictPopupProps {
longitude: number
latitude: number
onClose: () => void
district: DistrictFeature
year?: string
month?: string
filterCategory?: string | "all"
}
export default function DistrictPopup({
longitude,
latitude,
onClose,
district,
year,
month,
filterCategory = "all",
}: DistrictPopupProps) {
const [activeTab, setActiveTab] = useState("overview")
// Add debug log when the component is rendered
useEffect(() => {
console.log("DistrictPopup mounted:", {
district: district.name,
coords: [longitude, latitude],
year,
month
});
}, [district, longitude, latitude, year, month]);
// Extract all crime incidents from the district data and apply filtering if needed
const allCrimeIncidents = useMemo(() => {
// Check if there are crime incidents in the district object
if (!Array.isArray(district.crime_incidents)) {
console.warn("No crime incidents array found in district data")
return []
}
// Return all incidents if filterCategory is 'all'
if (filterCategory === "all") {
return district.crime_incidents
}
// Otherwise, filter by category
return district.crime_incidents.filter((incident) => incident.category === filterCategory)
}, [district, filterCategory])
const getCrimeRateBadge = (level?: string) => {
switch (level) {
case "low":
return <Badge className="bg-emerald-600 text-white">Low</Badge>
case "medium":
return <Badge className="bg-amber-500 text-white">Medium</Badge>
case "high":
return <Badge className="bg-rose-600 text-white">High</Badge>
case "critical":
return <Badge className="bg-red-700 text-white">Critical</Badge>
default:
return <Badge className="bg-slate-600">Unknown</Badge>
}
}
// Format a time period string from year and month
const getTimePeriod = () => {
if (year && month && month !== "all") {
return `${getMonthName(Number(month))} ${year}`
}
return year || "All time"
}
return (
<Popup
longitude={longitude}
latitude={latitude}
closeButton={false} // Hide default close button
closeOnClick={false}
onClose={onClose}
anchor="top"
maxWidth="300px"
className="district-popup z-50"
>
<Card className="bg-background p-0 w-full max-w-[300px] shadow-xl border-0 overflow-hidden">
<div className="bg-tertiary text-white p-3 relative">
{/* Custom close button */}
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 h-5 w-5 rounded-full bg-white/20 hover:bg-white/30 text-white"
onClick={onClose}
>
<X className="h-3 w-3" />
<span className="sr-only">Close</span>
</Button>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Building className="h-4 w-4" />
<h3 className="font-bold text-base">{district.name}</h3>
</div>
{getCrimeRateBadge(district.level)}
</div>
{/* <div className="mt-1 text-white/80 text-xs flex items-center gap-2">
<Calendar className="h-3 w-3" />
<span>{getTimePeriod()}</span>
</div> */}
</div>
<div className="grid grid-cols-3 gap-1 p-2 bg-background">
<div className="flex flex-col items-center justify-center p-1.5 bg-accent rounded-lg shadow-sm">
<AlertTriangle className="h-3.5 w-3.5 text-amber-500 mb-0.5" />
<span className="text-base font-bold">{formatNumber(district.number_of_crime || 0)}</span>
<span className="text-[10px] text-muted-foreground">Incidents</span>
</div>
<div className="flex flex-col items-center justify-center p-1.5 bg-accent rounded-lg shadow-sm">
<Users className="h-3.5 w-3.5 text-blue-500 mb-0.5" />
<span className="text-base font-bold">{formatNumber(district.demographics?.population || 0)}</span>
<span className="text-[10px] text-muted-foreground">Population</span>
</div>
<div className="flex flex-col items-center justify-center p-1.5 bg-accent rounded-lg shadow-sm">
<Home className="h-3.5 w-3.5 text-green-500 mb-0.5" />
<span className="text-base font-bold">{formatNumber(district.geographics?.land_area || 0)}</span>
<span className="text-[10px] text-muted-foreground">km²</span>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
{/* Improved tab headers */}
<TabsList className="w-full grid grid-cols-3 h-10 rounded-none bg-background border-b">
<TabsTrigger
value="overview"
className="rounded-none data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:text-primary-foreground dark:data-[state=active]:text-primary-foreground data-[state=active]:shadow-none font-medium text-xs"
>
Overview
</TabsTrigger>
<TabsTrigger
value="demographics"
className="rounded-none data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:text-primary-foreground dark:data-[state=active]:text-primary-foreground data-[state=active]:shadow-none font-medium text-xs"
>
Demographics
</TabsTrigger>
<TabsTrigger
value="crime_incidents"
className="rounded-none data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:text-primary-foreground dark:data-[state=active]:text-primary-foreground data-[state=active]:shadow-none font-medium text-xs"
>
Incidents
</TabsTrigger>
</TabsList>
{/* Tab content with improved section headers */}
<TabsContent value="overview" className="mt-0 p-4">
<div className="text-sm space-y-3">
<div className="flex items-start gap-3">
<div className="bg-amber-100 dark:bg-amber-950/30 p-2 rounded-full">
<AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400" />
</div>
<div>
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Crime Level</p>
<p className="text-muted-foreground text-xs">
This area has a {district.level || "unknown"} level of crime based on incident reports.
</p>
</div>
</div>
{district.geographics && district.geographics.land_area && (
<div className="flex items-start gap-3">
<div className="bg-emerald-100 dark:bg-emerald-950/30 p-2 rounded-full">
<Home className="w-5 h-5 text-emerald-600 dark:text-emerald-400" />
</div>
<div>
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Geography</p>
<p className="text-muted-foreground text-xs">
Land area: {formatNumber(district.geographics.land_area)} km²
</p>
{district.geographics.address && (
<p className="text-muted-foreground text-xs">Address: {district.geographics.address}</p>
)}
</div>
</div>
)}
<div className="flex items-start gap-3">
<div className="bg-blue-100 dark:bg-blue-950/30 p-2 rounded-full">
<Calendar className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Time Period</p>
<p className="text-muted-foreground text-xs">
Data shown for {getTimePeriod()}
{filterCategory !== "all" ? ` (${filterCategory} category)` : ""}
</p>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="demographics" className="mt-0 p-4">
{district.demographics ? (
<div className="text-sm space-y-3">
<div className="flex items-start gap-3">
<div className="bg-blue-100 dark:bg-blue-950/30 p-2 rounded-full">
<Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Population</p>
<p className="text-muted-foreground text-xs">
Total: {formatNumber(district.demographics.population || 0)}
</p>
<p className="text-muted-foreground text-xs">
Density: {formatNumber(district.demographics.population_density || 0)} people/km²
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="bg-red-100 dark:bg-red-950/30 p-2 rounded-full">
<BarChart className="w-5 h-5 text-red-600 dark:text-red-400" />
</div>
<div>
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Unemployment</p>
<p className="text-muted-foreground text-xs">
{formatNumber(district.demographics.number_of_unemployed || 0)} unemployed people
</p>
{district.demographics.population && district.demographics.number_of_unemployed && (
<p className="text-muted-foreground text-xs">
Rate:{" "}
{(
(district.demographics.number_of_unemployed / district.demographics.population) *
100
).toFixed(1)}
%
</p>
)}
</div>
</div>
<div className="flex items-start gap-3">
<div className="bg-purple-100 dark:bg-purple-950/30 p-2 rounded-full">
<AlertTriangle className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<div>
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Crime Rate</p>
{district.number_of_crime && district.demographics.population ? (
<p className="text-muted-foreground text-xs">
{((district.number_of_crime / district.demographics.population) * 10000).toFixed(2)} crime
incidents per 10,000 people
</p>
) : (
<p className="text-muted-foreground text-xs">No data available</p>
)}
</div>
</div>
</div>
) : (
<div className="text-center p-4 text-sm text-muted-foreground">
<Users className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>No demographic data available for this district.</p>
</div>
)}
</TabsContent>
<TabsContent value="crime_incidents" className="mt-0 max-h-[250px] overflow-y-auto">
{allCrimeIncidents && allCrimeIncidents.length > 0 ? (
<div className="divide-y divide-border">
{allCrimeIncidents.map((incident, index) => (
<div
key={incident.id || index}
className="p-3 text-xs hover:bg-slate-50 dark:hover:bg-slate-900/30 transition-colors"
>
<div className="flex justify-between items-center">
<span className="font-medium flex items-center gap-1">
<AlertTriangle className="w-3 h-3 text-amber-500" />
{incident.category || incident.type || "Unknown"}
</span>
<Badge variant="outline" className="text-[10px] h-5">
{incident.status || "unknown"}
</Badge>
</div>
<p className="text-muted-foreground mt-1 truncate">{incident.description || "No description"}</p>
<div className="flex justify-between items-center mt-1">
<p className="text-muted-foreground">
{incident.timestamp ? new Date(incident.timestamp).toLocaleString() : "Unknown date"}
</p>
<ChevronRight className="w-3 h-3 text-muted-foreground" />
</div>
</div>
))}
{district.number_of_crime > allCrimeIncidents.length && (
<div className="p-3 text-xs text-center text-muted-foreground bg-muted/50">
<p>
Showing {allCrimeIncidents.length} of {district.number_of_crime} total incidents
{filterCategory !== "all" ? ` for ${filterCategory} category` : ""}
</p>
</div>
)}
</div>
) : (
<div className="text-center p-4 text-sm text-muted-foreground">
<AlertTriangle className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>
No crime incidents available to display{filterCategory !== "all" ? ` for ${filterCategory}` : ""}.
</p>
<p className="text-xs mt-2">Total reported incidents: {district.number_of_crime || 0}</p>
</div>
)}
</TabsContent>
</Tabs>
</Card>
</Popup>
)
}