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

268 lines
12 KiB
TypeScript

"use client"
import React, { useState, useEffect } from "react"
import { AlertTriangle, Calendar, Clock, MapPin, X, ChevronRight } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardDescription } from "@/app/_components/ui/card"
import { cn } from "@/app/_lib/utils"
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/app/_components/ui/tabs"
import { Button } from "@/app/_components/ui/button"
import { Skeleton } from "@/app/_components/ui/skeleton"
import { useMap } from "react-map-gl/mapbox"
import { ICrimes } from "@/app/_utils/types/crimes"
// Import sidebar components
import { SidebarIncidentsTab } from "./tabs/incidents-tab"
import { useCrimeAnalytics } from "../../../(pages)/(admin)/dashboard/crime-management/crime-overview/_hooks/use-crime-analytics"
import { usePagination } from "../../../_hooks/use-pagination"
import { getMonthName } from "@/app/_utils/common"
import { SidebarInfoTab } from "./tabs/info-tab"
import { SidebarStatisticsTab } from "./tabs/statistics-tab"
interface CrimeSidebarProps {
className?: string
defaultCollapsed?: boolean
selectedCategory?: string | "all"
selectedYear: number
selectedMonth?: number | "all"
crimes: ICrimes[]
isLoading?: boolean
}
export default function CrimeSidebar({
className,
defaultCollapsed = true,
selectedCategory = "all",
selectedYear,
selectedMonth,
crimes = [],
isLoading = false,
}: CrimeSidebarProps) {
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed)
const [activeTab, setActiveTab] = useState("incidents")
const [activeIncidentTab, setActiveIncidentTab] = useState("recent")
const [currentTime, setCurrentTime] = useState<Date>(new Date())
const [location, setLocation] = useState<string>("Jember, East Java")
// Get the map instance to use for flyTo
const { current: map } = useMap()
// Use custom hooks for analytics and pagination
const crimeStats = useCrimeAnalytics(crimes)
const { paginationState, handlePageChange } = usePagination(crimeStats.availableMonths)
// 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)
// Generate a time period display for the current view
const getTimePeriodDisplay = () => {
if (selectedMonth && selectedMonth !== 'all') {
return `${getMonthName(Number(selectedMonth))} ${selectedYear}`
}
return `${selectedYear} - All months`
}
// Function to fly to incident location when clicked
const handleIncidentClick = (incident: any) => {
if (!map || !incident.longitude || !incident.latitude) return
// Fly to the incident location
map.flyTo({
center: [incident.longitude, incident.latitude],
zoom: 15,
pitch: 0,
bearing: 0,
duration: 1500,
easing: (t) => t * (2 - t), // easeOutQuad
})
// Create and dispatch a custom event for the incident click
const customEvent = new CustomEvent("incident_click", {
detail: incident,
bubbles: true
})
if (map.getMap().getCanvas()) {
map.getMap().getCanvas().dispatchEvent(customEvent)
} else {
document.dispatchEvent(customEvent)
}
}
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>
{!isLoading && (
<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">
{isLoading ? (
<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>
) : (
<>
<TabsContent value="incidents" className="m-0 p-0 space-y-4">
<SidebarIncidentsTab
crimeStats={crimeStats}
formattedDate={formattedDate}
formattedTime={formattedTime}
location={location}
selectedMonth={selectedMonth}
selectedYear={selectedYear}
selectedCategory={selectedCategory}
getTimePeriodDisplay={getTimePeriodDisplay}
paginationState={paginationState}
handlePageChange={handlePageChange}
handleIncidentClick={handleIncidentClick}
activeIncidentTab={activeIncidentTab}
setActiveIncidentTab={setActiveIncidentTab}
/>
</TabsContent>
<TabsContent value="statistics" className="m-0 p-0 space-y-4">
<SidebarStatisticsTab
crimeStats={crimeStats}
selectedMonth={selectedMonth}
selectedYear={selectedYear}
/>
</TabsContent>
<TabsContent value="info" className="m-0 p-0 space-y-4">
<SidebarInfoTab />
</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>
)
}