MIF_E31221222/sigap-website/app/_components/map/sidebar/map-sidebar.tsx

901 lines
50 KiB
TypeScript

"use client"
import React, { useState, useEffect, useMemo } from "react"
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'
import { Separator } from "@/app/_components/ui/separator"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/app/_components/ui/card"
import { cn } from "@/app/_lib/utils"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/tabs"
import { Badge } from "@/app/_components/ui/badge"
import { Button } from "@/app/_components/ui/button"
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 { getMonthName, formatDateString } from "@/app/_utils/common"
import { Skeleton } from "@/app/_components/ui/skeleton"
import { useGetCrimeCategories } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries"
import { $Enums } from "@prisma/client"
interface CrimeSidebarProps {
className?: string
defaultCollapsed?: boolean
selectedCategory?: string | "all"
selectedYear?: number
selectedMonth?: number | "all"
}
// Updated interface to match the structure returned by getCrimeByYearAndMonth
interface ICrimesProps {
id: string
district_id: string
districts: {
name: string
geographics?: {
address: string
land_area: number
year: number
}[]
demographics?: {
number_of_unemployed: number
population: number
population_density: number
year: number
}[]
}
number_of_crime: number
level: $Enums.crime_rates
score: number
month: number
year: number
crime_incidents: Array<{
id: string
timestamp: Date
description: string
status: string
crime_categories: {
name: string
type: string
}
locations: {
address: string
latitude: number
longitude: number
}
}>
}
export default function CrimeSidebar({
className,
defaultCollapsed = true,
selectedCategory = "all",
selectedYear: propSelectedYear,
selectedMonth: propSelectedMonth
}: CrimeSidebarProps) {
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed)
const [activeTab, setActiveTab] = useState("incidents")
const [currentTime, setCurrentTime] = useState<Date>(new Date())
const [location, setLocation] = useState<string>("Jember, East Java")
const {
availableYears,
isYearsLoading,
crimes,
isCrimesLoading,
crimesError,
selectedYear: hookSelectedYear,
selectedMonth: hookSelectedMonth,
} = usePrefetchedCrimeData()
// Use props for selectedYear and selectedMonth if provided, otherwise fall back to hook values
const selectedYear = propSelectedYear || hookSelectedYear
const selectedMonth = propSelectedMonth || hookSelectedMonth
// Update current time every minute for the real-time display
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date())
}, 60000)
return () => clearInterval(timer)
}, [])
// Format date with selected year and month if provided
const getDisplayDate = () => {
// If we have a specific month selected, use that for display
if (selectedMonth && selectedMonth !== 'all') {
const date = new Date()
date.setFullYear(selectedYear)
date.setMonth(Number(selectedMonth) - 1) // Month is 0-indexed in JS Date
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long'
}).format(date)
}
// Otherwise show today's date
return new Intl.DateTimeFormat('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(currentTime)
}
const formattedDate = getDisplayDate()
const formattedTime = new Intl.DateTimeFormat('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: true
}).format(currentTime)
const { data: categoriesData } = useGetCrimeCategories()
const crimeStats = useMemo(() => {
// Return default values if crimes is undefined, null, or not an array
if (!crimes || !Array.isArray(crimes) || crimes.length === 0) return {
todaysIncidents: 0,
totalIncidents: 0,
recentIncidents: [],
categoryCounts: {},
districts: {},
incidentsByMonth: Array(12).fill(0),
clearanceRate: 0
}
// Make sure we have a valid array to work with
let filteredCrimes = [...crimes]
if (selectedCategory !== "all") {
filteredCrimes = crimes.filter((crime: ICrimesProps) =>
crime.crime_incidents.some(incident =>
incident.crime_categories.name === selectedCategory
)
)
}
// Collect all incidents from all crimes
const allIncidents = filteredCrimes.flatMap((crime: ICrimesProps) =>
crime.crime_incidents.map(incident => ({
id: incident.id,
timestamp: incident.timestamp,
description: incident.description,
status: incident.status,
category: incident.crime_categories.name,
type: incident.crime_categories.type,
address: incident.locations.address,
latitude: incident.locations.latitude,
longitude: incident.locations.longitude
}))
)
const totalIncidents = allIncidents.length
const today = new Date()
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(today.getDate() - 30)
const recentIncidents = allIncidents
.filter((incident) => {
if (!incident?.timestamp) return false
const incidentDate = new Date(incident.timestamp)
return incidentDate >= thirtyDaysAgo
})
.sort((a, b) => {
const bTime = b?.timestamp ? new Date(b.timestamp).getTime() : 0
const aTime = a?.timestamp ? new Date(a.timestamp).getTime() : 0
return bTime - aTime
})
const todaysIncidents = recentIncidents.filter((incident) => {
const incidentDate = incident?.timestamp
? new Date(incident.timestamp)
: new Date(0)
return incidentDate.toDateString() === today.toDateString()
}).length
const categoryCounts = allIncidents.reduce((acc: Record<string, number>, incident) => {
const category = incident?.category || 'Unknown'
acc[category] = (acc[category] || 0) + 1
return acc
}, {} as Record<string, number>)
const districts = filteredCrimes.reduce((acc: Record<string, number>, crime: ICrimesProps) => {
const districtName = crime.districts.name || 'Unknown'
acc[districtName] = (acc[districtName] || 0) + (crime.number_of_crime || 0)
return acc
}, {} as Record<string, number>)
const incidentsByMonth = Array(12).fill(0)
allIncidents.forEach((incident) => {
if (!incident?.timestamp) return;
const date = new Date(incident.timestamp)
const month = date.getMonth()
if (month >= 0 && month < 12) {
incidentsByMonth[month]++
}
})
const resolvedIncidents = allIncidents.filter(incident =>
incident?.status?.toLowerCase() === "resolved"
).length
const clearanceRate = totalIncidents > 0 ?
Math.round((resolvedIncidents / totalIncidents) * 100) : 0
return {
todaysIncidents,
totalIncidents,
recentIncidents: recentIncidents.slice(0, 10),
categoryCounts,
districts,
incidentsByMonth,
clearanceRate
}
}, [crimes, selectedCategory])
// Generate a time period display for the current view
const getTimePeriodDisplay = () => {
if (selectedMonth && selectedMonth !== 'all') {
return `${getMonthName(Number(selectedMonth))} ${selectedYear}`
}
return `${selectedYear} - All months`
}
const topCategories = useMemo(() => {
if (!crimeStats.categoryCounts) return []
return Object.entries(crimeStats.categoryCounts)
.sort((a, b) => (b[1] as number) - (a[1] as number))
.slice(0, 4)
.map(([type, count]) => {
const percentage = Math.round(((count as number) / crimeStats.totalIncidents) * 100) || 0
return { type, count: count as number, percentage }
})
}, [crimeStats])
const getTimeAgo = (timestamp: string | Date) => {
const now = new Date()
const eventTime = new Date(timestamp)
const diffMs = now.getTime() - eventTime.getTime()
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMins / 60)
const diffDays = Math.floor(diffHours / 24)
if (diffDays > 0) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`
if (diffHours > 0) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`
if (diffMins > 0) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`
return 'just now'
}
const getIncidentSeverity = (incident: any): "Low" | "Medium" | "High" | "Critical" => {
if (!incident) return "Low";
const category = incident.category || "Unknown";
const highSeverityCategories = [
'Pembunuhan', 'Perkosaan', 'Penculikan', 'Lahgun Senpi/Handak/Sajam',
'PTPPO', 'Trafficking In Person'
]
const mediumSeverityCategories = [
'Penganiayaan Berat', 'Penganiayaan Ringan', 'Pencurian Biasa', 'Curat',
'Curas', 'Curanmor', 'Pengeroyokan', 'PKDRT', 'Penggelapan', 'Pengrusakan'
]
if (highSeverityCategories.includes(category)) return "High"
if (mediumSeverityCategories.includes(category)) return "Medium"
if (incident.type === "Pidana Tertentu") return "Medium"
return "Low"
}
return (
<div className={cn(
"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",
className
)}>
<div className="relative h-full flex items-stretch">
<div className="bg-background backdrop-blur-sm border-r border-sidebar-border h-full w-[420px]">
<div className="p-4 text-sidebar-foreground h-full flex flex-col max-h-full overflow-hidden">
{/* Header with improved styling */}
<CardHeader className="p-0 pb-4 shrink-0 relative">
<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
</CardTitle>
{!isCrimesLoading && (
<CardDescription className="text-sm text-sidebar-foreground/70">
{getTimePeriodDisplay()}
</CardDescription>
)}
</div>
</div>
</CardHeader>
{/* Improved tabs with pill style */}
<Tabs
defaultValue="incidents"
className="w-full flex-1 flex flex-col overflow-hidden"
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>
<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">
{isCrimesLoading ? (
<div className="space-y-4">
<Skeleton className="h-24 w-full" />
<div className="grid grid-cols-2 gap-2">
<Skeleton className="h-16 w-full" />
<Skeleton className="h-16 w-full" />
<Skeleton className="h-16 w-full" />
<Skeleton className="h-16 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
</div>
</div>
) : (
<>
{/* Enhanced info card */}
<Card className="bg-gradient-to-r from-sidebar-primary/30 to-sidebar-primary/20 border border-sidebar-primary/20 overflow-hidden">
<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">
<Calendar className="h-4 w-4 text-sidebar-primary" />
<span className="font-medium">{formattedDate}</span>
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-sidebar-primary" />
<span>{formattedTime}</span>
</div>
</div>
<div className="flex items-center gap-2 mb-3">
<MapPin className="h-4 w-4 text-sidebar-primary" />
<span className="text-sidebar-foreground/70">{location}</span>
</div>
<div className="flex items-center gap-2 bg-sidebar-accent/30 p-2 rounded-lg">
<AlertTriangle className="h-5 w-5 text-amber-400" />
<span>
<strong>{crimeStats.totalIncidents || 0}</strong> incidents reported
{selectedMonth !== 'all' ? ` in ${getMonthName(Number(selectedMonth))}` : ` in ${selectedYear}`}
</span>
</div>
</CardContent>
</Card>
{/* Enhanced stat cards */}
<div className="grid grid-cols-2 gap-3">
<SystemStatusCard
title="Total Cases"
status={`${crimeStats?.totalIncidents || 0}`}
statusIcon={<AlertCircle className="h-4 w-4 text-green-400" />}
statusColor="text-green-400"
updatedTime={getTimePeriodDisplay()}
bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20"
borderColor="border-sidebar-border"
/>
<SystemStatusCard
title="Recent Cases"
status={`${crimeStats?.recentIncidents?.length || 0}`}
statusIcon={<Clock className="h-4 w-4 text-amber-400" />}
statusColor="text-amber-400"
updatedTime="Last 30 days"
bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20"
borderColor="border-sidebar-border"
/>
<SystemStatusCard
title="Top Category"
status={topCategories.length > 0 ? topCategories[0].type : "None"}
statusIcon={<Shield className="h-4 w-4 text-green-400" />}
statusColor="text-green-400"
bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20"
borderColor="border-sidebar-border"
/>
<SystemStatusCard
title="Districts"
status={`${Object.keys(crimeStats.districts).length}`}
statusIcon={<MapPin className="h-4 w-4 text-purple-400" />}
statusColor="text-purple-400"
updatedTime="Affected areas"
bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20"
borderColor="border-sidebar-border"
/>
</div>
<SidebarSection
title={selectedCategory !== "all"
? `${selectedCategory} Incidents`
: "Recent Incidents"}
icon={<AlertTriangle className="h-4 w-4 text-red-400" />}
>
{crimeStats.recentIncidents.length === 0 ? (
<Card className="bg-white/5 border-0 text-white shadow-none">
<CardContent className="p-4 text-center">
<div className="flex flex-col items-center gap-2">
<AlertCircle className="h-6 w-6 text-white/40" />
<p className="text-sm text-white/70">
{selectedCategory !== "all"
? `No ${selectedCategory} incidents found`
: "No recent incidents reported"}
</p>
<p className="text-xs text-white/50">Try adjusting your filters or checking back later</p>
</div>
</CardContent>
</Card>
) : (
<div className="space-y-3">
{crimeStats.recentIncidents.slice(0, 6).map((incident) => (
<IncidentCard
key={incident.id}
title={`${incident.category || 'Unknown'} in ${incident.address?.split(',')[0] || 'Unknown Location'}`}
time={incident.timestamp ? getTimeAgo(incident.timestamp) : 'Unknown time'}
location={incident.address?.split(',').slice(1, 3).join(', ') || 'Unknown Location'}
severity={getIncidentSeverity(incident)}
/>
))}
</div>
)}
</SidebarSection>
</>
)}
</TabsContent>
<TabsContent value="statistics" className="m-0 p-0 space-y-4">
{isCrimesLoading ? (
<div className="space-y-4">
<Skeleton className="h-64 w-full" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-36 w-full" />
<Skeleton className="h-48 w-full" />
</div>
) : (
<>
<Card className="bg-gradient-to-r from-sidebar-primary/30 to-sidebar-primary/20 border border-sidebar-primary/20 overflow-hidden">
<CardHeader className="p-3 pb-0">
<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>
</CardHeader>
<CardContent className="p-3">
<div className="h-32 flex items-end gap-1 mt-2">
{crimeStats.incidentsByMonth.map((count, i) => {
const maxCount = Math.max(...crimeStats.incidentsByMonth)
const height = maxCount > 0 ? (count / maxCount) * 100 : 0
return (
<div
key={i}
className={cn(
"bg-gradient-to-t from-emerald-600 to-green-400 w-full rounded-t-md",
selectedMonth !== 'all' && i + 1 === Number(selectedMonth) ? "from-amber-500 to-amber-400" : ""
)}
style={{
height: `${Math.max(5, height)}%`,
opacity: 0.7 + (i / 24)
}}
title={`${getMonthName(i + 1)}: ${count} incidents`}
/>
)
})}
</div>
<div className="flex justify-between mt-2 text-[10px] text-white/60">
<span>Jan</span>
<span>Feb</span>
<span>Mar</span>
<span>Apr</span>
<span>May</span>
<span>Jun</span>
<span>Jul</span>
<span>Aug</span>
<span>Sep</span>
<span>Oct</span>
<span>Nov</span>
<span>Dec</span>
</div>
</CardContent>
</Card>
<SidebarSection title="Crime Overview" icon={<Activity className="h-4 w-4 text-blue-400" />}>
<div className="space-y-3">
<StatCard
title="Total Incidents"
value={crimeStats.totalIncidents.toString()}
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
title={selectedMonth !== 'all' ?
`${getMonthName(Number(selectedMonth))} Cases` :
"Monthly Average"}
value={selectedMonth !== 'all' ?
crimeStats.totalIncidents.toString() :
Math.round(crimeStats.totalIncidents /
(crimeStats.incidentsByMonth.filter(c => c > 0).length || 1)
).toString()}
change={selectedMonth !== 'all' ?
`in ${getMonthName(Number(selectedMonth))}` :
"per active month"}
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
title="Clearance Rate"
value={`${crimeStats.clearanceRate}%`}
change="of cases resolved"
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>
</SidebarSection>
<Separator className="bg-white/20 my-4" />
<SidebarSection title="Most Common Crimes" icon={<PieChart className="h-4 w-4 text-amber-400" />}>
<div className="space-y-3">
{topCategories.length > 0 ? (
topCategories.map((category) => (
<CrimeTypeCard
key={category.type}
type={category.type}
count={category.count}
percentage={category.percentage}
/>
))
) : (
<Card className="bg-white/5 border-0 text-white shadow-none">
<CardContent className="p-4 text-center">
<div className="flex flex-col items-center gap-2">
<FileText className="h-6 w-6 text-white/40" />
<p className="text-sm text-white/70">No crime data available</p>
<p className="text-xs text-white/50">Try selecting a different time period</p>
</div>
</CardContent>
</Card>
)}
</div>
</SidebarSection>
</>
)}
</TabsContent>
<TabsContent value="info" className="m-0 p-0 space-y-4">
<SidebarSection title="Map Legend" icon={<Layers className="h-4 w-4 text-green-400" />}>
<Card className="bg-gradient-to-r from-zinc-800/80 to-zinc-900/80 border border-white/10">
<CardContent className="p-4 text-xs space-y-3">
<div className="space-y-2">
<h4 className="font-medium mb-2 text-sm">Crime Severity</h4>
<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>
<span>Low Crime Rate</span>
</div>
<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>
<span>Medium Crime Rate</span>
</div>
<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>
<span>High Crime Rate</span>
</div>
</div>
<Separator className="bg-white/20 my-3" />
<div className="space-y-2">
<h4 className="font-medium mb-2 text-sm">Map Markers</h4>
<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" />
<span>Individual Incident</span>
</div>
<div className="flex items-center gap-2 p-1.5 hover:bg-white/5 rounded-md transition-colors">
<div className="w-5 h-5 rounded-full bg-pink-400 flex items-center justify-center text-[10px] text-white">5</div>
<span className="font-bold">Incident Cluster</span>
</div>
</div>
</CardContent>
</Card>
</SidebarSection>
<SidebarSection title="About" icon={<Info className="h-4 w-4 text-green-400" />}>
<Card className="bg-gradient-to-r from-zinc-800/80 to-zinc-900/80 border border-white/10">
<CardContent className="p-4 text-xs">
<p className="mb-3">
SIGAP Crime Map provides real-time visualization and analysis
of crime incidents across Jember region.
</p>
<p>
Data is sourced from official police reports and updated
daily to ensure accurate information.
</p>
<div className="mt-3 p-2 bg-white/5 rounded-lg text-white/60">
<div className="flex justify-between">
<span>Version</span>
<span className="font-medium">1.2.4</span>
</div>
<div className="flex justify-between mt-1">
<span>Last Updated</span>
<span className="font-medium">June 18, 2024</span>
</div>
</div>
</CardContent>
</Card>
</SidebarSection>
<SidebarSection title="How to Use" icon={<Eye className="h-4 w-4 text-green-400" />}>
<Card className="bg-gradient-to-r from-zinc-800/80 to-zinc-900/80 border border-white/10">
<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>
<span className="font-medium">Filtering</span>
<p className="text-white/70 mt-1">
Use the year, month, and category filters at the top to
refine the data shown on the map.
</p>
</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>
<span className="font-medium">District Information</span>
<p className="text-white/70 mt-1">
Click on any district to view detailed crime statistics for that area.
</p>
</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>
<span className="font-medium">Incidents</span>
<p className="text-white/70 mt-1">
Click on incident markers to view details about specific crime reports.
</p>
</div>
</div>
</CardContent>
</Card>
</SidebarSection>
</TabsContent>
</div>
</Tabs>
</div>
</div>
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className={cn(
"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",
isCollapsed ? "-right-8 rounded-r-md" : "left-[420px] rounded-r-md",
)}
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>
<ChevronRight
className={cn("h-5 w-5 text-sidebar-primary-foreground transition-transform",
!isCollapsed && "rotate-180")}
/>
</button>
</div>
</div>
)
}
interface SidebarSectionProps {
title: string
children: React.ReactNode
icon?: React.ReactNode
}
function SidebarSection({ title, children, icon }: SidebarSectionProps) {
return (
<div>
<h3 className="text-sm font-medium text-sidebar-foreground/90 mb-3 flex items-center gap-2 pl-1">
{icon}
{title}
</h3>
{children}
</div>
)
}
interface SystemStatusCardProps {
title: string
status: string
statusIcon: React.ReactNode
statusColor: string
updatedTime?: string
bgColor?: string
borderColor?: string
}
function SystemStatusCard({
title,
status,
statusIcon,
statusColor,
updatedTime,
bgColor = "bg-sidebar-accent/20",
borderColor = "border-sidebar-border"
}: SystemStatusCardProps) {
return (
<Card className={`${bgColor} border ${borderColor} hover:border-sidebar-border/80 transition-colors`}>
<CardContent className="p-3 text-xs">
<div className="font-medium mb-1.5">{title}</div>
<div className={`flex items-center gap-1.5 ${statusColor} text-base font-semibold`}>
{statusIcon}
<span>{status}</span>
</div>
{updatedTime && (
<div className="text-sidebar-foreground/50 text-[10px] mt-1.5">{updatedTime}</div>
)}
</CardContent>
</Card>
);
}
interface EnhancedIncidentCardProps {
title: string
time: string
location: string
severity: "Low" | "Medium" | "High" | "Critical"
}
function IncidentCard({ title, time, location, severity }: EnhancedIncidentCardProps) {
const getBadgeColor = () => {
switch (severity) {
case "Low": return "bg-green-500/20 text-green-300";
case "Medium": return "bg-yellow-500/20 text-yellow-300";
case "High": return "bg-orange-500/20 text-orange-300";
case "Critical": return "bg-red-500/20 text-red-300";
default: return "bg-gray-500/20 text-gray-300";
}
};
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 (
<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">
<div className="flex items-start gap-2">
<AlertTriangle className="h-4 w-4 text-red-400 shrink-0 mt-0.5" />
<div>
<div className="flex items-center justify-between">
<p className="font-medium">{title}</p>
<Badge className={`${getBadgeColor()} text-[9px] h-4 ml-1`}>{severity}</Badge>
</div>
<div className="flex items-center gap-2 mt-1.5 text-white/60">
<MapPin className="h-3 w-3" />
<span>{location}</span>
</div>
<div className="mt-1.5 text-white/60">{time}</div>
</div>
</div>
</CardContent>
</Card>
);
}
interface StatCardProps {
title: string
value: string
change: string
isPositive?: boolean
icon?: React.ReactNode
bgColor?: string
}
function StatCard({
title,
value,
change,
isPositive = false,
icon,
bgColor = "bg-white/10"
}: StatCardProps) {
return (
<Card className={`${bgColor} hover:bg-white/15 border-0 text-white shadow-none transition-colors`}>
<CardContent className="p-3">
<div className="flex justify-between items-center">
<span className="text-xs text-white/70 flex items-center gap-1.5">
{icon}
{title}
</span>
<span className={`text-xs ${isPositive ? "text-green-400" : "text-amber-400"}`}>{change}</span>
</div>
<div className="text-xl font-bold mt-1.5">{value}</div>
</CardContent>
</Card>
)
}
interface CrimeTypeCardProps {
type: string
count: number
percentage: number
}
function CrimeTypeCard({ type, count, percentage }: CrimeTypeCardProps) {
return (
<Card className="bg-white/5 hover:bg-white/10 border-0 text-white shadow-none transition-colors">
<CardContent className="p-3">
<div className="flex justify-between items-center">
<span className="font-medium">{type}</span>
<span className="text-sm text-white/70">{count} cases</span>
</div>
<div className="mt-2 h-2 bg-white/10 rounded-full overflow-hidden">
<div
className="bg-gradient-to-r from-primary to-primary/80 h-full rounded-full"
style={{ width: `${percentage}%` }}
></div>
</div>
<div className="mt-1 text-xs text-white/70 text-right">{percentage}%</div>
</CardContent>
</Card>
)
}