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:
vergiLgood1 2025-05-03 00:11:13 +07:00
parent 428986c927
commit 91f9574285
4 changed files with 567 additions and 378 deletions

View File

@ -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>

View File

@ -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>
)} )}

View File

@ -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} />;
}

View File

@ -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;
} }