Enhance Crime Sidebar UI and Improve Styling
- Updated CrimeSidebar component with improved layout and styling for better user experience. - Added new icons and adjusted colors for better visibility and aesthetics. - Enhanced tab styles to a pill format for a more modern look. - Improved card designs for incident statistics and crime overview. - Refactored SystemStatusCard and StatCard components to accept additional styling props. - Removed unused ReportCard component and related styles. - Cleaned up global CSS by removing unnecessary chart color variables.
This commit is contained in:
parent
428986c927
commit
91f9574285
|
@ -1,7 +1,11 @@
|
||||||
import { Popup } from 'react-map-gl/mapbox'
|
"use client"
|
||||||
import { Badge } from '@/app/_components/ui/badge'
|
|
||||||
import { Card } from '@/app/_components/ui/card'
|
import { Popup } from "react-map-gl/mapbox"
|
||||||
import { MapPin, AlertTriangle, Calendar, Clock, Tag, Bookmark, FileText } from 'lucide-react'
|
import { Badge } from "@/app/_components/ui/badge"
|
||||||
|
import { Card } from "@/app/_components/ui/card"
|
||||||
|
import { Separator } from "@/app/_components/ui/separator"
|
||||||
|
import { Button } from "@/app/_components/ui/button"
|
||||||
|
import { MapPin, AlertTriangle, Calendar, Clock, Tag, Bookmark, FileText, Navigation, X } from "lucide-react"
|
||||||
|
|
||||||
interface CrimePopupProps {
|
interface CrimePopupProps {
|
||||||
longitude: number
|
longitude: number
|
||||||
|
@ -22,15 +26,13 @@ interface CrimePopupProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CrimePopup({ longitude, latitude, onClose, crime }: CrimePopupProps) {
|
export default function CrimePopup({ longitude, latitude, onClose, crime }: CrimePopupProps) {
|
||||||
console.log("CrimePopup rendering with props:", { longitude, latitude, crime })
|
|
||||||
|
|
||||||
const formatDate = (date?: Date) => {
|
const formatDate = (date?: Date) => {
|
||||||
if (!date) return 'Unknown date'
|
if (!date) return "Unknown date"
|
||||||
return new Date(date).toLocaleDateString()
|
return new Date(date).toLocaleDateString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatTime = (date?: Date) => {
|
const formatTime = (date?: Date) => {
|
||||||
if (!date) return 'Unknown time'
|
if (!date) return "Unknown time"
|
||||||
return new Date(date).toLocaleTimeString()
|
return new Date(date).toLocaleTimeString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,86 +40,141 @@ export default function CrimePopup({ longitude, latitude, onClose, crime }: Crim
|
||||||
if (!status) return <Badge variant="outline">Unknown</Badge>
|
if (!status) return <Badge variant="outline">Unknown</Badge>
|
||||||
|
|
||||||
const statusLower = status.toLowerCase()
|
const statusLower = status.toLowerCase()
|
||||||
if (statusLower.includes('resolv') || statusLower.includes('closed')) {
|
if (statusLower.includes("resolv") || statusLower.includes("closed")) {
|
||||||
return <Badge className="bg-green-600">Resolved</Badge>
|
return <Badge className="bg-emerald-600 text-white">Resolved</Badge>
|
||||||
}
|
}
|
||||||
if (statusLower.includes('progress') || statusLower.includes('invest')) {
|
if (statusLower.includes("progress") || statusLower.includes("invest")) {
|
||||||
return <Badge className="bg-yellow-600">In Progress</Badge>
|
return <Badge className="bg-amber-500 text-white">In Progress</Badge>
|
||||||
}
|
}
|
||||||
if (statusLower.includes('open') || statusLower.includes('new')) {
|
if (statusLower.includes("open") || statusLower.includes("new")) {
|
||||||
return <Badge className="bg-blue-600">Open</Badge>
|
return <Badge className="bg-blue-600 text-white">Open</Badge>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Badge variant="outline">{status}</Badge>
|
return <Badge variant="outline">{status}</Badge>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine border color based on status
|
||||||
|
const getBorderColor = (status?: string) => {
|
||||||
|
if (!status) return "border-l-gray-400"
|
||||||
|
|
||||||
|
const statusLower = status.toLowerCase()
|
||||||
|
if (statusLower.includes("resolv") || statusLower.includes("closed")) {
|
||||||
|
return "border-l-emerald-600"
|
||||||
|
}
|
||||||
|
if (statusLower.includes("progress") || statusLower.includes("invest")) {
|
||||||
|
return "border-l-amber-500"
|
||||||
|
}
|
||||||
|
if (statusLower.includes("open") || statusLower.includes("new")) {
|
||||||
|
return "border-l-blue-600"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "border-l-gray-400"
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup
|
<Popup
|
||||||
longitude={longitude}
|
longitude={longitude}
|
||||||
latitude={latitude}
|
latitude={latitude}
|
||||||
closeButton={true}
|
closeButton={false} // Hide default close button
|
||||||
closeOnClick={false}
|
closeOnClick={false}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
anchor="top"
|
anchor="top"
|
||||||
maxWidth="280px"
|
maxWidth="320px"
|
||||||
className="crime-popup z-50"
|
className="crime-popup z-50"
|
||||||
>
|
>
|
||||||
<Card className="bg-background p-3 w-full max-w-[280px] shadow-xl border-0">
|
<Card
|
||||||
<div className="flex items-center justify-between mb-2">
|
className={`bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 ${getBorderColor(crime.status)}`}
|
||||||
<h3 className="font-semibold text-sm flex items-center gap-1">
|
>
|
||||||
<AlertTriangle className="h-3 w-3 text-red-500" />
|
<div className="p-4 relative">
|
||||||
{crime.category || 'Unknown Incident'}
|
{/* 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" />
|
||||||
|
{crime.category || "Unknown Incident"}
|
||||||
</h3>
|
</h3>
|
||||||
{getStatusBadge(crime.status)}
|
{getStatusBadge(crime.status)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{crime.description && (
|
{crime.description && (
|
||||||
<div className="mb-3">
|
<div className="mb-3 bg-slate-50 dark:bg-slate-900/40 p-3 rounded-lg">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-sm">
|
||||||
<FileText className="inline-block h-3 w-3 mr-1 align-text-top" />
|
<FileText className="inline-block h-3.5 w-3.5 mr-1.5 align-text-top text-slate-500" />
|
||||||
{crime.description}
|
{crime.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-1 text-xs text-muted-foreground">
|
<Separator className="my-3" />
|
||||||
|
|
||||||
|
{/* Improved section headers */}
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
{crime.district && (
|
{crime.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">
|
||||||
<Bookmark className="inline-block h-3 w-3 mr-1 shrink-0" />
|
<Bookmark className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" />
|
||||||
<span>{crime.district}</span>
|
<span className="font-medium">{crime.district}</span>
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{crime.address && (
|
{crime.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">
|
||||||
<MapPin className="inline-block h-3 w-3 mr-1 shrink-0" />
|
<MapPin className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-red-500" />
|
||||||
<span>{crime.address}</span>
|
<span className="font-medium">{crime.address}</span>
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{crime.timestamp && (
|
{crime.timestamp && (
|
||||||
<>
|
<>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p>
|
||||||
<p className="flex items-center">
|
<p className="flex items-center">
|
||||||
<Calendar className="inline-block h-3 w-3 mr-1 shrink-0" />
|
<Calendar className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-blue-500" />
|
||||||
<span>{formatDate(crime.timestamp)}</span>
|
<span className="font-medium">{formatDate(crime.timestamp)}</span>
|
||||||
</p>
|
</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">
|
<p className="flex items-center">
|
||||||
<Clock className="inline-block h-3 w-3 mr-1 shrink-0" />
|
<Clock className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-amber-500" />
|
||||||
<span>{formatTime(crime.timestamp)}</span>
|
<span className="font-medium">{formatTime(crime.timestamp)}</span>
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{crime.type && (
|
{crime.type && (
|
||||||
|
<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">
|
<p className="flex items-center">
|
||||||
<Tag className="inline-block h-3 w-3 mr-1 shrink-0" />
|
<Tag className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" />
|
||||||
<span>Type: {crime.type}</span>
|
<span className="font-medium">{crime.type}</span>
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<div className="mt-3 pt-3 border-t border-border">
|
||||||
ID: {crime.id}
|
<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>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">ID: {crime.id}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|
|
@ -1,27 +1,28 @@
|
||||||
import { useState, useMemo } from 'react'
|
"use client"
|
||||||
import { Popup } from 'react-map-gl/mapbox'
|
|
||||||
import { Badge } from '@/app/_components/ui/badge'
|
import { useState, useMemo } from "react"
|
||||||
import { Button } from '@/app/_components/ui/button'
|
import { Popup } from "react-map-gl/mapbox"
|
||||||
import { Card } from '@/app/_components/ui/card'
|
import { Badge } from "@/app/_components/ui/badge"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/app/_components/ui/tabs'
|
import { Card } from "@/app/_components/ui/card"
|
||||||
import { Separator } from '@/app/_components/ui/separator'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/tabs"
|
||||||
import { getMonthName } from '@/app/_utils/common'
|
import { Button } from "@/app/_components/ui/button"
|
||||||
import { BarChart, Map, Users, Home, FileBarChart, AlertTriangle } from 'lucide-react'
|
import { getMonthName } from "@/app/_utils/common"
|
||||||
import type { DistrictFeature } from '../layers/district-layer'
|
import { BarChart, Users, Home, AlertTriangle, ChevronRight, Building, Calendar, X } from 'lucide-react'
|
||||||
|
import type { DistrictFeature } from "../layers/district-layer"
|
||||||
|
|
||||||
// Helper function to format numbers
|
// Helper function to format numbers
|
||||||
function formatNumber(num?: number): string {
|
function formatNumber(num?: number): string {
|
||||||
if (num === undefined || num === null) return "N/A";
|
if (num === undefined || num === null) return "N/A"
|
||||||
|
|
||||||
if (num >= 1_000_000) {
|
if (num >= 1_000_000) {
|
||||||
return (num / 1_000_000).toFixed(1) + 'M';
|
return (num / 1_000_000).toFixed(1) + "M"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (num >= 1_000) {
|
if (num >= 1_000) {
|
||||||
return (num / 1_000).toFixed(1) + 'K';
|
return (num / 1_000).toFixed(1) + "K"
|
||||||
}
|
}
|
||||||
|
|
||||||
return num.toLocaleString();
|
return num.toLocaleString()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DistrictPopupProps {
|
interface DistrictPopupProps {
|
||||||
|
@ -41,140 +42,186 @@ export default function DistrictPopup({
|
||||||
district,
|
district,
|
||||||
year,
|
year,
|
||||||
month,
|
month,
|
||||||
filterCategory = "all"
|
filterCategory = "all",
|
||||||
}: DistrictPopupProps) {
|
}: DistrictPopupProps) {
|
||||||
console.log("DistrictPopup rendering with props:", { longitude, latitude, district, year, month })
|
const [activeTab, setActiveTab] = useState("overview")
|
||||||
const [activeTab, setActiveTab] = useState('overview')
|
|
||||||
|
|
||||||
// Extract all crime incidents from the district data and apply filtering if needed
|
// Extract all crime incidents from the district data and apply filtering if needed
|
||||||
const allCrimeIncidents = useMemo(() => {
|
const allCrimeIncidents = useMemo(() => {
|
||||||
// Check if there are crime incidents in the district object
|
// Check if there are crime incidents in the district object
|
||||||
if (!Array.isArray(district.crime_incidents)) {
|
if (!Array.isArray(district.crime_incidents)) {
|
||||||
console.warn("No crime incidents array found in district data");
|
console.warn("No crime incidents array found in district data")
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return all incidents if filterCategory is 'all'
|
// Return all incidents if filterCategory is 'all'
|
||||||
if (filterCategory === 'all') {
|
if (filterCategory === "all") {
|
||||||
return district.crime_incidents;
|
return district.crime_incidents
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, filter by category
|
// Otherwise, filter by category
|
||||||
return district.crime_incidents.filter(incident =>
|
return district.crime_incidents.filter((incident) => incident.category === filterCategory)
|
||||||
incident.category === filterCategory
|
}, [district, filterCategory])
|
||||||
);
|
|
||||||
}, [district, filterCategory]);
|
|
||||||
|
|
||||||
// For debugging: log the actual crime incidents count vs number_of_crime
|
|
||||||
console.log(`District ${district.name} - Number of crime from data: ${district.number_of_crime}, Incidents array length: ${allCrimeIncidents.length}`);
|
|
||||||
|
|
||||||
const getCrimeRateBadge = (level?: string) => {
|
const getCrimeRateBadge = (level?: string) => {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 'low':
|
case "low":
|
||||||
return <Badge className="bg-green-600">Low</Badge>
|
return <Badge className="bg-emerald-600 text-white">Low</Badge>
|
||||||
case 'medium':
|
case "medium":
|
||||||
return <Badge className="bg-yellow-600">Medium</Badge>
|
return <Badge className="bg-amber-500 text-white">Medium</Badge>
|
||||||
case 'high':
|
case "high":
|
||||||
return <Badge className="bg-red-600">High</Badge>
|
return <Badge className="bg-rose-600 text-white">High</Badge>
|
||||||
case 'critical':
|
case "critical":
|
||||||
return <Badge className="bg-red-800">Critical</Badge>
|
return <Badge className="bg-red-700 text-white">Critical</Badge>
|
||||||
default:
|
default:
|
||||||
return <Badge className="bg-gray-600">Unknown</Badge>
|
return <Badge className="bg-slate-600">Unknown</Badge>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format a time period string from year and month
|
// Format a time period string from year and month
|
||||||
const getTimePeriod = () => {
|
const getTimePeriod = () => {
|
||||||
if (year && month && month !== 'all') {
|
if (year && month && month !== "all") {
|
||||||
return `${getMonthName(Number(month))} ${year}`
|
return `${getMonthName(Number(month))} ${year}`
|
||||||
}
|
}
|
||||||
return year || 'All time'
|
return year || "All time"
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup
|
<Popup
|
||||||
longitude={longitude}
|
longitude={longitude}
|
||||||
latitude={latitude}
|
latitude={latitude}
|
||||||
closeButton={true}
|
closeButton={false} // Hide default close button
|
||||||
closeOnClick={false}
|
closeOnClick={false}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
anchor="top"
|
anchor="top"
|
||||||
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">
|
<Card className="bg-background p-0 w-full max-w-[300px] shadow-xl border-0 overflow-hidden">
|
||||||
<div className="p-3">
|
<div className="bg-tertiary text-white p-3 relative">
|
||||||
<div className="flex items-center justify-between mb-2">
|
{/* Custom close button */}
|
||||||
<h3 className="font-semibold text-base">{district.name}</h3>
|
<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)}
|
{getCrimeRateBadge(district.level)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<div className="mt-1 text-white/80 text-xs flex items-center gap-2">
|
||||||
<Map className="inline w-3 h-3 mr-1" />
|
<Calendar className="h-3 w-3" />
|
||||||
District ID: {district.id}
|
<span>{getTimePeriod()}</span>
|
||||||
</p>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mb-1">
|
</div>
|
||||||
<FileBarChart className="inline w-3 h-3 mr-1" />
|
|
||||||
{district.number_of_crime || 0} crime incidents in {getTimePeriod()}
|
<div className="grid grid-cols-3 gap-1 p-2 bg-background">
|
||||||
{filterCategory !== 'all' ? ` (${filterCategory} category)` : ''}
|
<div className="flex flex-col items-center justify-center p-1.5 bg-accent rounded-lg shadow-sm">
|
||||||
</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<div className="border-t border-border">
|
{/* Improved tab headers */}
|
||||||
<TabsList className="w-full rounded-none bg-muted/50 p-0 h-9">
|
<TabsList className="w-full grid grid-cols-3 h-10 rounded-none bg-background border-b">
|
||||||
<TabsTrigger value="overview" className="rounded-none flex-1 text-xs h-full">
|
<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
|
Overview
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="demographics" className="rounded-none flex-1 text-xs h-full">
|
<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
|
Demographics
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="crime_incidents" className="rounded-none flex-1 text-xs h-full">
|
<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
|
Incidents
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
|
||||||
|
|
||||||
<TabsContent value="overview" className="mt-0 p-3">
|
{/* Tab content with improved section headers */}
|
||||||
<div className="text-sm space-y-2">
|
<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>
|
<div>
|
||||||
<p className="font-medium">Crime Level</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">
|
||||||
This area has a {district.level || 'unknown'} level of crime based on incident reports.
|
This area has a {district.level || "unknown"} level of crime based on incident reports.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{district.geographics && district.geographics.land_area && (
|
{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>
|
<div>
|
||||||
<p className="font-medium flex items-center gap-1">
|
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Geography</p>
|
||||||
<Home className="w-3 h-3" /> Geography
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
Land area: {formatNumber(district.geographics.land_area)} km²
|
Land area: {formatNumber(district.geographics.land_area)} km²
|
||||||
</p>
|
</p>
|
||||||
{district.geographics.address && (
|
{district.geographics.address && (
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">Address: {district.geographics.address}</p>
|
||||||
Address: {district.geographics.address}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<p className="font-medium">Time Period</p>
|
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Time Period</p>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
Data shown for {getTimePeriod()}
|
Data shown for {getTimePeriod()}
|
||||||
|
{filterCategory !== "all" ? ` (${filterCategory} category)` : ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="demographics" className="mt-0 p-3">
|
<TabsContent value="demographics" className="mt-0 p-4">
|
||||||
{district.demographics ? (
|
{district.demographics ? (
|
||||||
<div className="text-sm space-y-3">
|
<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>
|
<div>
|
||||||
<p className="font-medium flex items-center gap-1">
|
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Population</p>
|
||||||
<Users className="w-3 h-3" /> Population
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
Total: {formatNumber(district.demographics.population || 0)}
|
Total: {formatNumber(district.demographics.population || 0)}
|
||||||
</p>
|
</p>
|
||||||
|
@ -182,30 +229,47 @@ export default function DistrictPopup({
|
||||||
Density: {formatNumber(district.demographics.population_density || 0)} people/km²
|
Density: {formatNumber(district.demographics.population_density || 0)} people/km²
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<p className="font-medium">Unemployment</p>
|
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Unemployment</p>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
{formatNumber(district.demographics.number_of_unemployed || 0)} unemployed people
|
{formatNumber(district.demographics.number_of_unemployed || 0)} unemployed people
|
||||||
</p>
|
</p>
|
||||||
{district.demographics.population && district.demographics.number_of_unemployed && (
|
{district.demographics.population && district.demographics.number_of_unemployed && (
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
Rate: {((district.demographics.number_of_unemployed / district.demographics.population) * 100).toFixed(1)}%
|
Rate:{" "}
|
||||||
|
{(
|
||||||
|
(district.demographics.number_of_unemployed / district.demographics.population) *
|
||||||
|
100
|
||||||
|
).toFixed(1)}
|
||||||
|
%
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<p className="font-medium">Crime Rate</p>
|
<p className="font-semibold text-base text-slate-800 dark:text-slate-200">Crime Rate</p>
|
||||||
{district.number_of_crime && district.demographics.population ? (
|
{district.number_of_crime && district.demographics.population ? (
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
{((district.number_of_crime / district.demographics.population) * 10000).toFixed(2)} crime incidents per 10,000 people
|
{((district.number_of_crime / district.demographics.population) * 10000).toFixed(2)} crime
|
||||||
|
incidents per 10,000 people
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground text-xs">No data available</p>
|
<p className="text-muted-foreground text-xs">No data available</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center p-4 text-sm text-muted-foreground">
|
<div className="text-center p-4 text-sm text-muted-foreground">
|
||||||
<Users className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
<Users className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||||
|
@ -214,35 +278,38 @@ export default function DistrictPopup({
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* // Inside the TabsContent for crime_incidents */}
|
|
||||||
<TabsContent value="crime_incidents" className="mt-0 max-h-[250px] overflow-y-auto">
|
<TabsContent value="crime_incidents" className="mt-0 max-h-[250px] overflow-y-auto">
|
||||||
{allCrimeIncidents && allCrimeIncidents.length > 0 ? (
|
{allCrimeIncidents && allCrimeIncidents.length > 0 ? (
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
{allCrimeIncidents.map((incident, index) => (
|
{allCrimeIncidents.map((incident, index) => (
|
||||||
<div key={incident.id || index} className="p-3 text-xs">
|
<div
|
||||||
<div className="flex justify-between">
|
key={incident.id || index}
|
||||||
<span className="font-medium">
|
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"}
|
{incident.category || incident.type || "Unknown"}
|
||||||
</span>
|
</span>
|
||||||
<Badge variant="outline" className="text-[10px] h-5">
|
<Badge variant="outline" className="text-[10px] h-5">
|
||||||
{incident.status || "unknown"}
|
{incident.status || "unknown"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground mt-1 truncate">
|
<p className="text-muted-foreground mt-1 truncate">{incident.description || "No description"}</p>
|
||||||
{incident.description || "No description"}
|
<div className="flex justify-between items-center mt-1">
|
||||||
</p>
|
<p className="text-muted-foreground">
|
||||||
<p className="text-muted-foreground mt-1">
|
|
||||||
{incident.timestamp ? new Date(incident.timestamp).toLocaleString() : "Unknown date"}
|
{incident.timestamp ? new Date(incident.timestamp).toLocaleString() : "Unknown date"}
|
||||||
</p>
|
</p>
|
||||||
|
<ChevronRight className="w-3 h-3 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Show a note if we're missing some incidents */}
|
|
||||||
{district.number_of_crime > allCrimeIncidents.length && (
|
{district.number_of_crime > allCrimeIncidents.length && (
|
||||||
<div className="p-3 text-xs text-center text-muted-foreground bg-muted/50">
|
<div className="p-3 text-xs text-center text-muted-foreground bg-muted/50">
|
||||||
<p>
|
<p>
|
||||||
Showing {allCrimeIncidents.length} of {district.number_of_crime} total incidents
|
Showing {allCrimeIncidents.length} of {district.number_of_crime} total incidents
|
||||||
{filterCategory !== 'all' ? ` for ${filterCategory} category` : ''}
|
{filterCategory !== "all" ? ` for ${filterCategory} category` : ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -250,7 +317,9 @@ export default function DistrictPopup({
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center p-4 text-sm text-muted-foreground">
|
<div className="text-center p-4 text-sm text-muted-foreground">
|
||||||
<AlertTriangle className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
<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>
|
||||||
|
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>
|
<p className="text-xs mt-2">Total reported incidents: {district.number_of_crime || 0}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,22 +1,19 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo } from "react"
|
import React, { useState, useEffect, useMemo } from "react"
|
||||||
import {
|
import { AlertTriangle, BarChart, ChevronRight, MapPin, Skull, Shield, FileText, Clock, Calendar, MapIcon, Info, CheckCircle, AlertCircle, XCircle, Bell, Users, Search, List, RefreshCw, Eye, X, ChevronLeft, PieChart, LineChart, Activity, Filter, Layers, Settings, Menu } from 'lucide-react'
|
||||||
AlertTriangle, BarChart, ChevronRight, MapPin, Skull, Shield, FileText,
|
|
||||||
Clock, Calendar, MapIcon, Info, CheckCircle, AlertCircle, XCircle,
|
|
||||||
Bell, Users, Search, List, RefreshCw, Eye
|
|
||||||
} from "lucide-react"
|
|
||||||
import { Separator } from "@/app/_components/ui/separator"
|
import { Separator } from "@/app/_components/ui/separator"
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/app/_components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/app/_components/ui/card"
|
||||||
import { cn } from "@/app/_lib/utils"
|
import { cn } from "@/app/_lib/utils"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/tabs"
|
||||||
import { Badge } from "@/app/_components/ui/badge"
|
import { Badge } from "@/app/_components/ui/badge"
|
||||||
|
import { Button } from "@/app/_components/ui/button"
|
||||||
import { CRIME_RATE_COLORS } from "@/app/_utils/const/map"
|
import { CRIME_RATE_COLORS } from "@/app/_utils/const/map"
|
||||||
import { usePrefetchedCrimeData } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_hooks/use-prefetch-crimes"
|
import { usePrefetchedCrimeData } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_hooks/use-prefetch-crimes"
|
||||||
import { getMonthName, formatDateString } from "@/app/_utils/common"
|
import { getMonthName, formatDateString } from "@/app/_utils/common"
|
||||||
import { Skeleton } from "@/app/_components/ui/skeleton"
|
import { Skeleton } from "@/app/_components/ui/skeleton"
|
||||||
import { useGetCrimeCategories } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries"
|
import { useGetCrimeCategories } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries"
|
||||||
import { $Enums, crime_categories, crime_incidents, crimes, demographics, districts, geographics, locations } from "@prisma/client"
|
import { $Enums } from "@prisma/client"
|
||||||
|
|
||||||
interface CrimeSidebarProps {
|
interface CrimeSidebarProps {
|
||||||
className?: string
|
className?: string
|
||||||
|
@ -297,33 +294,72 @@ export default function CrimeSidebar({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"fixed top-0 left-0 h-full z-40 transition-transform duration-300 ease-in-out bg-background backdrop-blur-sm border-r border-white/10",
|
"fixed top-0 left-0 h-full z-40 transition-transform duration-300 ease-in-out bg-background border-r border-sidebar-border",
|
||||||
isCollapsed ? "-translate-x-full" : "translate-x-0",
|
isCollapsed ? "-translate-x-full" : "translate-x-0",
|
||||||
className
|
className
|
||||||
)}>
|
)}>
|
||||||
<div className="relative h-full flex items-stretch">
|
<div className="relative h-full flex items-stretch">
|
||||||
<div className="bg-background backdrop-blur-sm border-r border-white/10 h-full w-[400px]">
|
<div className="bg-background backdrop-blur-sm border-r border-sidebar-border h-full w-[420px]">
|
||||||
<div className="p-4 text-white h-full flex flex-col max-h-full overflow-hidden">
|
<div className="p-4 text-sidebar-foreground h-full flex flex-col max-h-full overflow-hidden">
|
||||||
<CardHeader className="p-0 pb-2 shrink-0">
|
{/* Header with improved styling */}
|
||||||
<CardTitle className="text-xl font-semibold flex items-center gap-2">
|
<CardHeader className="p-0 pb-4 shrink-0 relative">
|
||||||
<AlertTriangle className="h-5 w-5" />
|
<div className="absolute top-0 right-0">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent/30"
|
||||||
|
onClick={() => setIsCollapsed(true)}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="bg-sidebar-primary p-2 rounded-lg">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-sidebar-primary-foreground" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-xl font-semibold">
|
||||||
Crime Analysis
|
Crime Analysis
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{!isCrimesLoading && (
|
{!isCrimesLoading && (
|
||||||
<CardDescription className="text-xs text-white/60">
|
<CardDescription className="text-sm text-sidebar-foreground/70">
|
||||||
{getTimePeriodDisplay()}
|
{getTimePeriodDisplay()}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<Tabs defaultValue="incidents" className="w-full flex-1 flex flex-col overflow-hidden" value={activeTab} onValueChange={setActiveTab}>
|
{/* Improved tabs with pill style */}
|
||||||
<TabsList className="w-full mb-2 bg-black/30">
|
<Tabs
|
||||||
<TabsTrigger value="incidents" className="flex-1">Dashboard</TabsTrigger>
|
defaultValue="incidents"
|
||||||
<TabsTrigger value="statistics" className="flex-1">Statistics</TabsTrigger>
|
className="w-full flex-1 flex flex-col overflow-hidden"
|
||||||
<TabsTrigger value="info" className="flex-1">Information</TabsTrigger>
|
value={activeTab}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
>
|
||||||
|
<TabsList className="w-full mb-4 bg-sidebar-accent p-1 rounded-full">
|
||||||
|
<TabsTrigger
|
||||||
|
value="incidents"
|
||||||
|
className="flex-1 rounded-full data-[state=active]:bg-sidebar-primary data-[state=active]:text-sidebar-primary-foreground"
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="statistics"
|
||||||
|
className="flex-1 rounded-full data-[state=active]:bg-sidebar-primary data-[state=active]:text-sidebar-primary-foreground"
|
||||||
|
>
|
||||||
|
Statistics
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="info"
|
||||||
|
className="flex-1 rounded-full data-[state=active]:bg-sidebar-primary data-[state=active]:text-sidebar-primary-foreground"
|
||||||
|
>
|
||||||
|
Information
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto overflow-x-hidden pr-1">
|
<div className="flex-1 overflow-y-auto overflow-x-hidden pr-1 custom-scrollbar">
|
||||||
<TabsContent value="incidents" className="m-0 p-0 space-y-4">
|
<TabsContent value="incidents" className="m-0 p-0 space-y-4">
|
||||||
{isCrimesLoading ? (
|
{isCrimesLoading ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
@ -342,24 +378,28 @@ export default function CrimeSidebar({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Card className="bg-black/20 border border-white/10">
|
{/* Enhanced info card */}
|
||||||
<CardContent className="p-3 text-sm">
|
<Card className="bg-gradient-to-r from-sidebar-primary/30 to-sidebar-primary/20 border border-sidebar-primary/20 overflow-hidden">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<CardContent className="p-4 text-sm relative">
|
||||||
|
<div className="absolute top-0 right-0 w-24 h-24 bg-sidebar-primary/10 rounded-full -translate-y-1/2 translate-x-1/2"></div>
|
||||||
|
<div className="absolute bottom-0 left-0 w-16 h-16 bg-sidebar-primary/10 rounded-full translate-y-1/2 -translate-x-1/2"></div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar className="h-4 w-4 text-blue-400" />
|
<Calendar className="h-4 w-4 text-sidebar-primary" />
|
||||||
<span className="font-medium">{formattedDate}</span>
|
<span className="font-medium">{formattedDate}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="h-4 w-4 text-blue-400" />
|
<Clock className="h-4 w-4 text-sidebar-primary" />
|
||||||
<span>{formattedTime}</span>
|
<span>{formattedTime}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<MapPin className="h-4 w-4 text-blue-400" />
|
<MapPin className="h-4 w-4 text-sidebar-primary" />
|
||||||
<span className="text-white/70">{location}</span>
|
<span className="text-sidebar-foreground/70">{location}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 bg-sidebar-accent/30 p-2 rounded-lg">
|
||||||
<AlertTriangle className="h-4 w-4 text-amber-400" />
|
<AlertTriangle className="h-5 w-5 text-amber-400" />
|
||||||
<span>
|
<span>
|
||||||
<strong>{crimeStats.totalIncidents || 0}</strong> incidents reported
|
<strong>{crimeStats.totalIncidents || 0}</strong> incidents reported
|
||||||
{selectedMonth !== 'all' ? ` in ${getMonthName(Number(selectedMonth))}` : ` in ${selectedYear}`}
|
{selectedMonth !== 'all' ? ` in ${getMonthName(Number(selectedMonth))}` : ` in ${selectedYear}`}
|
||||||
|
@ -368,33 +408,42 @@ export default function CrimeSidebar({
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
{/* Enhanced stat cards */}
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<SystemStatusCard
|
<SystemStatusCard
|
||||||
title="Total Cases"
|
title="Total Cases"
|
||||||
status={`${crimeStats?.totalIncidents || 0}`}
|
status={`${crimeStats?.totalIncidents || 0}`}
|
||||||
statusIcon={<AlertCircle className="h-4 w-4 text-blue-500" />}
|
statusIcon={<AlertCircle className="h-4 w-4 text-green-400" />}
|
||||||
statusColor="text-blue-500"
|
statusColor="text-green-400"
|
||||||
updatedTime={getTimePeriodDisplay()}
|
updatedTime={getTimePeriodDisplay()}
|
||||||
|
bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20"
|
||||||
|
borderColor="border-sidebar-border"
|
||||||
/>
|
/>
|
||||||
<SystemStatusCard
|
<SystemStatusCard
|
||||||
title="Recent Cases"
|
title="Recent Cases"
|
||||||
status={`${crimeStats?.recentIncidents?.length || 0}`}
|
status={`${crimeStats?.recentIncidents?.length || 0}`}
|
||||||
statusIcon={<Clock className="h-4 w-4 text-amber-500" />}
|
statusIcon={<Clock className="h-4 w-4 text-amber-400" />}
|
||||||
statusColor="text-amber-500"
|
statusColor="text-amber-400"
|
||||||
updatedTime="Last 30 days"
|
updatedTime="Last 30 days"
|
||||||
|
bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20"
|
||||||
|
borderColor="border-sidebar-border"
|
||||||
/>
|
/>
|
||||||
<SystemStatusCard
|
<SystemStatusCard
|
||||||
title="Top Category"
|
title="Top Category"
|
||||||
status={topCategories.length > 0 ? topCategories[0].type : "None"}
|
status={topCategories.length > 0 ? topCategories[0].type : "None"}
|
||||||
statusIcon={<Shield className="h-4 w-4 text-green-500" />}
|
statusIcon={<Shield className="h-4 w-4 text-green-400" />}
|
||||||
statusColor="text-green-500"
|
statusColor="text-green-400"
|
||||||
|
bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20"
|
||||||
|
borderColor="border-sidebar-border"
|
||||||
/>
|
/>
|
||||||
<SystemStatusCard
|
<SystemStatusCard
|
||||||
title="Districts"
|
title="Districts"
|
||||||
status={`${Object.keys(crimeStats.districts).length}`}
|
status={`${Object.keys(crimeStats.districts).length}`}
|
||||||
statusIcon={<MapPin className="h-4 w-4 text-purple-500" />}
|
statusIcon={<MapPin className="h-4 w-4 text-purple-400" />}
|
||||||
statusColor="text-purple-500"
|
statusColor="text-purple-400"
|
||||||
updatedTime="Affected areas"
|
updatedTime="Affected areas"
|
||||||
|
bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20"
|
||||||
|
borderColor="border-sidebar-border"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -419,7 +468,7 @@ export default function CrimeSidebar({
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
{crimeStats.recentIncidents.slice(0, 6).map((incident) => (
|
{crimeStats.recentIncidents.slice(0, 6).map((incident) => (
|
||||||
<IncidentCard
|
<IncidentCard
|
||||||
key={incident.id}
|
key={incident.id}
|
||||||
|
@ -446,13 +495,16 @@ export default function CrimeSidebar({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Card className="bg-black/20 border border-white/10 p-3">
|
<Card className="bg-gradient-to-r from-sidebar-primary/30 to-sidebar-primary/20 border border-sidebar-primary/20 overflow-hidden">
|
||||||
<CardHeader className="p-0 pb-2">
|
<CardHeader className="p-3 pb-0">
|
||||||
<CardTitle className="text-sm font-medium">Monthly Incidents</CardTitle>
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<LineChart className="h-4 w-4 text-green-400" />
|
||||||
|
Monthly Incidents
|
||||||
|
</CardTitle>
|
||||||
<CardDescription className="text-xs text-white/60">{selectedYear}</CardDescription>
|
<CardDescription className="text-xs text-white/60">{selectedYear}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-3">
|
||||||
<div className="h-24 flex items-end gap-1 mt-2">
|
<div className="h-32 flex items-end gap-1 mt-2">
|
||||||
{crimeStats.incidentsByMonth.map((count, i) => {
|
{crimeStats.incidentsByMonth.map((count, i) => {
|
||||||
const maxCount = Math.max(...crimeStats.incidentsByMonth)
|
const maxCount = Math.max(...crimeStats.incidentsByMonth)
|
||||||
const height = maxCount > 0 ? (count / maxCount) * 100 : 0
|
const height = maxCount > 0 ? (count / maxCount) * 100 : 0
|
||||||
|
@ -461,19 +513,19 @@ export default function CrimeSidebar({
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-blue-500 w-full rounded-t",
|
"bg-gradient-to-t from-emerald-600 to-green-400 w-full rounded-t-md",
|
||||||
selectedMonth !== 'all' && i + 1 === Number(selectedMonth) ? "bg-yellow-500" : ""
|
selectedMonth !== 'all' && i + 1 === Number(selectedMonth) ? "from-amber-500 to-amber-400" : ""
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
height: `${Math.max(5, height)}%`,
|
height: `${Math.max(5, height)}%`,
|
||||||
opacity: 0.6 + (i / 24)
|
opacity: 0.7 + (i / 24)
|
||||||
}}
|
}}
|
||||||
title={`${getMonthName(i + 1)}: ${count} incidents`}
|
title={`${getMonthName(i + 1)}: ${count} incidents`}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mt-1 text-[10px] text-white/60">
|
<div className="flex justify-between mt-2 text-[10px] text-white/60">
|
||||||
<span>Jan</span>
|
<span>Jan</span>
|
||||||
<span>Feb</span>
|
<span>Feb</span>
|
||||||
<span>Mar</span>
|
<span>Mar</span>
|
||||||
|
@ -490,12 +542,14 @@ export default function CrimeSidebar({
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<SidebarSection title="Crime Overview" icon={<BarChart className="h-4 w-4 text-blue-400" />}>
|
<SidebarSection title="Crime Overview" icon={<Activity className="h-4 w-4 text-blue-400" />}>
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Total Incidents"
|
title="Total Incidents"
|
||||||
value={crimeStats.totalIncidents.toString()}
|
value={crimeStats.totalIncidents.toString()}
|
||||||
change={`${Object.keys(crimeStats.districts).length} districts`}
|
change={`${Object.keys(crimeStats.districts).length} districts`}
|
||||||
|
icon={<AlertTriangle className="h-4 w-4 text-blue-400" />}
|
||||||
|
bgColor="bg-gradient-to-r from-blue-900/30 to-blue-800/20"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title={selectedMonth !== 'all' ?
|
title={selectedMonth !== 'all' ?
|
||||||
|
@ -510,20 +564,24 @@ export default function CrimeSidebar({
|
||||||
`in ${getMonthName(Number(selectedMonth))}` :
|
`in ${getMonthName(Number(selectedMonth))}` :
|
||||||
"per active month"}
|
"per active month"}
|
||||||
isPositive={false}
|
isPositive={false}
|
||||||
|
icon={<Calendar className="h-4 w-4 text-amber-400" />}
|
||||||
|
bgColor="bg-gradient-to-r from-amber-900/30 to-amber-800/20"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Clearance Rate"
|
title="Clearance Rate"
|
||||||
value={`${crimeStats.clearanceRate}%`}
|
value={`${crimeStats.clearanceRate}%`}
|
||||||
change="of cases resolved"
|
change="of cases resolved"
|
||||||
isPositive={crimeStats.clearanceRate > 50}
|
isPositive={crimeStats.clearanceRate > 50}
|
||||||
|
icon={<CheckCircle className="h-4 w-4 text-green-400" />}
|
||||||
|
bgColor="bg-gradient-to-r from-green-900/30 to-green-800/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
|
|
||||||
<Separator className="bg-white/20 my-2" />
|
<Separator className="bg-white/20 my-4" />
|
||||||
|
|
||||||
<SidebarSection title="Most Common Crimes" icon={<Skull className="h-4 w-4 text-amber-400" />}>
|
<SidebarSection title="Most Common Crimes" icon={<PieChart className="h-4 w-4 text-amber-400" />}>
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
{topCategories.length > 0 ? (
|
{topCategories.length > 0 ? (
|
||||||
topCategories.map((category) => (
|
topCategories.map((category) => (
|
||||||
<CrimeTypeCard
|
<CrimeTypeCard
|
||||||
|
@ -551,46 +609,46 @@ export default function CrimeSidebar({
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="info" className="m-0 p-0 space-y-4">
|
<TabsContent value="info" className="m-0 p-0 space-y-4">
|
||||||
<SidebarSection title="Map Legend" icon={<MapIcon className="h-4 w-4 text-blue-400" />}>
|
<SidebarSection title="Map Legend" icon={<Layers className="h-4 w-4 text-green-400" />}>
|
||||||
<Card className="bg-black/20 border border-white/10">
|
<Card className="bg-gradient-to-r from-zinc-800/80 to-zinc-900/80 border border-white/10">
|
||||||
<CardContent className="p-3 text-xs space-y-2">
|
<CardContent className="p-4 text-xs space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="font-medium mb-1">Crime Severity</h4>
|
<h4 className="font-medium mb-2 text-sm">Crime Severity</h4>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 p-1.5 hover:bg-white/5 rounded-md transition-colors">
|
||||||
<div className="w-4 h-4 rounded" style={{ backgroundColor: CRIME_RATE_COLORS.low }}></div>
|
<div className="w-4 h-4 rounded" style={{ backgroundColor: CRIME_RATE_COLORS.low }}></div>
|
||||||
<span>Low Crime Rate</span>
|
<span>Low Crime Rate</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 p-1.5 hover:bg-white/5 rounded-md transition-colors">
|
||||||
<div className="w-4 h-4 rounded" style={{ backgroundColor: CRIME_RATE_COLORS.medium }}></div>
|
<div className="w-4 h-4 rounded" style={{ backgroundColor: CRIME_RATE_COLORS.medium }}></div>
|
||||||
<span>Medium Crime Rate</span>
|
<span>Medium Crime Rate</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 p-1.5 hover:bg-white/5 rounded-md transition-colors">
|
||||||
<div className="w-4 h-4 rounded" style={{ backgroundColor: CRIME_RATE_COLORS.high }}></div>
|
<div className="w-4 h-4 rounded" style={{ backgroundColor: CRIME_RATE_COLORS.high }}></div>
|
||||||
<span>High Crime Rate</span>
|
<span>High Crime Rate</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator className="bg-white/20 my-2" />
|
<Separator className="bg-white/20 my-3" />
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="font-medium mb-1">Map Markers</h4>
|
<h4 className="font-medium mb-2 text-sm">Map Markers</h4>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 p-1.5 hover:bg-white/5 rounded-md transition-colors">
|
||||||
<AlertCircle className="h-4 w-4 text-red-500" />
|
<AlertCircle className="h-4 w-4 text-red-500" />
|
||||||
<span>Individual Incident</span>
|
<span>Individual Incident</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 p-1.5 hover:bg-white/5 rounded-md transition-colors">
|
||||||
<div className="w-4 h-4 rounded-full bg-blue-500 flex items-center justify-center text-[8px] text-white">5</div>
|
<div className="w-5 h-5 rounded-full bg-pink-400 flex items-center justify-center text-[10px] text-white">5</div>
|
||||||
<span>Incident Cluster</span>
|
<span className="font-bold">Incident Cluster</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
|
|
||||||
<SidebarSection title="About" icon={<Info className="h-4 w-4 text-blue-400" />}>
|
<SidebarSection title="About" icon={<Info className="h-4 w-4 text-green-400" />}>
|
||||||
<Card className="bg-black/20 border border-white/10">
|
<Card className="bg-gradient-to-r from-zinc-800/80 to-zinc-900/80 border border-white/10">
|
||||||
<CardContent className="p-3 text-xs">
|
<CardContent className="p-4 text-xs">
|
||||||
<p className="mb-2">
|
<p className="mb-3">
|
||||||
SIGAP Crime Map provides real-time visualization and analysis
|
SIGAP Crime Map provides real-time visualization and analysis
|
||||||
of crime incidents across Jember region.
|
of crime incidents across Jember region.
|
||||||
</p>
|
</p>
|
||||||
|
@ -598,42 +656,59 @@ export default function CrimeSidebar({
|
||||||
Data is sourced from official police reports and updated
|
Data is sourced from official police reports and updated
|
||||||
daily to ensure accurate information.
|
daily to ensure accurate information.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 text-white/60">
|
<div className="mt-3 p-2 bg-white/5 rounded-lg text-white/60">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>Version</span>
|
<span>Version</span>
|
||||||
<span>1.2.4</span>
|
<span className="font-medium">1.2.4</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between mt-1">
|
||||||
<span>Last Updated</span>
|
<span>Last Updated</span>
|
||||||
<span>June 18, 2024</span>
|
<span className="font-medium">June 18, 2024</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
|
|
||||||
<SidebarSection title="How to Use" icon={<Eye className="h-4 w-4 text-blue-400" />}>
|
<SidebarSection title="How to Use" icon={<Eye className="h-4 w-4 text-green-400" />}>
|
||||||
<Card className="bg-black/20 border border-white/10">
|
<Card className="bg-gradient-to-r from-zinc-800/80 to-zinc-900/80 border border-white/10">
|
||||||
<CardContent className="p-3 text-xs space-y-2">
|
<CardContent className="p-4 text-xs space-y-3">
|
||||||
|
<div className="flex gap-3 items-start">
|
||||||
|
<div className="bg-emerald-900/50 p-1.5 rounded-md">
|
||||||
|
<Filter className="h-3.5 w-3.5 text-emerald-400" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">Filtering</span>
|
<span className="font-medium">Filtering</span>
|
||||||
<p className="text-white/70">
|
<p className="text-white/70 mt-1">
|
||||||
Use the year, month, and category filters at the top to
|
Use the year, month, and category filters at the top to
|
||||||
refine the data shown on the map.
|
refine the data shown on the map.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 items-start">
|
||||||
|
<div className="bg-emerald-900/50 p-1.5 rounded-md">
|
||||||
|
<MapPin className="h-3.5 w-3.5 text-emerald-400" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">District Information</span>
|
<span className="font-medium">District Information</span>
|
||||||
<p className="text-white/70">
|
<p className="text-white/70 mt-1">
|
||||||
Click on any district to view detailed crime statistics for that area.
|
Click on any district to view detailed crime statistics for that area.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 items-start">
|
||||||
|
<div className="bg-emerald-900/50 p-1.5 rounded-md">
|
||||||
|
<AlertTriangle className="h-3.5 w-3.5 text-emerald-400" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">Incidents</span>
|
<span className="font-medium">Incidents</span>
|
||||||
<p className="text-white/70">
|
<p className="text-white/70 mt-1">
|
||||||
Click on incident markers to view details about specific crime reports.
|
Click on incident markers to view details about specific crime reports.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
|
@ -646,14 +721,14 @@ export default function CrimeSidebar({
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute h-12 w-8 bg-background backdrop-blur-sm border-t border-b border-r border-white/10 flex items-center justify-center",
|
"absolute h-12 w-8 bg-background border-t border-b border-r border-sidebar-primary-foreground/30 flex items-center justify-center",
|
||||||
"top-1/2 -translate-y-1/2 transition-all duration-300 ease-in-out",
|
"top-1/2 -translate-y-1/2 transition-all duration-300 ease-in-out",
|
||||||
isCollapsed ? "-right-8 rounded-r-md" : "left-[400px] rounded-r-md",
|
isCollapsed ? "-right-8 rounded-r-md" : "left-[420px] rounded-r-md",
|
||||||
)}
|
)}
|
||||||
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||||
>
|
>
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
className={cn("h-5 w-5 text-white/80 transition-transform",
|
className={cn("h-5 w-5 text-sidebar-primary-foreground transition-transform",
|
||||||
!isCollapsed && "rotate-180")}
|
!isCollapsed && "rotate-180")}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -671,7 +746,7 @@ interface SidebarSectionProps {
|
||||||
function SidebarSection({ title, children, icon }: SidebarSectionProps) {
|
function SidebarSection({ title, children, icon }: SidebarSectionProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-white/80 mb-2 flex items-center gap-1.5">
|
<h3 className="text-sm font-medium text-sidebar-foreground/90 mb-3 flex items-center gap-2 pl-1">
|
||||||
{icon}
|
{icon}
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
|
@ -686,19 +761,29 @@ interface SystemStatusCardProps {
|
||||||
statusIcon: React.ReactNode
|
statusIcon: React.ReactNode
|
||||||
statusColor: string
|
statusColor: string
|
||||||
updatedTime?: string
|
updatedTime?: string
|
||||||
|
bgColor?: string
|
||||||
|
borderColor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function SystemStatusCard({ title, status, statusIcon, statusColor, updatedTime }: SystemStatusCardProps) {
|
function SystemStatusCard({
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
statusIcon,
|
||||||
|
statusColor,
|
||||||
|
updatedTime,
|
||||||
|
bgColor = "bg-sidebar-accent/20",
|
||||||
|
borderColor = "border-sidebar-border"
|
||||||
|
}: SystemStatusCardProps) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-black/20 border border-white/10">
|
<Card className={`${bgColor} border ${borderColor} hover:border-sidebar-border/80 transition-colors`}>
|
||||||
<CardContent className="p-2 text-xs">
|
<CardContent className="p-3 text-xs">
|
||||||
<div className="font-medium mb-1">{title}</div>
|
<div className="font-medium mb-1.5">{title}</div>
|
||||||
<div className={`flex items-center gap-1 ${statusColor}`}>
|
<div className={`flex items-center gap-1.5 ${statusColor} text-base font-semibold`}>
|
||||||
{statusIcon}
|
{statusIcon}
|
||||||
<span>{status}</span>
|
<span>{status}</span>
|
||||||
</div>
|
</div>
|
||||||
{updatedTime && (
|
{updatedTime && (
|
||||||
<div className="text-white/50 text-[10px] mt-1">{updatedTime}</div>
|
<div className="text-sidebar-foreground/50 text-[10px] mt-1.5">{updatedTime}</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -723,8 +808,18 @@ function IncidentCard({ title, time, location, severity }: EnhancedIncidentCardP
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBorderColor = () => {
|
||||||
|
switch (severity) {
|
||||||
|
case "Low": return "border-l-green-500";
|
||||||
|
case "Medium": return "border-l-yellow-500";
|
||||||
|
case "High": return "border-l-orange-500";
|
||||||
|
case "Critical": return "border-l-red-500";
|
||||||
|
default: return "border-l-gray-500";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white/10 border-0 text-white shadow-none">
|
<Card className={`bg-white/5 hover:bg-white/10 border-0 text-white shadow-none transition-colors border-l-2 ${getBorderColor()}`}>
|
||||||
<CardContent className="p-3 text-xs">
|
<CardContent className="p-3 text-xs">
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<AlertTriangle className="h-4 w-4 text-red-400 shrink-0 mt-0.5" />
|
<AlertTriangle className="h-4 w-4 text-red-400 shrink-0 mt-0.5" />
|
||||||
|
@ -733,11 +828,11 @@ function IncidentCard({ title, time, location, severity }: EnhancedIncidentCardP
|
||||||
<p className="font-medium">{title}</p>
|
<p className="font-medium">{title}</p>
|
||||||
<Badge className={`${getBadgeColor()} text-[9px] h-4 ml-1`}>{severity}</Badge>
|
<Badge className={`${getBadgeColor()} text-[9px] h-4 ml-1`}>{severity}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-1 text-white/60">
|
<div className="flex items-center gap-2 mt-1.5 text-white/60">
|
||||||
<MapPin className="h-3 w-3" />
|
<MapPin className="h-3 w-3" />
|
||||||
<span>{location}</span>
|
<span>{location}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-white/60">{time}</div>
|
<div className="mt-1.5 text-white/60">{time}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
@ -750,17 +845,29 @@ interface StatCardProps {
|
||||||
value: string
|
value: string
|
||||||
change: string
|
change: string
|
||||||
isPositive?: boolean
|
isPositive?: boolean
|
||||||
|
icon?: React.ReactNode
|
||||||
|
bgColor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatCard({ title, value, change, isPositive = false }: StatCardProps) {
|
function StatCard({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
change,
|
||||||
|
isPositive = false,
|
||||||
|
icon,
|
||||||
|
bgColor = "bg-white/10"
|
||||||
|
}: StatCardProps) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white/10 border-0 text-white shadow-none">
|
<Card className={`${bgColor} hover:bg-white/15 border-0 text-white shadow-none transition-colors`}>
|
||||||
<CardContent className="p-3">
|
<CardContent className="p-3">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-xs text-white/70">{title}</span>
|
<span className="text-xs text-white/70 flex items-center gap-1.5">
|
||||||
<span className={`text-xs ${isPositive ? "text-green-400" : "text-red-400"}`}>{change}</span>
|
{icon}
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<span className={`text-xs ${isPositive ? "text-green-400" : "text-amber-400"}`}>{change}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-bold mt-1">{value}</div>
|
<div className="text-xl font-bold mt-1.5">{value}</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
@ -774,47 +881,20 @@ interface CrimeTypeCardProps {
|
||||||
|
|
||||||
function CrimeTypeCard({ type, count, percentage }: CrimeTypeCardProps) {
|
function CrimeTypeCard({ type, count, percentage }: CrimeTypeCardProps) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white/10 border-0 text-white shadow-none">
|
<Card className="bg-white/5 hover:bg-white/10 border-0 text-white shadow-none transition-colors">
|
||||||
<CardContent className="p-3">
|
<CardContent className="p-3">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="font-medium">{type}</span>
|
<span className="font-medium">{type}</span>
|
||||||
<span className="text-sm text-white/70">{count} cases</span>
|
<span className="text-sm text-white/70">{count} cases</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 h-1.5 bg-white/20 rounded-full overflow-hidden">
|
<div className="mt-2 h-2 bg-white/10 rounded-full overflow-hidden">
|
||||||
<div className="bg-blue-500 h-full rounded-full" style={{ width: `${percentage}%` }}></div>
|
<div
|
||||||
|
className="bg-gradient-to-r from-primary to-primary/80 h-full rounded-full"
|
||||||
|
style={{ width: `${percentage}%` }}
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-white/70 text-right">{percentage}%</div>
|
<div className="mt-1 text-xs text-white/70 text-right">{percentage}%</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReportCardProps {
|
|
||||||
title: string
|
|
||||||
date: string
|
|
||||||
author: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function ReportCard({ title, date, author }: ReportCardProps) {
|
|
||||||
return (
|
|
||||||
<Card className="bg-white/10 border-0 text-white shadow-none">
|
|
||||||
<CardContent className="p-3 text-xs">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<FileText className="h-4 w-4 text-indigo-400 shrink-0 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">{title}</p>
|
|
||||||
<div className="flex items-center gap-2 mt-1 text-white/60">
|
|
||||||
<Shield className="h-3 w-3" />
|
|
||||||
<span>{author}</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-white/60">{date}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PieChart(props: any) {
|
|
||||||
return <BarChart {...props} />;
|
|
||||||
}
|
|
||||||
|
|
|
@ -127,24 +127,7 @@
|
||||||
--sidebar-border: 0 0% 25%; /* #404040 */
|
--sidebar-border: 0 0% 25%; /* #404040 */
|
||||||
--sidebar-ring: 155 100% 19%; /* #006239 */
|
--sidebar-ring: 155 100% 19%; /* #006239 */
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
:root {
|
|
||||||
--chart-1: 12 76% 61%;
|
|
||||||
--chart-2: 173 58% 39%;
|
|
||||||
--chart-3: 197 37% 24%;
|
|
||||||
--chart-4: 43 74% 66%;
|
|
||||||
--chart-5: 27 87% 67%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--chart-1: 220 70% 50%;
|
|
||||||
--chart-2: 160 60% 45%;
|
|
||||||
--chart-3: 30 80% 55%;
|
|
||||||
--chart-4: 280 65% 60%;
|
|
||||||
--chart-5: 340 75% 55%;
|
|
||||||
}
|
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue