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:
vergiLgood1 2025-05-07 07:21:38 +07:00
parent da94277f4d
commit 4cc01babf1
7 changed files with 669 additions and 536 deletions

View File

@ -82,101 +82,125 @@ export default function IncidentPopup({ longitude, latitude, onClose, incident }
maxWidth="320px"
className="incident-popup z-50"
>
<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 */}
<Button
variant="ghost"
size="icon"
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>
</Button>
<div className="relative">
<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 */}
<Button
variant="ghost"
size="icon"
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>
</Button>
<div className="flex items-center justify-between mb-3">
<h3 className="font-bold text-base flex items-center gap-1.5">
<AlertTriangle className="h-4 w-4 text-red-500" />
{incident.category || "Unknown Incident"}
</h3>
{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 className="flex items-center justify-between mb-3">
<h3 className="font-bold text-base flex items-center gap-1.5">
<AlertTriangle className="h-4 w-4 text-red-500" />
{incident.category || "Unknown Incident"}
</h3>
{getStatusBadge(incident.status)}
</div>
)}
<Separator className="my-3" />
{/* Improved section headers */}
<div className="grid grid-cols-2 gap-2 text-sm">
{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">
<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>
{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>
)}
{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">
<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>
)}
<Separator className="my-3" />
{incident.timestamp && (
<>
<div>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p>
{/* Improved section headers */}
<div className="grid grid-cols-2 gap-2 text-sm">
{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">
<Calendar className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-blue-500" />
<span className="font-medium">{formatDate(incident.timestamp)}</span>
<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>
</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">
<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>
<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.type_category && (
<div className="col-span-2">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Type</p>
<p className="flex items-center">
<Tag className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" />
<span className="font-medium">{incident.type_category}</span>
</p>
</div>
)}
</div>
{incident.timestamp && (
<>
<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.timestamp)}</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.timestamp)}</span>
</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: {incident.id}</p>
{incident.type_category && (
<div className="col-span-2">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Type</p>
<p className="flex items-center">
<Tag className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" />
<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>
</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>
)
}

View File

@ -108,235 +108,253 @@ export default function DistrictPopup({
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"
<div className="relative">
<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}
>
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>
<X className="h-3 w-3" />
<span className="sr-only">Close</span>
</Button>
{/* 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 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>
</TabsContent>
</div>
<TabsContent value="demographics" className="mt-0 p-4">
{district.demographics ? (
<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">
<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="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 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">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">
Total: {formatNumber(district.demographics.population || 0)}
</p>
<p className="text-muted-foreground text-xs">
Density: {formatNumber(district.demographics.population_density || 0)} people/km²
This area has a {district.level || "unknown"} level of crime based on incident reports.
</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>
{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>
<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"}
<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>
<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>
))}
{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 className="text-center p-4 text-sm text-muted-foreground">
<AlertTriangle className="w-8 h-8 mx-auto mb-2 opacity-50" />
</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>
<p className="text-xs mt-2">Total reported incidents: {district.number_of_crime || 0}</p>
</div>
)}
</TabsContent>
</Tabs>
</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>
)
}

View File

@ -65,116 +65,140 @@ export default function IncidentPopup({
maxWidth="320px"
className="incident-popup z-50"
>
<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 */}
<Button
variant="ghost"
size="icon"
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>
</Button>
<div className="relative">
<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 */}
<Button
variant="ghost"
size="icon"
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>
</Button>
<div className="flex items-center justify-between mb-3">
<h3 className="font-bold text-base flex items-center gap-1.5">
<AlertTriangle className="h-4 w-4 text-red-500" />
{incident.category || "Unknown Incident"}
</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 className="flex items-center justify-between mb-3">
<h3 className="font-bold text-base flex items-center gap-1.5">
<AlertTriangle className="h-4 w-4 text-red-500" />
{incident.category || "Unknown Incident"}
</h3>
</div>
)}
<Separator className="my-3" />
<div className="grid grid-cols-2 gap-2 text-sm">
{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">
<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>
{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>
)}
{incident.date && (
<>
<div>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p>
<Separator className="my-3" />
<div className="grid grid-cols-2 gap-2 text-sm">
{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">
<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>
<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>
</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 */}
<Separator className="my-3" />
{incident.date && (
<>
<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>
<h4 className="text-sm font-medium mb-2">Nearby Police Units</h4>
{/* Distances to police units section */}
<Separator className="my-3" />
{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>
<h4 className="text-sm font-medium mb-2">Nearby Police Units</h4>
{isLoadingDistances ? (
<div className="space-y-2">
{distances.map((item) => (
<div key={item.unit_code} className="flex justify-between items-center text-xs border-b pb-1">
<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>
))}
<Skeleton className="h-6 w-full" />
<Skeleton className="h-6 w-full" />
<Skeleton className="h-6 w-full" />
</div>
</ScrollArea>
) : (
<p className="text-xs text-muted-foreground text-center p-2">
No police units data available
</p>
)}
</div>
) : distances.length > 0 ? (
<ScrollArea className="h-[120px] rounded-md border p-2">
<div className="space-y-2">
{distances.map((item) => (
<div key={item.unit_code} className="flex justify-between items-center text-xs border-b pb-1">
<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">
<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 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>
</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>
)
}

View File

@ -7,7 +7,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../..
import { Button } from "../../ui/button"
import { Badge } from "../../ui/badge"
interface TimelinePopupProps {
longitude: number
latitude: number
@ -63,60 +62,84 @@ export default function TimelinePopup({
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 className="relative">
<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>
<div className="text-xs text-muted-foreground">
Based on {district.totalIncidents} incidents
<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>
<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 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="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 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>
</div>
</CardContent>
</Card>
</CardContent>
</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>
)
}

View File

@ -56,112 +56,136 @@ export default function UnitPopup({
maxWidth="320px"
className="unit-popup z-50"
>
<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 */}
<Button
variant="ghost"
size="icon"
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>
</Button>
<div className="relative">
<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 */}
<Button
variant="ghost"
size="icon"
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>
</Button>
<div className="flex items-center justify-between mb-3">
<h3 className="font-bold text-base flex items-center gap-1.5">
<Shield className="h-4 w-4 text-blue-700" />
{unit.name || "Police Unit"}
</h3>
<Badge variant="outline" className="bg-blue-100 text-blue-800 border-blue-200">
{unit.type || "Unit"}
</Badge>
</div>
<div className="flex items-center justify-between mb-3">
<h3 className="font-bold text-base flex items-center gap-1.5">
<Shield className="h-4 w-4 text-blue-700" />
{unit.name || "Police Unit"}
</h3>
<Badge variant="outline" className="bg-blue-100 text-blue-800 border-blue-200">
{unit.type || "Unit"}
</Badge>
</div>
<div className="grid grid-cols-1 gap-2 text-sm">
{unit.address && (
<div>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Address</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">{unit.address}</span>
</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 className="grid grid-cols-1 gap-2 text-sm">
{unit.address && (
<div>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Address</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">{unit.address}</span>
</p>
</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>
{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>
</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>
</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>
)
}

View File

@ -11,9 +11,9 @@ interface TimeZoneMarker {
}
const TIME_ZONES: TimeZoneMarker[] = [
{ name: "WIB", offset: 7, longitude: 106.8456, latitude: -6.2088 }, // Jakarta
{ name: "WITA", offset: 8, longitude: 115.1889, latitude: -8.4095 }, // Denpasar
{ name: "WIT", offset: 9, longitude: 140.7887, latitude: -2.5916 }, // Jayapura
{ name: "WIB", offset: 7, longitude: 110.5, latitude: -3.3 }, // Between Java and Sumatra
{ name: "WITA", offset: 8, longitude: 118.8, latitude: -2.5 }, // Between Java and Kalimantan
{ name: "WIT", offset: 9, longitude: 128.0, latitude: -2.0 }, // Between Sulawesi and Papua
]
export default function TimeZonesDisplay() {
@ -49,12 +49,19 @@ export default function TimeZonesDisplay() {
<>
{TIME_ZONES.map((zone) => (
<Marker key={zone.name} longitude={zone.longitude} latitude={zone.latitude}>
<div className="relative group">
<div className="absolute -translate-x-1/2 -translate-y-full mb-2 pointer-events-none">
<div className="bg-black/80 text-white px-2 py-1 rounded-md text-xs font-mono">
<div className="text-center font-bold">{zone.name}</div>
<div className="digital-clock">{currentTimes[zone.name] || "00:00:00"}</div>
<div className="text-center text-xs text-gray-300">GMT+{zone.offset}</div>
<div className="relative">
<div className="absolute -translate-x-1/2 -translate-y-1/2 pointer-events-none">
<div className="bg-black/90 border-2 border-orange-600/70 rounded-lg p-2 shadow-lg">
<div className="text-center text-orange-500 text-xs font-bold">{zone.name} / GMT+{zone.offset}</div>
<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>

View File

@ -217,3 +217,16 @@
text-align: center;
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;
}