123 lines
4.5 KiB
TypeScript
123 lines
4.5 KiB
TypeScript
"use client"
|
|
|
|
import { Popup } from "react-map-gl/mapbox"
|
|
import { X } from 'lucide-react'
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../ui/card"
|
|
import { Button } from "../../ui/button"
|
|
import { Badge } from "../../ui/badge"
|
|
|
|
|
|
interface TimelinePopupProps {
|
|
longitude: number
|
|
latitude: number
|
|
onClose: () => void
|
|
district: {
|
|
id: string
|
|
name: string
|
|
formattedTime: string
|
|
timeDescription: string
|
|
totalIncidents: number
|
|
earliestTime: string
|
|
latestTime: string
|
|
mostFrequentHour: number
|
|
categoryCounts: Record<string, number>
|
|
timeOfDay: string
|
|
}
|
|
}
|
|
|
|
export default function TimelinePopup({
|
|
longitude,
|
|
latitude,
|
|
onClose,
|
|
district,
|
|
}: TimelinePopupProps) {
|
|
// Get top 5 categories
|
|
const topCategories = Object.entries(district.categoryCounts)
|
|
.sort(([, countA], [, countB]) => countB - countA)
|
|
.slice(0, 5)
|
|
|
|
// Get time of day color
|
|
const getTimeOfDayColor = (timeOfDay: string) => {
|
|
switch (timeOfDay) {
|
|
case "morning":
|
|
return "bg-yellow-400 text-black"
|
|
case "afternoon":
|
|
return "bg-orange-500 text-white"
|
|
case "evening":
|
|
return "bg-indigo-600 text-white"
|
|
case "night":
|
|
return "bg-slate-800 text-white"
|
|
default:
|
|
return "bg-green-500 text-white"
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Popup
|
|
longitude={longitude}
|
|
latitude={latitude}
|
|
anchor="bottom"
|
|
closeOnClick={false}
|
|
onClose={onClose}
|
|
className="z-10"
|
|
maxWidth="300px"
|
|
>
|
|
<Card className="border-0 shadow-none">
|
|
<CardHeader className="p-3 pb-2">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-base">{district.name}</CardTitle>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6"
|
|
onClick={onClose}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
<CardDescription className="text-xs">
|
|
Average incident time analysis
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="p-3 pt-0">
|
|
<div className="mb-3">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<div className="text-xl font-bold font-mono">{district.formattedTime}</div>
|
|
<Badge variant="outline" className={`${getTimeOfDayColor(district.timeOfDay)}`}>
|
|
{district.timeDescription}
|
|
</Badge>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
Based on {district.totalIncidents} incidents
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-sm space-y-1 mb-3">
|
|
<div className="flex justify-between">
|
|
<span>Earliest incident:</span>
|
|
<span className="font-medium">{district.earliestTime}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Latest incident:</span>
|
|
<span className="font-medium">{district.latestTime}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t border-border pt-2">
|
|
<div className="text-xs font-medium mb-1">Top incident types:</div>
|
|
<div className="space-y-1">
|
|
{topCategories.map(([category, count]) => (
|
|
<div key={category} className="flex justify-between">
|
|
<span className="text-xs truncate mr-2">{category}</span>
|
|
<span className="text-xs font-semibold">{count}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</Popup>
|
|
)
|
|
}
|