Enhance pop-up components with connection indicators and improve styling
- Refactored IncidentPopup, TimelinePopup, and UnitPopup components to include connection lines and dots for better visual indication of their relation to the map. - Updated the layout and styling of the pop-ups for improved readability and consistency. - Adjusted the TIME_ZONES data to reflect more accurate geographical locations. - Enhanced the digital clock display in the TimeZonesDisplay component with improved styling and shadow effects. - Added new CSS styles for digital clock presentation to enhance user experience.
This commit is contained in:
parent
da94277f4d
commit
4cc01babf1
|
@ -82,101 +82,125 @@ export default function IncidentPopup({ longitude, latitude, onClose, incident }
|
||||||
maxWidth="320px"
|
maxWidth="320px"
|
||||||
className="incident-popup z-50"
|
className="incident-popup z-50"
|
||||||
>
|
>
|
||||||
<Card
|
<div className="relative">
|
||||||
className={`bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 ${getBorderColor(incident.status)}`}
|
<Card
|
||||||
>
|
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">
|
>
|
||||||
{/* Custom close button */}
|
<div className="p-4 relative">
|
||||||
<Button
|
{/* Custom close button */}
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
className="absolute top-2 right-2 h-6 w-6 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
|
size="icon"
|
||||||
onClick={onClose}
|
className="absolute top-2 right-2 h-6 w-6 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
|
||||||
>
|
onClick={onClose}
|
||||||
<X className="h-4 w-4" />
|
>
|
||||||
<span className="sr-only">Close</span>
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
<span className="sr-only">Close</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<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" />
|
||||||
{incident.category || "Unknown Incident"}
|
{incident.category || "Unknown Incident"}
|
||||||
</h3>
|
</h3>
|
||||||
{getStatusBadge(incident.status)}
|
{getStatusBadge(incident.status)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{incident.description && (
|
|
||||||
<div className="mb-3 bg-slate-50 dark:bg-slate-900/40 p-3 rounded-lg">
|
|
||||||
<p className="text-sm">
|
|
||||||
<FileText className="inline-block h-3.5 w-3.5 mr-1.5 align-text-top text-slate-500" />
|
|
||||||
{incident.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<Separator className="my-3" />
|
{incident.description && (
|
||||||
|
<div className="mb-3 bg-slate-50 dark:bg-slate-900/40 p-3 rounded-lg">
|
||||||
{/* Improved section headers */}
|
<p className="text-sm">
|
||||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
<FileText className="inline-block h-3.5 w-3.5 mr-1.5 align-text-top text-slate-500" />
|
||||||
{incident.district && (
|
{incident.description}
|
||||||
<div className="col-span-2">
|
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">District</p>
|
|
||||||
<p className="flex items-center">
|
|
||||||
<Bookmark className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" />
|
|
||||||
<span className="font-medium">{incident.district}</span>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{incident.address && (
|
<Separator className="my-3" />
|
||||||
<div className="col-span-2">
|
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Location</p>
|
|
||||||
<p className="flex items-center">
|
|
||||||
<MapPin className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-red-500" />
|
|
||||||
<span className="font-medium">{incident.address}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{incident.timestamp && (
|
{/* Improved section headers */}
|
||||||
<>
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
<div>
|
{incident.district && (
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p>
|
<div className="col-span-2">
|
||||||
|
<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">
|
||||||
<Calendar className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-blue-500" />
|
<Bookmark className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" />
|
||||||
<span className="font-medium">{formatDate(incident.timestamp)}</span>
|
<span className="font-medium">{incident.district}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
)}
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Time</p>
|
|
||||||
|
{incident.address && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<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">
|
||||||
<Clock className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-amber-500" />
|
<MapPin className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-red-500" />
|
||||||
<span className="font-medium">{formatTime(incident.timestamp)}</span>
|
<span className="font-medium">{incident.address}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{incident.type_category && (
|
{incident.timestamp && (
|
||||||
<div className="col-span-2">
|
<>
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Type</p>
|
<div>
|
||||||
<p className="flex items-center">
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p>
|
||||||
<Tag className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" />
|
<p className="flex items-center">
|
||||||
<span className="font-medium">{incident.type_category}</span>
|
<Calendar className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-blue-500" />
|
||||||
</p>
|
<span className="font-medium">{formatDate(incident.timestamp)}</span>
|
||||||
</div>
|
</p>
|
||||||
)}
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Time</p>
|
||||||
|
<p className="flex items-center">
|
||||||
|
<Clock className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-amber-500" />
|
||||||
|
<span className="font-medium">{formatTime(incident.timestamp)}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-3 pt-3 border-t border-border">
|
{incident.type_category && (
|
||||||
<p className="text-xs text-muted-foreground flex items-center">
|
<div className="col-span-2">
|
||||||
<Navigation className="inline-block h-3 w-3 mr-1 shrink-0" />
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Type</p>
|
||||||
Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)}
|
<p className="flex items-center">
|
||||||
</p>
|
<Tag className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" />
|
||||||
<p className="text-xs text-muted-foreground mt-1">ID: {incident.id}</p>
|
<span className="font-medium">{incident.type_category}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 pt-3 border-t border-border">
|
||||||
|
<p className="text-xs text-muted-foreground flex items-center">
|
||||||
|
<Navigation className="inline-block h-3 w-3 mr-1 shrink-0" />
|
||||||
|
Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">ID: {incident.id}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
{/* Connection line */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
||||||
|
style={{
|
||||||
|
width: '2px',
|
||||||
|
height: '20px',
|
||||||
|
backgroundColor: 'red',
|
||||||
|
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Connection dot */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
||||||
|
style={{
|
||||||
|
width: '6px',
|
||||||
|
height: '6px',
|
||||||
|
backgroundColor: 'red',
|
||||||
|
borderRadius: '50%',
|
||||||
|
marginTop: '20px',
|
||||||
|
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,235 +108,253 @@ export default function DistrictPopup({
|
||||||
maxWidth="300px"
|
maxWidth="300px"
|
||||||
className="district-popup z-50"
|
className="district-popup z-50"
|
||||||
>
|
>
|
||||||
<Card className="bg-background p-0 w-full max-w-[300px] shadow-xl border-0 overflow-hidden">
|
<div className="relative">
|
||||||
<div className="bg-tertiary text-white p-3 relative">
|
<Card className="bg-background p-0 w-full max-w-[300px] shadow-xl border-0 overflow-hidden">
|
||||||
{/* Custom close button */}
|
<div className="bg-tertiary text-white p-3 relative">
|
||||||
<Button
|
{/* Custom close button */}
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
className="absolute top-2 right-2 h-5 w-5 rounded-full bg-white/20 hover:bg-white/30 text-white"
|
size="icon"
|
||||||
onClick={onClose}
|
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
|
<X className="h-3 w-3" />
|
||||||
</TabsTrigger>
|
<span className="sr-only">Close</span>
|
||||||
<TabsTrigger
|
</Button>
|
||||||
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 */}
|
<div className="flex items-center justify-between">
|
||||||
<TabsContent value="overview" className="mt-0 p-4">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-sm space-y-3">
|
<Building className="h-4 w-4" />
|
||||||
<div className="flex items-start gap-3">
|
<h3 className="font-bold text-base">{district.name}</h3>
|
||||||
<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>
|
||||||
|
{getCrimeRateBadge(district.level)}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
|
||||||
<TabsContent value="demographics" className="mt-0 p-4">
|
<div className="grid grid-cols-3 gap-1 p-2 bg-background">
|
||||||
{district.demographics ? (
|
<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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<TabsContent value="overview" className="mt-0 p-4">
|
||||||
<div className="text-sm space-y-3">
|
<div className="text-sm space-y-3">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="bg-blue-100 dark:bg-blue-950/30 p-2 rounded-full">
|
<div className="bg-amber-100 dark:bg-amber-950/30 p-2 rounded-full">
|
||||||
<Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
<AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Population</p>
|
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Crime Level</p>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
Total: {formatNumber(district.demographics.population || 0)}
|
This area has a {district.level || "unknown"} level of crime based on incident reports.
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
Density: {formatNumber(district.demographics.population_density || 0)} people/km²
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start gap-3">
|
{district.geographics && district.geographics.land_area && (
|
||||||
<div className="bg-red-100 dark:bg-red-950/30 p-2 rounded-full">
|
<div className="flex items-start gap-3">
|
||||||
<BarChart className="w-5 h-5 text-red-600 dark:text-red-400" />
|
<div className="bg-emerald-100 dark:bg-emerald-950/30 p-2 rounded-full">
|
||||||
</div>
|
<Home className="w-5 h-5 text-emerald-600 dark:text-emerald-400" />
|
||||||
<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>
|
</div>
|
||||||
<p className="text-muted-foreground mt-1 truncate">{incident.description || "No description"}</p>
|
<div>
|
||||||
<div className="flex justify-between items-center mt-1">
|
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Geography</p>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground text-xs">
|
||||||
{incident.timestamp ? new Date(incident.timestamp).toLocaleString() : "Unknown date"}
|
Land area: {formatNumber(district.geographics.land_area)} km²
|
||||||
</p>
|
</p>
|
||||||
<ChevronRight className="w-3 h-3 text-muted-foreground" />
|
{district.geographics.address && (
|
||||||
|
<p className="text-muted-foreground text-xs">Address: {district.geographics.address}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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 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>
|
</div>
|
||||||
) : (
|
</TabsContent>
|
||||||
<div className="text-center p-4 text-sm text-muted-foreground">
|
|
||||||
<AlertTriangle className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
<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>
|
<p>
|
||||||
No crime incidents available to display{filterCategory !== "all" ? ` for ${filterCategory}` : ""}.
|
No crime incidents available to display{filterCategory !== "all" ? ` for ${filterCategory}` : ""}.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs mt-2">Total reported incidents: {district.number_of_crime || 0}</p>
|
<p className="text-xs mt-2">Total reported incidents: {district.number_of_crime || 0}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Card>
|
</Card>
|
||||||
|
{/* Connection line */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
||||||
|
style={{
|
||||||
|
width: '2px',
|
||||||
|
height: '20px',
|
||||||
|
backgroundColor: 'red',
|
||||||
|
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Connection dot */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
||||||
|
style={{
|
||||||
|
width: '6px',
|
||||||
|
height: '6px',
|
||||||
|
backgroundColor: 'red',
|
||||||
|
borderRadius: '50%',
|
||||||
|
marginTop: '20px',
|
||||||
|
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,116 +65,140 @@ export default function IncidentPopup({
|
||||||
maxWidth="320px"
|
maxWidth="320px"
|
||||||
className="incident-popup z-50"
|
className="incident-popup z-50"
|
||||||
>
|
>
|
||||||
<Card
|
<div className="relative">
|
||||||
className="bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 border-l-red-600"
|
<Card
|
||||||
>
|
className="bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 border-l-red-600"
|
||||||
<div className="p-4 relative">
|
>
|
||||||
{/* Custom close button */}
|
<div className="p-4 relative">
|
||||||
<Button
|
{/* Custom close button */}
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
className="absolute top-2 right-2 h-6 w-6 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
|
size="icon"
|
||||||
onClick={onClose}
|
className="absolute top-2 right-2 h-6 w-6 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
|
||||||
>
|
onClick={onClose}
|
||||||
<X className="h-4 w-4" />
|
>
|
||||||
<span className="sr-only">Close</span>
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
<span className="sr-only">Close</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<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" />
|
||||||
{incident.category || "Unknown Incident"}
|
{incident.category || "Unknown Incident"}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
|
||||||
|
|
||||||
{incident.description && (
|
|
||||||
<div className="mb-3 bg-slate-50 dark:bg-slate-900/40 p-3 rounded-lg">
|
|
||||||
<p className="text-sm">
|
|
||||||
<FileText className="inline-block h-3.5 w-3.5 mr-1.5 align-text-top text-slate-500" />
|
|
||||||
{incident.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<Separator className="my-3" />
|
{incident.description && (
|
||||||
|
<div className="mb-3 bg-slate-50 dark:bg-slate-900/40 p-3 rounded-lg">
|
||||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
<p className="text-sm">
|
||||||
{incident.district && (
|
<FileText className="inline-block h-3.5 w-3.5 mr-1.5 align-text-top text-slate-500" />
|
||||||
<div className="col-span-2">
|
{incident.description}
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">District</p>
|
|
||||||
<p className="flex items-center">
|
|
||||||
<Bookmark className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" />
|
|
||||||
<span className="font-medium">{incident.district}</span>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{incident.date && (
|
<Separator className="my-3" />
|
||||||
<>
|
|
||||||
<div>
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p>
|
{incident.district && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<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">
|
||||||
<Calendar className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-blue-500" />
|
<Bookmark className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" />
|
||||||
<span className="font-medium">{formatDate(incident.date)}</span>
|
<span className="font-medium">{incident.district}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
)}
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Time</p>
|
|
||||||
<p className="flex items-center">
|
|
||||||
<Clock className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-amber-500" />
|
|
||||||
<span className="font-medium">{formatTime(incident.date)}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Distances to police units section */}
|
{incident.date && (
|
||||||
<Separator className="my-3" />
|
<>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p>
|
||||||
|
<p className="flex items-center">
|
||||||
|
<Calendar className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-blue-500" />
|
||||||
|
<span className="font-medium">{formatDate(incident.date)}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Time</p>
|
||||||
|
<p className="flex items-center">
|
||||||
|
<Clock className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-amber-500" />
|
||||||
|
<span className="font-medium">{formatTime(incident.date)}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* Distances to police units section */}
|
||||||
<h4 className="text-sm font-medium mb-2">Nearby Police Units</h4>
|
<Separator className="my-3" />
|
||||||
|
|
||||||
{isLoadingDistances ? (
|
<div>
|
||||||
<div className="space-y-2">
|
<h4 className="text-sm font-medium mb-2">Nearby Police Units</h4>
|
||||||
<Skeleton className="h-6 w-full" />
|
|
||||||
<Skeleton className="h-6 w-full" />
|
{isLoadingDistances ? (
|
||||||
<Skeleton className="h-6 w-full" />
|
|
||||||
</div>
|
|
||||||
) : distances.length > 0 ? (
|
|
||||||
<ScrollArea className="h-[120px] rounded-md border p-2">
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{distances.map((item) => (
|
<Skeleton className="h-6 w-full" />
|
||||||
<div key={item.unit_code} className="flex justify-between items-center text-xs border-b pb-1">
|
<Skeleton className="h-6 w-full" />
|
||||||
<div>
|
<Skeleton className="h-6 w-full" />
|
||||||
<p className="font-medium">{item.unit_name || "Unknown Unit"}</p>
|
|
||||||
<p className="text-muted-foreground text-[10px]">
|
|
||||||
{item.unit_type || "Police Unit"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="ml-2">
|
|
||||||
{formatDistance(item.distance_meters)}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
) : distances.length > 0 ? (
|
||||||
) : (
|
<ScrollArea className="h-[120px] rounded-md border p-2">
|
||||||
<p className="text-xs text-muted-foreground text-center p-2">
|
<div className="space-y-2">
|
||||||
No police units data available
|
{distances.map((item) => (
|
||||||
</p>
|
<div key={item.unit_code} className="flex justify-between items-center text-xs border-b pb-1">
|
||||||
)}
|
<div>
|
||||||
</div>
|
<p className="font-medium">{item.unit_name || "Unknown Unit"}</p>
|
||||||
|
<p className="text-muted-foreground text-[10px]">
|
||||||
|
{item.unit_type || "Police Unit"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="ml-2">
|
||||||
|
{formatDistance(item.distance_meters)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-muted-foreground text-center p-2">
|
||||||
|
No police units data available
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 pt-3 border-t border-border">
|
<div className="mt-3 pt-3 border-t border-border">
|
||||||
<p className="text-xs text-muted-foreground flex items-center">
|
<p className="text-xs text-muted-foreground flex items-center">
|
||||||
<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: {incident.id}</p>
|
<p className="text-xs text-muted-foreground mt-1">ID: {incident.id}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
{/* Connection line */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
||||||
|
style={{
|
||||||
|
width: '2px',
|
||||||
|
height: '20px',
|
||||||
|
backgroundColor: 'red',
|
||||||
|
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Connection dot */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
||||||
|
style={{
|
||||||
|
width: '6px',
|
||||||
|
height: '6px',
|
||||||
|
backgroundColor: 'red',
|
||||||
|
borderRadius: '50%',
|
||||||
|
marginTop: '20px',
|
||||||
|
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../..
|
||||||
import { Button } from "../../ui/button"
|
import { Button } from "../../ui/button"
|
||||||
import { Badge } from "../../ui/badge"
|
import { Badge } from "../../ui/badge"
|
||||||
|
|
||||||
|
|
||||||
interface TimelinePopupProps {
|
interface TimelinePopupProps {
|
||||||
longitude: number
|
longitude: number
|
||||||
latitude: number
|
latitude: number
|
||||||
|
@ -63,60 +62,84 @@ export default function TimelinePopup({
|
||||||
className="z-10"
|
className="z-10"
|
||||||
maxWidth="300px"
|
maxWidth="300px"
|
||||||
>
|
>
|
||||||
<Card className="border-0 shadow-none">
|
<div className="relative">
|
||||||
<CardHeader className="p-3 pb-2">
|
<Card className="border-0 shadow-none">
|
||||||
<div className="flex items-center justify-between">
|
<CardHeader className="p-3 pb-2">
|
||||||
<CardTitle className="text-base">{district.name}</CardTitle>
|
<div className="flex items-center justify-between">
|
||||||
<Button
|
<CardTitle className="text-base">{district.name}</CardTitle>
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
className="h-6 w-6"
|
size="icon"
|
||||||
onClick={onClose}
|
className="h-6 w-6"
|
||||||
>
|
onClick={onClose}
|
||||||
<X className="h-4 w-4" />
|
>
|
||||||
</Button>
|
<X className="h-4 w-4" />
|
||||||
</div>
|
</Button>
|
||||||
<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>
|
||||||
<div className="text-xs text-muted-foreground">
|
<CardDescription className="text-xs">
|
||||||
Based on {district.totalIncidents} incidents
|
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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm space-y-1 mb-3">
|
<div className="text-sm space-y-1 mb-3">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>Earliest incident:</span>
|
<span>Earliest incident:</span>
|
||||||
<span className="font-medium">{district.earliestTime}</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>
|
||||||
<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="border-t border-border pt-2">
|
||||||
<div className="text-xs font-medium mb-1">Top incident types:</div>
|
<div className="text-xs font-medium mb-1">Top incident types:</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{topCategories.map(([category, count]) => (
|
{topCategories.map(([category, count]) => (
|
||||||
<div key={category} className="flex justify-between">
|
<div key={category} className="flex justify-between">
|
||||||
<span className="text-xs truncate mr-2">{category}</span>
|
<span className="text-xs truncate mr-2">{category}</span>
|
||||||
<span className="text-xs font-semibold">{count}</span>
|
<span className="text-xs font-semibold">{count}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
{/* Connection line */}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-full"
|
||||||
|
style={{
|
||||||
|
width: '2px',
|
||||||
|
height: '20px',
|
||||||
|
backgroundColor: 'red',
|
||||||
|
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Connection dot */}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-full"
|
||||||
|
style={{
|
||||||
|
width: '6px',
|
||||||
|
height: '6px',
|
||||||
|
backgroundColor: 'red',
|
||||||
|
borderRadius: '50%',
|
||||||
|
marginBottom: '20px',
|
||||||
|
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,112 +56,136 @@ export default function UnitPopup({
|
||||||
maxWidth="320px"
|
maxWidth="320px"
|
||||||
className="unit-popup z-50"
|
className="unit-popup z-50"
|
||||||
>
|
>
|
||||||
<Card
|
<div className="relative">
|
||||||
className="bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 border-l-blue-700"
|
<Card
|
||||||
>
|
className="bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 border-l-blue-700"
|
||||||
<div className="p-4 relative">
|
>
|
||||||
{/* Custom close button */}
|
<div className="p-4 relative">
|
||||||
<Button
|
{/* Custom close button */}
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
className="absolute top-2 right-2 h-6 w-6 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
|
size="icon"
|
||||||
onClick={onClose}
|
className="absolute top-2 right-2 h-6 w-6 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
|
||||||
>
|
onClick={onClose}
|
||||||
<X className="h-4 w-4" />
|
>
|
||||||
<span className="sr-only">Close</span>
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
<span className="sr-only">Close</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<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">
|
||||||
<Shield className="h-4 w-4 text-blue-700" />
|
<Shield className="h-4 w-4 text-blue-700" />
|
||||||
{unit.name || "Police Unit"}
|
{unit.name || "Police Unit"}
|
||||||
</h3>
|
</h3>
|
||||||
<Badge variant="outline" className="bg-blue-100 text-blue-800 border-blue-200">
|
<Badge variant="outline" className="bg-blue-100 text-blue-800 border-blue-200">
|
||||||
{unit.type || "Unit"}
|
{unit.type || "Unit"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-2 text-sm">
|
<div className="grid grid-cols-1 gap-2 text-sm">
|
||||||
{unit.address && (
|
{unit.address && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Address</p>
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Address</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">{unit.address}</span>
|
<span className="font-medium">{unit.address}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{unit.phone && (
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Contact</p>
|
|
||||||
<p className="flex items-center">
|
|
||||||
<Phone className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" />
|
|
||||||
<span className="font-medium">{unit.phone}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{unit.district && (
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">District</p>
|
|
||||||
<p className="flex items-center">
|
|
||||||
<Building2 className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" />
|
|
||||||
<span className="font-medium">{unit.district}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Distances to incidents section */}
|
|
||||||
<Separator className="my-3" />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium mb-2 flex items-center">
|
|
||||||
<Compass className="h-4 w-4 mr-1.5 text-blue-600" />
|
|
||||||
Nearby Incidents
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
{isLoadingDistances ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Skeleton className="h-6 w-full" />
|
|
||||||
<Skeleton className="h-6 w-full" />
|
|
||||||
<Skeleton className="h-6 w-full" />
|
|
||||||
</div>
|
|
||||||
) : distances.length > 0 ? (
|
|
||||||
<ScrollArea className="h-[120px] rounded-md border p-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{distances.map((item) => (
|
|
||||||
<div key={item.incident_id} className="flex justify-between items-center text-xs border-b pb-1">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">{item.category_name || "Unknown"}</p>
|
|
||||||
<p className="text-muted-foreground text-[10px] truncate" style={{ maxWidth: "160px" }}>
|
|
||||||
{item.incident_description || "No description"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="ml-2 whitespace-nowrap">
|
|
||||||
{formatDistance(item.distance_meters)}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
)}
|
||||||
) : (
|
|
||||||
<p className="text-xs text-muted-foreground text-center p-2">
|
|
||||||
No incident data available
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 pt-3 border-t border-border">
|
{unit.phone && (
|
||||||
<p className="text-xs text-muted-foreground flex items-center">
|
<div>
|
||||||
<Navigation className="inline-block h-3 w-3 mr-1 shrink-0" />
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Contact</p>
|
||||||
Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)}
|
<p className="flex items-center">
|
||||||
</p>
|
<Phone className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" />
|
||||||
<p className="text-xs text-muted-foreground mt-1">ID: {unit.id}</p>
|
<span className="font-medium">{unit.phone}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{unit.district && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">District</p>
|
||||||
|
<p className="flex items-center">
|
||||||
|
<Building2 className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" />
|
||||||
|
<span className="font-medium">{unit.district}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Distances to incidents section */}
|
||||||
|
<Separator className="my-3" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-2 flex items-center">
|
||||||
|
<Compass className="h-4 w-4 mr-1.5 text-blue-600" />
|
||||||
|
Nearby Incidents
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{isLoadingDistances ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="h-6 w-full" />
|
||||||
|
<Skeleton className="h-6 w-full" />
|
||||||
|
<Skeleton className="h-6 w-full" />
|
||||||
|
</div>
|
||||||
|
) : distances.length > 0 ? (
|
||||||
|
<ScrollArea className="h-[120px] rounded-md border p-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{distances.map((item) => (
|
||||||
|
<div key={item.incident_id} className="flex justify-between items-center text-xs border-b pb-1">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{item.category_name || "Unknown"}</p>
|
||||||
|
<p className="text-muted-foreground text-[10px] truncate" style={{ maxWidth: "160px" }}>
|
||||||
|
{item.incident_description || "No description"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="ml-2 whitespace-nowrap">
|
||||||
|
{formatDistance(item.distance_meters)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-muted-foreground text-center p-2">
|
||||||
|
No incident data available
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 pt-3 border-t border-border">
|
||||||
|
<p className="text-xs text-muted-foreground flex items-center">
|
||||||
|
<Navigation className="inline-block h-3 w-3 mr-1 shrink-0" />
|
||||||
|
Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">ID: {unit.id}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
{/* Connection line */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
||||||
|
style={{
|
||||||
|
width: '2px',
|
||||||
|
height: '20px',
|
||||||
|
backgroundColor: 'red',
|
||||||
|
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Connection dot */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
||||||
|
style={{
|
||||||
|
width: '6px',
|
||||||
|
height: '6px',
|
||||||
|
backgroundColor: 'red',
|
||||||
|
borderRadius: '50%',
|
||||||
|
marginTop: '20px',
|
||||||
|
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,9 @@ interface TimeZoneMarker {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIME_ZONES: TimeZoneMarker[] = [
|
const TIME_ZONES: TimeZoneMarker[] = [
|
||||||
{ name: "WIB", offset: 7, longitude: 106.8456, latitude: -6.2088 }, // Jakarta
|
{ name: "WIB", offset: 7, longitude: 110.5, latitude: -3.3 }, // Between Java and Sumatra
|
||||||
{ name: "WITA", offset: 8, longitude: 115.1889, latitude: -8.4095 }, // Denpasar
|
{ name: "WITA", offset: 8, longitude: 118.8, latitude: -2.5 }, // Between Java and Kalimantan
|
||||||
{ name: "WIT", offset: 9, longitude: 140.7887, latitude: -2.5916 }, // Jayapura
|
{ name: "WIT", offset: 9, longitude: 128.0, latitude: -2.0 }, // Between Sulawesi and Papua
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function TimeZonesDisplay() {
|
export default function TimeZonesDisplay() {
|
||||||
|
@ -49,12 +49,19 @@ export default function TimeZonesDisplay() {
|
||||||
<>
|
<>
|
||||||
{TIME_ZONES.map((zone) => (
|
{TIME_ZONES.map((zone) => (
|
||||||
<Marker key={zone.name} longitude={zone.longitude} latitude={zone.latitude}>
|
<Marker key={zone.name} longitude={zone.longitude} latitude={zone.latitude}>
|
||||||
<div className="relative group">
|
<div className="relative">
|
||||||
<div className="absolute -translate-x-1/2 -translate-y-full mb-2 pointer-events-none">
|
<div className="absolute -translate-x-1/2 -translate-y-1/2 pointer-events-none">
|
||||||
<div className="bg-black/80 text-white px-2 py-1 rounded-md text-xs font-mono">
|
<div className="bg-black/90 border-2 border-orange-600/70 rounded-lg p-2 shadow-lg">
|
||||||
<div className="text-center font-bold">{zone.name}</div>
|
<div className="text-center text-orange-500 text-xs font-bold">{zone.name} / GMT+{zone.offset}</div>
|
||||||
<div className="digital-clock">{currentTimes[zone.name] || "00:00:00"}</div>
|
<div
|
||||||
<div className="text-center text-xs text-gray-300">GMT+{zone.offset}</div>
|
className="digital-clock font-mono font-bold text-amber-500 text-xl md:text-2xl tracking-wider"
|
||||||
|
style={{
|
||||||
|
textShadow: '0 0 5px rgba(255,170,0,0.7)',
|
||||||
|
fontFamily: "'Digital', monospace"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentTimes[zone.name] || "00:00:00"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -217,3 +217,16 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Digital Clock Styling */
|
||||||
|
.digital-clock {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue