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:
parent
5b57476437
commit
8f12715072
|
@ -1,7 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
import { Checkbox } from "@/app/_components/ui/checkbox"
|
import { Checkbox } from "@/app/_components/ui/checkbox"
|
||||||
import { Label } from "@/app/_components/ui/label"
|
import { Label } from "@/app/_components/ui/label"
|
||||||
import { Overlay } from "../overlay"
|
import { Overlay } from "../../overlay"
|
||||||
import { ControlPosition } from "mapbox-gl"
|
import { ControlPosition } from "mapbox-gl"
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,11 +13,11 @@ import { ICrimes } from "@/app/_utils/types/crimes"
|
||||||
// Import sidebar components
|
// Import sidebar components
|
||||||
import { SidebarIncidentsTab } from "./tabs/incidents-tab"
|
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 { getMonthName } from "@/app/_utils/common"
|
||||||
import { SidebarInfoTab } from "./tabs/info-tab"
|
import { SidebarInfoTab } from "./tabs/info-tab"
|
||||||
import { SidebarStatisticsTab } from "./tabs/statistics-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 {
|
interface CrimeSidebarProps {
|
||||||
className?: string
|
className?: string
|
|
@ -4,8 +4,9 @@ import React from "react"
|
||||||
import { Button } from "@/app/_components/ui/button"
|
import { Button } from "@/app/_components/ui/button"
|
||||||
import { cn } from "@/app/_lib/utils"
|
import { cn } from "@/app/_lib/utils"
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||||
import { Overlay } from "../overlay"
|
|
||||||
import type { ControlPosition } from "mapbox-gl"
|
import type { ControlPosition } from "mapbox-gl"
|
||||||
|
import { Overlay } from "../../../overlay"
|
||||||
|
|
||||||
interface SidebarToggleProps {
|
interface SidebarToggleProps {
|
||||||
isCollapsed: boolean
|
isCollapsed: boolean
|
|
@ -1,107 +1,107 @@
|
||||||
"use client"
|
// "use client"
|
||||||
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
|
// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
|
||||||
import { Button } from "@/app/_components/ui/button"
|
// import { Button } from "@/app/_components/ui/button"
|
||||||
import { FilterX } from "lucide-react"
|
// import { FilterX } from "lucide-react"
|
||||||
import { useCallback } from "react"
|
// import { useCallback } from "react"
|
||||||
|
|
||||||
interface MapFilterControlProps {
|
// interface MapFilterControlProps {
|
||||||
selectedYear: number
|
// selectedYear: number
|
||||||
selectedMonth: number | "all"
|
// selectedMonth: number | "all"
|
||||||
availableYears: (number | null)[]
|
// availableYears: (number | null)[]
|
||||||
yearsLoading: boolean
|
// yearsLoading: boolean
|
||||||
onYearChange: (year: number) => void
|
// onYearChange: (year: number) => void
|
||||||
onMonthChange: (month: number | "all") => void
|
// onMonthChange: (month: number | "all") => void
|
||||||
onApplyFilters: () => void
|
// onApplyFilters: () => void
|
||||||
onResetFilters: () => void
|
// onResetFilters: () => void
|
||||||
}
|
// }
|
||||||
|
|
||||||
const months = [
|
// const months = [
|
||||||
{ value: "1", label: "January" },
|
// { value: "1", label: "January" },
|
||||||
{ value: "2", label: "February" },
|
// { value: "2", label: "February" },
|
||||||
{ value: "3", label: "March" },
|
// { value: "3", label: "March" },
|
||||||
{ value: "4", label: "April" },
|
// { value: "4", label: "April" },
|
||||||
{ value: "5", label: "May" },
|
// { value: "5", label: "May" },
|
||||||
{ value: "6", label: "June" },
|
// { value: "6", label: "June" },
|
||||||
{ value: "7", label: "July" },
|
// { value: "7", label: "July" },
|
||||||
{ value: "8", label: "August" },
|
// { value: "8", label: "August" },
|
||||||
{ value: "9", label: "September" },
|
// { value: "9", label: "September" },
|
||||||
{ value: "10", label: "October" },
|
// { value: "10", label: "October" },
|
||||||
{ value: "11", label: "November" },
|
// { value: "11", label: "November" },
|
||||||
{ value: "12", label: "December" },
|
// { value: "12", label: "December" },
|
||||||
]
|
// ]
|
||||||
|
|
||||||
export default function MapFilterControl({
|
// export default function MapFilterControl({
|
||||||
selectedYear,
|
// selectedYear,
|
||||||
selectedMonth,
|
// selectedMonth,
|
||||||
availableYears,
|
// availableYears,
|
||||||
yearsLoading,
|
// yearsLoading,
|
||||||
onYearChange,
|
// onYearChange,
|
||||||
onMonthChange,
|
// onMonthChange,
|
||||||
onApplyFilters,
|
// onApplyFilters,
|
||||||
onResetFilters,
|
// onResetFilters,
|
||||||
}: MapFilterControlProps) {
|
// }: MapFilterControlProps) {
|
||||||
const handleYearChange = useCallback(
|
// const handleYearChange = useCallback(
|
||||||
(value: string) => {
|
// (value: string) => {
|
||||||
onYearChange(Number(value))
|
// onYearChange(Number(value))
|
||||||
},
|
// },
|
||||||
[onYearChange],
|
// [onYearChange],
|
||||||
)
|
// )
|
||||||
|
|
||||||
const handleMonthChange = useCallback(
|
// const handleMonthChange = useCallback(
|
||||||
(value: string) => {
|
// (value: string) => {
|
||||||
onMonthChange(value === "all" ? "all" : Number(value))
|
// onMonthChange(value === "all" ? "all" : Number(value))
|
||||||
},
|
// },
|
||||||
[onMonthChange],
|
// [onMonthChange],
|
||||||
)
|
// )
|
||||||
|
|
||||||
const isDefaultFilter = selectedYear === 2024 && selectedMonth === "all"
|
// const isDefaultFilter = selectedYear === 2024 && selectedMonth === "all"
|
||||||
|
|
||||||
return (
|
// 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="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="text-sm font-medium mb-1">Map Filters</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-2">
|
// <div className="grid grid-cols-1 gap-2">
|
||||||
<Select value={selectedYear.toString()} onValueChange={handleYearChange}>
|
// <Select value={selectedYear.toString()} onValueChange={handleYearChange}>
|
||||||
<SelectTrigger className="h-8 w-full">
|
// <SelectTrigger className="h-8 w-full">
|
||||||
<SelectValue placeholder="Year" />
|
// <SelectValue placeholder="Year" />
|
||||||
</SelectTrigger>
|
// </SelectTrigger>
|
||||||
<SelectContent>
|
// <SelectContent>
|
||||||
{!yearsLoading &&
|
// {!yearsLoading &&
|
||||||
availableYears
|
// availableYears
|
||||||
?.filter((year) => year !== null)
|
// ?.filter((year) => year !== null)
|
||||||
.map((year) => (
|
// .map((year) => (
|
||||||
<SelectItem key={year} value={year!.toString()}>
|
// <SelectItem key={year} value={year!.toString()}>
|
||||||
{year}
|
// {year}
|
||||||
</SelectItem>
|
// </SelectItem>
|
||||||
))}
|
// ))}
|
||||||
</SelectContent>
|
// </SelectContent>
|
||||||
</Select>
|
// </Select>
|
||||||
|
|
||||||
<Select value={selectedMonth.toString()} onValueChange={handleMonthChange}>
|
// <Select value={selectedMonth.toString()} onValueChange={handleMonthChange}>
|
||||||
<SelectTrigger className="h-8 w-full">
|
// <SelectTrigger className="h-8 w-full">
|
||||||
<SelectValue placeholder="Month" />
|
// <SelectValue placeholder="Month" />
|
||||||
</SelectTrigger>
|
// </SelectTrigger>
|
||||||
<SelectContent>
|
// <SelectContent>
|
||||||
<SelectItem value="all">All Months</SelectItem>
|
// <SelectItem value="all">All Months</SelectItem>
|
||||||
{months.map((month) => (
|
// {months.map((month) => (
|
||||||
<SelectItem key={month.value} value={month.value}>
|
// <SelectItem key={month.value} value={month.value}>
|
||||||
{month.label}
|
// {month.label}
|
||||||
</SelectItem>
|
// </SelectItem>
|
||||||
))}
|
// ))}
|
||||||
</SelectContent>
|
// </SelectContent>
|
||||||
</Select>
|
// </Select>
|
||||||
|
|
||||||
<div className="flex gap-1">
|
// <div className="flex gap-1">
|
||||||
<Button className="h-8 text-xs flex-1" variant="default" onClick={onApplyFilters}>
|
// <Button className="h-8 text-xs flex-1" variant="default" onClick={onApplyFilters}>
|
||||||
Apply
|
// Apply
|
||||||
</Button>
|
// </Button>
|
||||||
<Button className="h-8 text-xs" variant="ghost" onClick={onResetFilters} disabled={isDefaultFilter}>
|
// <Button className="h-8 text-xs" variant="ghost" onClick={onResetFilters} disabled={isDefaultFilter}>
|
||||||
<FilterX className="h-3 w-3 mr-1" />
|
// <FilterX className="h-3 w-3 mr-1" />
|
||||||
Reset
|
// Reset
|
||||||
</Button>
|
// </Button>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,38 +1,15 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react"
|
|
||||||
import { Button } from "@/app/_components/ui/button"
|
import { Button } from "@/app/_components/ui/button"
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/app/_components/ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/app/_components/ui/tooltip"
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/app/_components/ui/popover"
|
import { Search, XCircle, Info, ExternalLink, Calendar, MapPin, MessageSquare, FileText, Map, FolderOpen } from 'lucide-react'
|
||||||
import { ChevronDown, MessageSquare, MapPin, Calendar, Info, ExternalLink } from "lucide-react"
|
|
||||||
import YearSelector from "./year-selector"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import MonthSelector from "./month-selector"
|
|
||||||
import CategorySelector from "./category-selector"
|
|
||||||
import ActionSearchBar from "@/app/_components/action-search-bar"
|
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
import {
|
import ActionSearchBar from "@/app/_components/action-search-bar"
|
||||||
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 { Card } from "@/app/_components/ui/card"
|
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
|
// Expanded sample crime data with more entries for testing
|
||||||
const SAMPLE_CRIME_DATA = [
|
const SAMPLE_CRIME_DATA = [
|
||||||
|
@ -46,8 +23,6 @@ const SAMPLE_CRIME_DATA = [
|
||||||
{ id: "CR-34517-2024", description: "Mugging at Central Station" },
|
{ id: "CR-34517-2024", description: "Mugging at Central Station" },
|
||||||
{ id: "CR-14517-2024", description: "Shoplifting at Mall" },
|
{ id: "CR-14517-2024", description: "Shoplifting at Mall" },
|
||||||
{ id: "CR-24517-2024", description: "Break-in at Office Building" },
|
{ 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
|
// Generate additional sample data for testing scrolling
|
||||||
|
@ -140,52 +115,13 @@ const ACTIONS = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// Define the primary crime data controls
|
interface SearchTooltipProps {
|
||||||
const crimeControls = [
|
onControlChange?: (controlId: ITooltips) => void
|
||||||
{ 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
|
|
||||||
activeControl?: string
|
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({
|
export default function SearchTooltip({ onControlChange, activeControl }: SearchTooltipProps) {
|
||||||
onControlChange,
|
|
||||||
activeControl,
|
|
||||||
selectedYear,
|
|
||||||
setSelectedYear,
|
|
||||||
selectedMonth,
|
|
||||||
setSelectedMonth,
|
|
||||||
selectedCategory,
|
|
||||||
setSelectedCategory,
|
|
||||||
availableYears = [2022, 2023, 2024],
|
|
||||||
categories = [],
|
|
||||||
|
|
||||||
}: TopControlProps) {
|
|
||||||
const [showSelectors, setShowSelectors] = useState(false)
|
|
||||||
const [showSearch, setShowSearch] = useState(false)
|
const [showSearch, setShowSearch] = useState(false)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [selectedSearchType, setSelectedSearchType] = useState<string | null>(null)
|
const [selectedSearchType, setSelectedSearchType] = useState<string | null>(null)
|
||||||
const [searchValue, setSearchValue] = useState("")
|
const [searchValue, setSearchValue] = useState("")
|
||||||
|
@ -203,14 +139,6 @@ export default function TopControl({
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [showInfoBox, setShowInfoBox] = useState(false)
|
const [showInfoBox, setShowInfoBox] = useState(false)
|
||||||
|
|
||||||
const [isClient, setIsClient] = useState(false)
|
|
||||||
|
|
||||||
const container = isClient ? document.getElementById("root") : null
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsClient(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showSearch && searchInputRef.current) {
|
if (showSearch && searchInputRef.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -239,8 +167,6 @@ export default function TopControl({
|
||||||
initialSuggestions = EXPANDED_SAMPLE_DATA;
|
initialSuggestions = EXPANDED_SAMPLE_DATA;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Initial suggestions count:", initialSuggestions.length);
|
|
||||||
|
|
||||||
// Force a re-render by setting suggestions in the next tick
|
// Force a re-render by setting suggestions in the next tick
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSuggestions(initialSuggestions);
|
setSuggestions(initialSuggestions);
|
||||||
|
@ -383,23 +309,15 @@ export default function TopControl({
|
||||||
|
|
||||||
// Restore original suggestions for the current search type
|
// Restore original suggestions for the current search type
|
||||||
if (selectedSearchType) {
|
if (selectedSearchType) {
|
||||||
const currentPrefix = ACTIONS.find(action => action.id === selectedSearchType)?.prefix || "";
|
const initialSuggestions = filterSuggestions(selectedSearchType, searchValue);
|
||||||
const initialSuggestions = filterSuggestions(selectedSearchType, currentPrefix);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setSuggestions(initialSuggestions);
|
setSuggestions(initialSuggestions);
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleSelectors = () => {
|
|
||||||
setShowSelectors(!showSelectors)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleSearch = () => {
|
const toggleSearch = () => {
|
||||||
setShowSearch(!showSearch)
|
setShowSearch(!showSearch)
|
||||||
if (!showSearch && onControlChange) {
|
if (!showSearch && onControlChange) {
|
||||||
onControlChange("search" as ITopTooltipsMapId)
|
onControlChange("search" as ITooltips)
|
||||||
setSelectedSearchType(null);
|
setSelectedSearchType(null);
|
||||||
setSearchValue("");
|
setSearchValue("");
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
|
@ -407,110 +325,7 @@ export default function TopControl({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="z-10 bg-background rounded-md p-1 flex items-center space-x-1">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
@ -534,25 +349,6 @@ export default function TopControl({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<AnimatePresence>
|
||||||
{showSearch && (
|
{showSearch && (
|
||||||
|
@ -754,6 +550,6 @@ export default function TopControl({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div >
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -10,18 +10,17 @@ import { getMonthName } from "@/app/_utils/common"
|
||||||
import { useRef, useState, useCallback, useMemo, useEffect } from "react"
|
import { useRef, useState, useCallback, useMemo, useEffect } from "react"
|
||||||
import { useFullscreen } from "@/app/_hooks/use-fullscreen"
|
import { useFullscreen } from "@/app/_hooks/use-fullscreen"
|
||||||
import { Overlay } from "./overlay"
|
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 { 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 MapSelectors from "./controls/map-selector"
|
||||||
|
|
||||||
import CrimeSidebar from "./sidebar/map-sidebar"
|
|
||||||
import SidebarToggle from "./sidebar/sidebar-toggle"
|
|
||||||
import { cn } from "@/app/_lib/utils"
|
import { cn } from "@/app/_lib/utils"
|
||||||
import CrimePopup from "./pop-up/crime-popup"
|
import CrimePopup from "./pop-up/crime-popup"
|
||||||
import { $Enums, crime_categories, crime_incidents, crimes, demographics, districts, geographics, locations } from "@prisma/client"
|
import { $Enums, crime_categories, crime_incidents, crimes, demographics, districts, geographics, locations } from "@prisma/client"
|
||||||
import { CrimeTimelapse } from "./controls/crime-timelapse"
|
import { CrimeTimelapse } from "./controls/bottom/crime-timelapse"
|
||||||
import TopControl from "./controls/top-controls"
|
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
|
// Updated CrimeIncident type to match the structure in crime_incidents
|
||||||
interface CrimeIncident {
|
interface CrimeIncident {
|
||||||
|
@ -45,7 +44,7 @@ export default function CrimeMap() {
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string | "all">("all")
|
const [selectedCategory, setSelectedCategory] = useState<string | "all">("all")
|
||||||
const [selectedYear, setSelectedYear] = useState<number>(2024)
|
const [selectedYear, setSelectedYear] = useState<number>(2024)
|
||||||
const [selectedMonth, setSelectedMonth] = useState<number | "all">("all")
|
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 [yearProgress, setYearProgress] = useState(0)
|
||||||
const [isTimelapsePlaying, setisTimelapsePlaying] = useState(false)
|
const [isTimelapsePlaying, setisTimelapsePlaying] = useState(false)
|
||||||
const [isSearchActive, setIsSearchActive] = useState(false)
|
const [isSearchActive, setIsSearchActive] = useState(false)
|
||||||
|
@ -277,7 +276,7 @@ export default function CrimeMap() {
|
||||||
}, [sidebarCollapsed])
|
}, [sidebarCollapsed])
|
||||||
|
|
||||||
// Handle control changes from the top controls component
|
// Handle control changes from the top controls component
|
||||||
const handleControlChange = (controlId: ITopTooltipsMapId) => {
|
const handleControlChange = (controlId: ITooltips) => {
|
||||||
setActiveControl(controlId)
|
setActiveControl(controlId)
|
||||||
|
|
||||||
// Toggle search state when search control is clicked
|
// 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">
|
<Overlay position="top" className="m-0 bg-transparent shadow-none p-0 border-none">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<TopControl
|
<Tooltips
|
||||||
activeControl={activeControl}
|
activeControl={activeControl}
|
||||||
onControlChange={handleControlChange}
|
onControlChange={handleControlChange}
|
||||||
selectedYear={selectedYear}
|
selectedYear={selectedYear}
|
||||||
|
|
Loading…
Reference in New Issue