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"
|
||||
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"
|
||||
|
||||
|
|
@ -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 { 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
|
|
@ -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
|
|
@ -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>
|
||||
// )
|
||||
// }
|
||||
|
|
|
@ -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"
|
||||
|
||||
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(() => {
|
||||
const initialSuggestions = filterSuggestions(selectedSearchType, searchValue);
|
||||
setSuggestions(initialSuggestions);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSelectors = () => {
|
||||
setShowSelectors(!showSelectors)
|
||||
}
|
||||
|
||||
const toggleSearch = () => {
|
||||
setShowSearch(!showSearch)
|
||||
if (!showSearch && onControlChange) {
|
||||
onControlChange("search" as ITopTooltipsMapId)
|
||||
onControlChange("search" as ITooltips)
|
||||
setSelectedSearchType(null);
|
||||
setSearchValue("");
|
||||
setSuggestions([]);
|
||||
|
@ -407,110 +325,7 @@ 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>
|
||||
|
@ -534,25 +349,6 @@ export default function TopControl({
|
|||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</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>
|
||||
{showSearch && (
|
||||
|
@ -754,6 +550,6 @@ export default function TopControl({
|
|||
</>
|
||||
)}
|
||||
</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 { 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}
|
||||
|
|
Loading…
Reference in New Issue