feat: Add Sidebar Statistics Tab for crime analytics overview

- Implemented SidebarStatisticsTab component to display monthly incidents and crime overview statistics.
- Integrated StatCard components for total incidents, monthly average, and clearance rate.
- Added visualization for incidents by month with dynamic height based on incident counts.
- Included CrimeTypeCard for displaying the most common crimes with percentage breakdown.

feat: Create Additional Tooltips for enhanced map controls

- Developed AdditionalTooltips component to provide year, month, and category selection.
- Integrated MonthSelector, YearSelector, and CategorySelector for filtering options.
- Added functionality to toggle visibility of selectors.

feat: Implement Crime Tooltips for crime data controls

- Created CrimeTooltips component to manage various crime data views.
- Included tooltips for incidents, heatmap, trends, patrol areas, clusters, and timeline.

feat: Enhance Search Control with incident search capabilities

- Developed SearchTooltip component for searching incidents by various criteria.
- Implemented suggestion filtering based on selected search type (crime ID, incident ID, coordinates, description, address).
- Added functionality to display detailed information about selected incidents.

feat: Consolidate tooltips into a unified Tooltip component

- Merged CrimeTooltips, AdditionalTooltips, and SearchTooltip into a single Tooltips component.
- Streamlined props for managing active controls and selected filters.

feat: Add Map Legend for crime rate visualization

- Created MapLegend component to visually represent crime rates using color coding.
- Integrated with existing map overlay for better user experience.
This commit is contained in:
vergiLgood1 2025-05-05 01:11:37 +07:00
parent 5b57476437
commit 8f12715072
26 changed files with 467 additions and 718 deletions

View File

@ -1,7 +1,7 @@
"use client"
import { Checkbox } from "@/app/_components/ui/checkbox"
import { Label } from "@/app/_components/ui/label"
import { Overlay } from "../overlay"
import { Overlay } from "../../overlay"
import { ControlPosition } from "mapbox-gl"

View File

@ -1,66 +0,0 @@
import { Map } from "mapbox-gl";
/* Idea from Stack Overflow https://stackoverflow.com/a/51683226 */
export class CustomControl {
private _className: string;
private _title: string;
private _eventHandler: (event: MouseEvent) => void;
private _btn!: HTMLButtonElement;
private _container!: HTMLDivElement;
private _map?: Map;
private _root: any; // React root for rendering our component
constructor({
className = "",
title = "",
eventHandler = () => { }
}: {
className?: string;
title?: string;
eventHandler?: (event: MouseEvent) => void;
}) {
this._className = className;
this._title = title;
this._eventHandler = eventHandler;
}
onAdd(map: Map) {
this._map = map;
this._btn = document.createElement("button");
this._btn.className = "mapboxgl-ctrl-icon" + " " + this._className;
this._btn.type = "button";
this._btn.title = this._title;
this._btn.onclick = this._eventHandler;
// Apply pointer-events: auto; style dynamically
this._btn.style.pointerEvents = "auto";
// Dynamically append the style to the auto-generated className
const styleSheet = document.styleSheets[0];
styleSheet.insertRule(
`.${this._className} { pointer-events: auto; }`,
styleSheet.cssRules.length
);
this._container = document.createElement("div");
this._container.className = "mapboxgl-ctrl-group mapboxgl-ctrl";
this._container.appendChild(this._btn);
return this._container;
}
onRemove() {
if (this._container && this._container.parentNode) {
this._container.parentNode.removeChild(this._container);
}
// Defer unmounting React component to prevent race conditions
if (this._root) {
setTimeout(() => {
this._root.unmount();
});
}
this._map = undefined;
}
}

View File

@ -13,11 +13,11 @@ 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"
import { useCrimeAnalytics } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_hooks/use-crime-analytics"
import { usePagination } from "@/app/_hooks/use-pagination"
interface CrimeSidebarProps {
className?: string
@ -211,35 +211,35 @@ export default function CrimeSidebar({
</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="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="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>
<TabsContent value="info" className="m-0 p-0 space-y-4">
<SidebarInfoTab />
</TabsContent>
</>
)}
</div>

View File

@ -4,8 +4,9 @@ import React from "react"
import { Button } from "@/app/_components/ui/button"
import { cn } from "@/app/_lib/utils"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { Overlay } from "../overlay"
import type { ControlPosition } from "mapbox-gl"
import { Overlay } from "../../../overlay"
interface SidebarToggleProps {
isCollapsed: boolean

View File

@ -1,107 +1,107 @@
"use client"
// "use client"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
import { Button } from "@/app/_components/ui/button"
import { FilterX } from "lucide-react"
import { useCallback } from "react"
// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
// import { Button } from "@/app/_components/ui/button"
// import { FilterX } from "lucide-react"
// import { useCallback } from "react"
interface MapFilterControlProps {
selectedYear: number
selectedMonth: number | "all"
availableYears: (number | null)[]
yearsLoading: boolean
onYearChange: (year: number) => void
onMonthChange: (month: number | "all") => void
onApplyFilters: () => void
onResetFilters: () => void
}
// interface MapFilterControlProps {
// selectedYear: number
// selectedMonth: number | "all"
// availableYears: (number | null)[]
// yearsLoading: boolean
// onYearChange: (year: number) => void
// onMonthChange: (month: number | "all") => void
// onApplyFilters: () => void
// onResetFilters: () => void
// }
const months = [
{ value: "1", label: "January" },
{ value: "2", label: "February" },
{ value: "3", label: "March" },
{ value: "4", label: "April" },
{ value: "5", label: "May" },
{ value: "6", label: "June" },
{ value: "7", label: "July" },
{ value: "8", label: "August" },
{ value: "9", label: "September" },
{ value: "10", label: "October" },
{ value: "11", label: "November" },
{ value: "12", label: "December" },
]
// const months = [
// { value: "1", label: "January" },
// { value: "2", label: "February" },
// { value: "3", label: "March" },
// { value: "4", label: "April" },
// { value: "5", label: "May" },
// { value: "6", label: "June" },
// { value: "7", label: "July" },
// { value: "8", label: "August" },
// { value: "9", label: "September" },
// { value: "10", label: "October" },
// { value: "11", label: "November" },
// { value: "12", label: "December" },
// ]
export default function MapFilterControl({
selectedYear,
selectedMonth,
availableYears,
yearsLoading,
onYearChange,
onMonthChange,
onApplyFilters,
onResetFilters,
}: MapFilterControlProps) {
const handleYearChange = useCallback(
(value: string) => {
onYearChange(Number(value))
},
[onYearChange],
)
// export default function MapFilterControl({
// selectedYear,
// selectedMonth,
// availableYears,
// yearsLoading,
// onYearChange,
// onMonthChange,
// onApplyFilters,
// onResetFilters,
// }: MapFilterControlProps) {
// const handleYearChange = useCallback(
// (value: string) => {
// onYearChange(Number(value))
// },
// [onYearChange],
// )
const handleMonthChange = useCallback(
(value: string) => {
onMonthChange(value === "all" ? "all" : Number(value))
},
[onMonthChange],
)
// const handleMonthChange = useCallback(
// (value: string) => {
// onMonthChange(value === "all" ? "all" : Number(value))
// },
// [onMonthChange],
// )
const isDefaultFilter = selectedYear === 2024 && selectedMonth === "all"
// const isDefaultFilter = selectedYear === 2024 && selectedMonth === "all"
return (
<div className="absolute top-20 right-2 z-10 bg-white bg-opacity-90 p-2 rounded-md shadow-lg flex flex-col gap-2 max-w-[220px]">
<div className="text-sm font-medium mb-1">Map Filters</div>
// return (
// <div className="absolute top-20 right-2 z-10 bg-white bg-opacity-90 p-2 rounded-md shadow-lg flex flex-col gap-2 max-w-[220px]">
// <div className="text-sm font-medium mb-1">Map Filters</div>
<div className="grid grid-cols-1 gap-2">
<Select value={selectedYear.toString()} onValueChange={handleYearChange}>
<SelectTrigger className="h-8 w-full">
<SelectValue placeholder="Year" />
</SelectTrigger>
<SelectContent>
{!yearsLoading &&
availableYears
?.filter((year) => year !== null)
.map((year) => (
<SelectItem key={year} value={year!.toString()}>
{year}
</SelectItem>
))}
</SelectContent>
</Select>
// <div className="grid grid-cols-1 gap-2">
// <Select value={selectedYear.toString()} onValueChange={handleYearChange}>
// <SelectTrigger className="h-8 w-full">
// <SelectValue placeholder="Year" />
// </SelectTrigger>
// <SelectContent>
// {!yearsLoading &&
// availableYears
// ?.filter((year) => year !== null)
// .map((year) => (
// <SelectItem key={year} value={year!.toString()}>
// {year}
// </SelectItem>
// ))}
// </SelectContent>
// </Select>
<Select value={selectedMonth.toString()} onValueChange={handleMonthChange}>
<SelectTrigger className="h-8 w-full">
<SelectValue placeholder="Month" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Months</SelectItem>
{months.map((month) => (
<SelectItem key={month.value} value={month.value}>
{month.label}
</SelectItem>
))}
</SelectContent>
</Select>
// <Select value={selectedMonth.toString()} onValueChange={handleMonthChange}>
// <SelectTrigger className="h-8 w-full">
// <SelectValue placeholder="Month" />
// </SelectTrigger>
// <SelectContent>
// <SelectItem value="all">All Months</SelectItem>
// {months.map((month) => (
// <SelectItem key={month.value} value={month.value}>
// {month.label}
// </SelectItem>
// ))}
// </SelectContent>
// </Select>
<div className="flex gap-1">
<Button className="h-8 text-xs flex-1" variant="default" onClick={onApplyFilters}>
Apply
</Button>
<Button className="h-8 text-xs" variant="ghost" onClick={onResetFilters} disabled={isDefaultFilter}>
<FilterX className="h-3 w-3 mr-1" />
Reset
</Button>
</div>
</div>
</div>
)
}
// <div className="flex gap-1">
// <Button className="h-8 text-xs flex-1" variant="default" onClick={onApplyFilters}>
// Apply
// </Button>
// <Button className="h-8 text-xs" variant="ghost" onClick={onResetFilters} disabled={isDefaultFilter}>
// <FilterX className="h-3 w-3 mr-1" />
// Reset
// </Button>
// </div>
// </div>
// </div>
// )
// }

View File

@ -1,184 +0,0 @@
"use client"
import { ChevronLeft, ChevronRight, Cloud, Droplets, Wind } from "lucide-react"
import { Button } from "@/app/_components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/app/_components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/tabs"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/app/_components/ui/collapsible"
import { cn } from "@/app/_lib/utils"
interface MapSidebarProps {
isOpen: boolean
onToggle: () => void
crimes?: Array<{
id: string
district_name: string
district_id?: string
number_of_crime?: number
level?: "low" | "medium" | "high" | "critical"
incidents: any[]
}>
selectedYear?: number | string
selectedMonth?: number | string
weatherData?: {
temperature: number
condition: string
humidity: number
windSpeed: number
forecast: Array<{
time: string
temperature: number
condition: string
}>
}
}
export default function MapSidebar({
isOpen,
onToggle,
crimes = [],
selectedYear,
selectedMonth,
weatherData = {
temperature: 78,
condition: "Mostly cloudy",
humidity: 65,
windSpeed: 8,
forecast: [
{ time: "Now", temperature: 78, condition: "Cloudy" },
{ time: "9:00 PM", temperature: 75, condition: "Cloudy" },
{ time: "10:00 PM", temperature: 73, condition: "Cloudy" },
{ time: "11:00 PM", temperature: 72, condition: "Cloudy" },
{ time: "12:00 AM", temperature: 70, condition: "Cloudy" },
],
},
}: MapSidebarProps) {
return (
<div
className={cn(
"h-full bg-background border-r transition-all duration-300 flex flex-col",
isOpen ? "w-80" : "w-0 overflow-hidden",
)}
>
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-lg font-semibold">Weather Information</h2>
<Button variant="ghost" size="icon" onClick={onToggle} className="h-8 w-8">
<ChevronLeft className="h-4 w-4" />
<span className="sr-only">Close sidebar</span>
</Button>
</div>
<div className="flex-1 overflow-auto p-4">
<Tabs defaultValue="current">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="current">Current</TabsTrigger>
<TabsTrigger value="forecast">Forecast</TabsTrigger>
</TabsList>
<TabsContent value="current" className="space-y-4 mt-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-2xl font-bold flex items-center justify-between">
<span>{weatherData.temperature}°F</span>
<span className="text-sm font-normal">{weatherData.condition}</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center gap-2">
<Droplets className="h-4 w-4 text-blue-500" />
<span>Humidity: {weatherData.humidity}%</span>
</div>
<div className="flex items-center gap-2">
<Wind className="h-4 w-4 text-gray-500" />
<span>Wind: {weatherData.windSpeed} mph</span>
</div>
</div>
</CardContent>
</Card>
<div className="space-y-2">
<h3 className="text-sm font-medium">Today's Recommendations</h3>
<div className="grid grid-cols-2 gap-2">
<Card className="bg-muted/50">
<CardContent className="p-3">
<div className="flex flex-col items-center text-center">
<div className="mb-1">🌂</div>
<div className="text-xs font-medium">Umbrella</div>
<div className="text-xs">No need</div>
</div>
</CardContent>
</Card>
<Card className="bg-muted/50">
<CardContent className="p-3">
<div className="flex flex-col items-center text-center">
<div className="mb-1">🏞</div>
<div className="text-xs font-medium">Outdoors</div>
<div className="text-xs text-red-500">Very poor</div>
</div>
</CardContent>
</Card>
</div>
</div>
<Collapsible className="w-full">
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="flex w-full justify-between p-0 h-8">
<span>Crime Statistics</span>
<ChevronRight className="h-4 w-4 transition-transform ui-open:rotate-90" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-2 mt-2">
{crimes.length > 0 ? (
crimes.map((crime) => (
<Card key={crime.id} className="bg-muted/50">
<CardContent className="p-3">
<div className="flex justify-between items-center">
<span className="text-sm">{crime.district_name}</span>
<span
className={cn(
"text-xs px-2 py-0.5 rounded-full",
crime.level === "low" && "bg-green-100 text-green-800",
crime.level === "medium" && "bg-yellow-100 text-yellow-800",
crime.level === "high" && "bg-orange-100 text-orange-800",
crime.level === "critical" && "bg-red-100 text-red-800",
)}
>
{crime.number_of_crime}
</span>
</div>
</CardContent>
</Card>
))
) : (
<div className="text-sm text-muted-foreground">No crime data available</div>
)}
</CollapsibleContent>
</Collapsible>
</TabsContent>
<TabsContent value="forecast" className="mt-4">
<div className="space-y-3">
{weatherData.forecast.map((item, index) => (
<Card key={index}>
<CardContent className="p-3 flex justify-between items-center">
<div className="flex items-center gap-2">
<Cloud className="h-5 w-5 text-blue-500" />
<span>{item.time}</span>
</div>
<div className="flex items-center gap-2">
<span>{item.condition}</span>
<span className="font-medium">{item.temperature}°</span>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
</Tabs>
</div>
</div>
)
}

View File

@ -1,40 +0,0 @@
"use client"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { Button } from "../../ui/button"
import { cn } from "@/app/_lib/utils"
import { Overlay } from "../overlay"
interface SidebarToggleProps {
isOpen: boolean
onToggle: () => void
position?: "left" | "right"
className?: string
}
export default function SidebarToggle({ isOpen, onToggle, position = "left", className }: SidebarToggleProps) {
return (
<Overlay position={position}>
<Button
variant="secondary"
size="icon"
onClick={onToggle}
className={cn(
"absolute z-10 shadow-md h-8 w-8 bg-background border"
)}
>
{isOpen ? (
position === "left" ? (
<ChevronLeft className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)
) : position === "left" ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
<span className="sr-only">{isOpen ? "Close sidebar" : "Open sidebar"}</span>
</Button>
</Overlay>
)
}

View File

@ -1,35 +0,0 @@
"use client"
import { ReactNode } from "react"
// Define the possible control IDs for the crime map
export type ITopTooltipsMapId =
// Crime data views
| "incidents"
| "heatmap"
| "trends"
| "patrol"
| "reports"
| "clusters"
| "timeline"
// Tools and features
| "refresh"
| "search"
| "alerts"
| "layers"
| "evidence"
| "arrests";
// Map tools type definition
export interface IMapTool {
id: ITopTooltipsMapId;
label: string;
icon: ReactNode;
description?: string;
}
// Default export for future expansion
export default function MapTools() {
return null;
}

View File

@ -1,15 +0,0 @@
"use client"
export default function SeverityIndicator() {
return (
<div className="absolute bottom-0 left-0 right-0 z-10 flex justify-center">
<div className="bg-black/75 rounded-t-md px-4 py-1 flex items-center space-x-4 text-white text-sm">
<div className="flex items-center">
<span className="font-medium mr-2">Low</span>
<span className="font-medium mr-2">Medium</span>
<span className="font-medium mr-2">High</span>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,151 @@
"use client"
import { Button } from "@/app/_components/ui/button"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/app/_components/ui/tooltip"
import { Popover, PopoverContent, PopoverTrigger } from "@/app/_components/ui/popover"
import { ChevronDown, Layers, Siren } from "lucide-react"
import { IconMessage } from "@tabler/icons-react"
import { useRef, useState } from "react"
import { ITooltips } from "./tooltips"
import MonthSelector from "../month-selector"
import YearSelector from "../year-selector"
import CategorySelector from "../category-selector"
// Define the additional tools and features
const additionalTooltips = [
{ id: "reports" as ITooltips, icon: <IconMessage size={20} />, label: "Police Report" },
{ id: "layers" as ITooltips, icon: <Layers size={20} />, label: "Map Layers" },
{ id: "alerts" as ITooltips, icon: <Siren size={20} className="text-red-500" />, label: "Active Alerts" },
]
interface AdditionalTooltipsProps {
activeControl?: string
onControlChange?: (controlId: ITooltips) => void
selectedYear: number
setSelectedYear: (year: number) => void
selectedMonth: number | "all"
setSelectedMonth: (month: number | "all") => void
selectedCategory: string | "all"
setSelectedCategory: (category: string | "all") => void
availableYears?: (number | null)[]
categories?: string[]
}
export default function AdditionalTooltips({
activeControl,
onControlChange,
selectedYear,
setSelectedYear,
selectedMonth,
setSelectedMonth,
selectedCategory,
setSelectedCategory,
availableYears = [2022, 2023, 2024],
categories = [],
}: AdditionalTooltipsProps) {
const [showSelectors, setShowSelectors] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const [isClient, setIsClient] = useState(false)
const container = isClient ? document.getElementById("root") : null
return (
<>
<div className="z-10 bg-background rounded-md p-1 flex items-center space-x-1">
<TooltipProvider>
{additionalTooltips.map((control) => (
<Tooltip key={control.id}>
<TooltipTrigger asChild>
<Button
variant={activeControl === control.id ? "default" : "ghost"}
size="icon"
className={`h-8 w-8 rounded-md ${activeControl === control.id
? "bg-white text-black hover:bg-white/90"
: "text-white hover:bg-white/10"
}`}
onClick={() => onControlChange?.(control.id)}
>
{control.icon}
<span className="sr-only">{control.label}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>{control.label}</p>
</TooltipContent>
</Tooltip>
))}
<Tooltip>
<Popover open={showSelectors} onOpenChange={setShowSelectors}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-md text-white hover:bg-white/10"
onClick={() => setShowSelectors(!showSelectors)}
>
<ChevronDown size={20} />
<span className="sr-only">Filters</span>
</Button>
</PopoverTrigger>
<PopoverContent
container={containerRef.current || container || undefined}
className="w-auto p-3 bg-black/90 border-gray-700 text-white"
align="end"
style={{ zIndex: 2000 }}
>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<span className="text-xs w-16">Year:</span>
<YearSelector
availableYears={availableYears}
selectedYear={selectedYear}
onYearChange={setSelectedYear}
className="w-[180px]"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-xs w-16">Month:</span>
<MonthSelector
selectedMonth={selectedMonth}
onMonthChange={setSelectedMonth}
className="w-[180px]"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-xs w-16">Category:</span>
<CategorySelector
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={setSelectedCategory}
className="w-[180px]"
/>
</div>
</div>
</PopoverContent>
</Popover>
</Tooltip>
</TooltipProvider>
</div>
{showSelectors && (
<div className="z-10 bg-background rounded-md p-2 flex items-center gap-2 md:hidden">
<YearSelector
availableYears={availableYears}
selectedYear={selectedYear}
onYearChange={setSelectedYear}
className="w-[100px]"
/>
<MonthSelector selectedMonth={selectedMonth} onMonthChange={setSelectedMonth} className="w-[100px]" />
<CategorySelector
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={setSelectedCategory}
className="w-[100px]"
/>
</div>
)}
</>
)
}

View File

@ -0,0 +1,52 @@
"use client"
import { Button } from "@/app/_components/ui/button"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/app/_components/ui/tooltip"
import { AlertTriangle, BarChart2, Clock, Map, Shield, Users } from "lucide-react"
import { ITooltips } from "./tooltips"
// Define the primary crime data controls
const crimeTooltips = [
{ id: "incidents" as ITooltips, icon: <AlertTriangle size={20} />, label: "All Incidents" },
{ id: "heatmap" as ITooltips, icon: <Map size={20} />, label: "Crime Heatmap" },
{ id: "trends" as ITooltips, icon: <BarChart2 size={20} />, label: "Crime Trends" },
{ id: "patrol" as ITooltips, icon: <Shield size={20} />, label: "Patrol Areas" },
{ id: "clusters" as ITooltips, icon: <Users size={20} />, label: "Clusters" },
{ id: "timeline" as ITooltips, icon: <Clock size={20} />, label: "Time Analysis" },
]
interface CrimeTooltipsProps {
activeControl?: string
onControlChange?: (controlId: ITooltips) => void
}
export default function CrimeTooltips({ activeControl, onControlChange }: CrimeTooltipsProps) {
return (
<div className="z-10 bg-background rounded-md p-1 flex items-center space-x-1">
<TooltipProvider>
{crimeTooltips.map((control) => (
<Tooltip key={control.id}>
<TooltipTrigger asChild>
<Button
variant={activeControl === control.id ? "default" : "ghost"}
size="icon"
className={`h-8 w-8 rounded-md ${activeControl === control.id
? "bg-white text-black hover:bg-white/90"
: "text-white hover:bg-white/10"
}`}
onClick={() => onControlChange?.(control.id)}
>
{control.icon}
<span className="sr-only">{control.label}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>{control.label}</p>
</TooltipContent>
</Tooltip>
))}
</TooltipProvider>
</div>
)
}

View File

@ -1,38 +1,15 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { Button } from "@/app/_components/ui/button"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/app/_components/ui/tooltip"
import { Popover, PopoverContent, PopoverTrigger } from "@/app/_components/ui/popover"
import { ChevronDown, MessageSquare, MapPin, Calendar, Info, ExternalLink } from "lucide-react"
import YearSelector from "./year-selector"
import MonthSelector from "./month-selector"
import CategorySelector from "./category-selector"
import ActionSearchBar from "@/app/_components/action-search-bar"
import { Search, XCircle, Info, ExternalLink, Calendar, MapPin, MessageSquare, FileText, Map, FolderOpen } from 'lucide-react'
import { useEffect, useRef, useState } from "react"
import { AnimatePresence, motion } from "framer-motion"
import {
AlertTriangle,
Shield,
FileText,
Users,
Map,
BarChart2,
Clock,
Filter,
Search,
RefreshCw,
Layers,
Siren,
BadgeAlert,
FolderOpen,
XCircle,
} from "lucide-react"
import { ITopTooltipsMapId } from "./map-tooltips"
import { IconAnalyze, IconMessage } from "@tabler/icons-react"
import { FloatingActionSearchBar } from "../../floating-action-search-bar"
import { format } from 'date-fns'
import ActionSearchBar from "@/app/_components/action-search-bar"
import { Card } from "@/app/_components/ui/card"
import { ICrimes } from "@/app/_utils/types/crimes"
import { format } from 'date-fns'
import { ITooltips } from "./tooltips"
// Expanded sample crime data with more entries for testing
const SAMPLE_CRIME_DATA = [
@ -46,8 +23,6 @@ const SAMPLE_CRIME_DATA = [
{ id: "CR-34517-2024", description: "Mugging at Central Station" },
{ id: "CR-14517-2024", description: "Shoplifting at Mall" },
{ id: "CR-24517-2024", description: "Break-in at Office Building" },
// Add more entries for testing (up to 100)
// ...more sample entries...
];
// Generate additional sample data for testing scrolling
@ -140,52 +115,13 @@ const ACTIONS = [
},
]
// Define the primary crime data controls
const crimeControls = [
{ id: "incidents" as ITopTooltipsMapId, icon: <AlertTriangle size={20} />, label: "All Incidents" },
{ id: "heatmap" as ITopTooltipsMapId, icon: <Map size={20} />, label: "Crime Heatmap" },
{ id: "trends" as ITopTooltipsMapId, icon: <BarChart2 size={20} />, label: "Crime Trends" },
{ id: "patrol" as ITopTooltipsMapId, icon: <Shield size={20} />, label: "Patrol Areas" },
{ id: "clusters" as ITopTooltipsMapId, icon: <Users size={20} />, label: "Clusters" },
{ id: "timeline" as ITopTooltipsMapId, icon: <Clock size={20} />, label: "Time Analysis" },
]
// Define the additional tools and features
const additionalControls = [
{ id: "reports" as ITopTooltipsMapId, icon: <IconMessage size={20} />, label: "Police Report" },
{ id: "layers" as ITopTooltipsMapId, icon: <Layers size={20} />, label: "Map Layers" },
{ id: "alerts" as ITopTooltipsMapId, icon: <Siren size={20} className="text-red-500" />, label: "Active Alerts" },
]
interface TopControlProps {
onControlChange?: (controlId: ITopTooltipsMapId) => void
interface SearchTooltipProps {
onControlChange?: (controlId: ITooltips) => void
activeControl?: string
selectedYear: number
setSelectedYear: (year: number) => void
selectedMonth: number | "all"
setSelectedMonth: (month: number | "all") => void
selectedCategory: string | "all"
setSelectedCategory: (category: string | "all") => void
availableYears?: (number | null)[]
categories?: string[]
}
export default function TopControl({
onControlChange,
activeControl,
selectedYear,
setSelectedYear,
selectedMonth,
setSelectedMonth,
selectedCategory,
setSelectedCategory,
availableYears = [2022, 2023, 2024],
categories = [],
}: TopControlProps) {
const [showSelectors, setShowSelectors] = useState(false)
export default function SearchTooltip({ onControlChange, activeControl }: SearchTooltipProps) {
const [showSearch, setShowSearch] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const searchInputRef = useRef<HTMLInputElement>(null)
const [selectedSearchType, setSelectedSearchType] = useState<string | null>(null)
const [searchValue, setSearchValue] = useState("")
@ -203,14 +139,6 @@ export default function TopControl({
} | null>(null)
const [showInfoBox, setShowInfoBox] = useState(false)
const [isClient, setIsClient] = useState(false)
const container = isClient ? document.getElementById("root") : null
useEffect(() => {
setIsClient(true)
}, [])
useEffect(() => {
if (showSearch && searchInputRef.current) {
setTimeout(() => {
@ -239,8 +167,6 @@ export default function TopControl({
initialSuggestions = EXPANDED_SAMPLE_DATA;
}
console.log("Initial suggestions count:", initialSuggestions.length);
// Force a re-render by setting suggestions in the next tick
setTimeout(() => {
setSuggestions(initialSuggestions);
@ -383,23 +309,15 @@ export default function TopControl({
// Restore original suggestions for the current search type
if (selectedSearchType) {
const currentPrefix = ACTIONS.find(action => action.id === selectedSearchType)?.prefix || "";
const initialSuggestions = filterSuggestions(selectedSearchType, currentPrefix);
setTimeout(() => {
setSuggestions(initialSuggestions);
}, 0);
const initialSuggestions = filterSuggestions(selectedSearchType, searchValue);
setSuggestions(initialSuggestions);
}
};
const toggleSelectors = () => {
setShowSelectors(!showSelectors)
}
const toggleSearch = () => {
setShowSearch(!showSearch)
if (!showSearch && onControlChange) {
onControlChange("search" as ITopTooltipsMapId)
onControlChange("search" as ITooltips)
setSelectedSearchType(null);
setSearchValue("");
setSuggestions([]);
@ -407,153 +325,31 @@ export default function TopControl({
}
return (
<div ref={containerRef} className="flex flex-col items-center gap-2">
<div className="flex items-center gap-2">
<div className="z-10 bg-background rounded-md p-1 flex items-center space-x-1">
<TooltipProvider>
{crimeControls.map((control) => (
<Tooltip key={control.id}>
<TooltipTrigger asChild>
<Button
variant={activeControl === control.id ? "default" : "ghost"}
size="icon"
className={`h-8 w-8 rounded-md ${activeControl === control.id
? "bg-white text-black hover:bg-white/90"
: "text-white hover:bg-white/10"
}`}
onClick={() => onControlChange?.(control.id)}
>
{control.icon}
<span className="sr-only">{control.label}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>{control.label}</p>
</TooltipContent>
</Tooltip>
))}
</TooltipProvider>
</div>
<div className="z-10 bg-background rounded-md p-1 flex items-center space-x-1">
<TooltipProvider>
{additionalControls.map((control) => (
<Tooltip key={control.id}>
<TooltipTrigger asChild>
<Button
variant={activeControl === control.id ? "default" : "ghost"}
size="icon"
className={`h-8 w-8 rounded-md ${activeControl === control.id
? "bg-white text-black hover:bg-white/90"
: "text-white hover:bg-white/10"
}`}
onClick={() => onControlChange?.(control.id)}
>
{control.icon}
<span className="sr-only">{control.label}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>{control.label}</p>
</TooltipContent>
</Tooltip>
))}
<Tooltip>
<Popover open={showSelectors} onOpenChange={setShowSelectors}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-md text-white hover:bg-white/10"
onClick={toggleSelectors}
>
<ChevronDown size={20} />
<span className="sr-only">Filters</span>
</Button>
</PopoverTrigger>
<PopoverContent
container={containerRef.current || container || undefined}
className="w-auto p-3 bg-black/90 border-gray-700 text-white"
align="end"
style={{ zIndex: 2000 }}>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<span className="text-xs w-16">Year:</span>
<YearSelector
availableYears={availableYears}
selectedYear={selectedYear}
onYearChange={setSelectedYear}
className="w-[180px]"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-xs w-16">Month:</span>
<MonthSelector
selectedMonth={selectedMonth}
onMonthChange={setSelectedMonth}
className="w-[180px]"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-xs w-16">Category:</span>
<CategorySelector
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={setSelectedCategory}
className="w-[180px]"
/>
</div>
</div>
</PopoverContent>
</Popover>
</Tooltip>
</TooltipProvider>
</div>
<div className="z-10 bg-background rounded-md p-1 flex items-center space-x-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={showSearch ? "default" : "ghost"}
size="icon"
className={`h-8 w-8 rounded-md ${showSearch
? "bg-white text-black hover:bg-white/90"
: "text-white hover:bg-white/10"
}`}
onClick={toggleSearch}
>
<Search size={20} />
<span className="sr-only">Search Incidents</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Search Incidents</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<>
<div className="z-10 bg-background rounded-md p-1 flex items-center space-x-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={showSearch ? "default" : "ghost"}
size="icon"
className={`h-8 w-8 rounded-md ${showSearch
? "bg-white text-black hover:bg-white/90"
: "text-white hover:bg-white/10"
}`}
onClick={toggleSearch}
>
<Search size={20} />
<span className="sr-only">Search Incidents</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Search Incidents</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{showSelectors && (
<div className="z-10 bg-background rounded-md p-2 flex items-center gap-2 md:hidden">
<YearSelector
availableYears={availableYears}
selectedYear={selectedYear}
onYearChange={setSelectedYear}
className="w-[100px]"
/>
<MonthSelector selectedMonth={selectedMonth} onMonthChange={setSelectedMonth} className="w-[100px]" />
<CategorySelector
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={setSelectedCategory}
className="w-[100px]"
/>
</div>
)}
<AnimatePresence>
{showSearch && (
<>
@ -689,7 +485,7 @@ export default function TopControl({
) : (
<Card className="p-4 border border-border">
<div className="flex justify-between items-start mb-4">
<h3 className="text-lg font-semibold">{selectedSuggestion?.id}</h3>
<h3 className="text-lg font-semibold">{selectedSuggestion?.id}</h3>
</div>
{selectedSuggestion && (
@ -754,6 +550,6 @@ export default function TopControl({
</>
)}
</AnimatePresence>
</div >
</>
)
}

View File

@ -0,0 +1,90 @@
"use client"
import { useRef, useState } from "react"
import CrimeTooltips from "./crime-tooltips"
import AdditionalTooltips from "./additional-tooltips"
import SearchTooltip from "./search-control"
import { ReactNode } from "react"
// Define the possible control IDs for the crime map
export type ITooltips =
// Crime data views
| "incidents"
| "heatmap"
| "trends"
| "patrol"
| "reports"
| "clusters"
| "timeline"
// Tools and features
| "refresh"
| "search"
| "alerts"
| "layers"
| "evidence"
| "arrests";
// Map tools type definition
export interface IMapTools {
id: ITooltips;
label: string;
icon: ReactNode;
description?: string;
}
interface TooltipProps {
onControlChange?: (controlId: ITooltips) => void
activeControl?: string
selectedYear: number
setSelectedYear: (year: number) => void
selectedMonth: number | "all"
setSelectedMonth: (month: number | "all") => void
selectedCategory: string | "all"
setSelectedCategory: (category: string | "all") => void
availableYears?: (number | null)[]
categories?: string[]
}
export default function Tooltips({
onControlChange,
activeControl,
selectedYear,
setSelectedYear,
selectedMonth,
setSelectedMonth,
selectedCategory,
setSelectedCategory,
availableYears = [2022, 2023, 2024],
categories = [],
}: TooltipProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [isClient, setIsClient] = useState(false)
return (
<div ref={containerRef} className="flex flex-col items-center gap-2">
<div className="flex items-center gap-2">
{/* Crime Tooltips Component */}
<CrimeTooltips activeControl={activeControl} onControlChange={onControlChange} />
{/* Additional Tooltips Component */}
<AdditionalTooltips
activeControl={activeControl}
onControlChange={onControlChange}
selectedYear={selectedYear}
setSelectedYear={setSelectedYear}
selectedMonth={selectedMonth}
setSelectedMonth={setSelectedMonth}
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
availableYears={availableYears}
categories={categories}
/>
{/* Search Control Component */}
<SearchTooltip activeControl={activeControl} onControlChange={onControlChange} />
</div>
</div>
)
}

View File

@ -10,18 +10,17 @@ import { getMonthName } from "@/app/_utils/common"
import { useRef, useState, useCallback, useMemo, useEffect } from "react"
import { useFullscreen } from "@/app/_hooks/use-fullscreen"
import { Overlay } from "./overlay"
import MapLegend from "./controls/map-legend"
import MapLegend from "./legends/map-legend"
import { useGetAvailableYears, useGetCrimeCategories, useGetCrimes } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries"
import { ITopTooltipsMapId } from "./controls/map-tooltips"
import MapSelectors from "./controls/map-selector"
import CrimeSidebar from "./sidebar/map-sidebar"
import SidebarToggle from "./sidebar/sidebar-toggle"
import { cn } from "@/app/_lib/utils"
import CrimePopup from "./pop-up/crime-popup"
import { $Enums, crime_categories, crime_incidents, crimes, demographics, districts, geographics, locations } from "@prisma/client"
import { CrimeTimelapse } from "./controls/crime-timelapse"
import TopControl from "./controls/top-controls"
import { CrimeTimelapse } from "./controls/bottom/crime-timelapse"
import { ITooltips } from "./controls/top/tooltips"
import CrimeSidebar from "./controls/left/sidebar/map-sidebar"
import Tooltips from "./controls/top/tooltips"
// Updated CrimeIncident type to match the structure in crime_incidents
interface CrimeIncident {
@ -45,7 +44,7 @@ export default function CrimeMap() {
const [selectedCategory, setSelectedCategory] = useState<string | "all">("all")
const [selectedYear, setSelectedYear] = useState<number>(2024)
const [selectedMonth, setSelectedMonth] = useState<number | "all">("all")
const [activeControl, setActiveControl] = useState<ITopTooltipsMapId>("incidents")
const [activeControl, setActiveControl] = useState<ITooltips>("incidents")
const [yearProgress, setYearProgress] = useState(0)
const [isTimelapsePlaying, setisTimelapsePlaying] = useState(false)
const [isSearchActive, setIsSearchActive] = useState(false)
@ -277,7 +276,7 @@ export default function CrimeMap() {
}, [sidebarCollapsed])
// Handle control changes from the top controls component
const handleControlChange = (controlId: ITopTooltipsMapId) => {
const handleControlChange = (controlId: ITooltips) => {
setActiveControl(controlId)
// Toggle search state when search control is clicked
@ -348,7 +347,7 @@ export default function CrimeMap() {
<>
<Overlay position="top" className="m-0 bg-transparent shadow-none p-0 border-none">
<div className="flex justify-center">
<TopControl
<Tooltips
activeControl={activeControl}
onControlChange={handleControlChange}
selectedYear={selectedYear}