721 lines
35 KiB
TypeScript
721 lines
35 KiB
TypeScript
"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 { 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 { Card } from "@/app/_components/ui/card"
|
|
import { ICrimes } from "@/app/_utils/types/crimes"
|
|
|
|
// Expanded sample crime data with more entries for testing
|
|
const SAMPLE_CRIME_DATA = [
|
|
{ id: "CR-12345-2023", description: "Robbery at Main Street" },
|
|
{ id: "CR-23456-2023", description: "Assault in Central Park" },
|
|
{ id: "CI-7890-2023", description: "Burglary report at Downtown" },
|
|
{ id: "CI-4567-2024", description: "Vandalism at City Hall" },
|
|
{ id: "CI-4167-2024", description: "Graffiti at Public Library" },
|
|
{ id: "CI-4067-2024", description: "Property damage at School" },
|
|
{ id: "CR-34567-2024", description: "Car theft on 5th Avenue" },
|
|
{ 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
|
|
const generateSampleData = () => {
|
|
const additionalData = [];
|
|
for (let i = 1; i <= 90; i++) {
|
|
// Mix of crime and incident IDs
|
|
const prefix = i % 2 === 0 ? "CR-" : "CI-";
|
|
const id = `${prefix}${10000 + i}-${2022 + i % 3}`;
|
|
const descriptions = [
|
|
"Theft at residence",
|
|
"Traffic violation",
|
|
"Noise complaint",
|
|
"Suspicious activity",
|
|
"Drug related incident",
|
|
"Vandalism of public property",
|
|
"Illegal parking",
|
|
"Public disturbance",
|
|
"Domestic dispute",
|
|
"Assault case"
|
|
];
|
|
const description = descriptions[i % descriptions.length];
|
|
additionalData.push({ id, description: `${description} #${i}` });
|
|
}
|
|
return [...SAMPLE_CRIME_DATA, ...additionalData];
|
|
}
|
|
|
|
const EXPANDED_SAMPLE_DATA = generateSampleData();
|
|
|
|
const ACTIONS = [
|
|
{
|
|
id: "crime_id",
|
|
label: "Search by Crime ID",
|
|
icon: <Search className="h-4 w-4 text-blue-500" />,
|
|
description: "e.g., CR-12345",
|
|
category: "Search",
|
|
prefix: "CR-",
|
|
regex: /^CR-\d+(-\d{4})?$/,
|
|
placeholder: "CR-12345-2023",
|
|
},
|
|
{
|
|
id: "incident_id",
|
|
label: "Search by Incident ID",
|
|
icon: <FileText className="h-4 w-4 text-orange-500" />,
|
|
description: "e.g., CI-789",
|
|
category: "Search",
|
|
prefix: "CI-",
|
|
regex: /^CI-\d+(-\d{4})?$/,
|
|
placeholder: "CI-7890-2023",
|
|
},
|
|
{
|
|
id: "coordinates",
|
|
label: "Search by Coordinates",
|
|
icon: <Map className="h-4 w-4 text-green-500" />,
|
|
description: "e.g., -6.2, 106.8",
|
|
category: "Search",
|
|
prefix: "",
|
|
regex: /^-?\d+(\.\d+)?,\s*-?\d+(\.\d+)?$/,
|
|
placeholder: "-6.2, 106.8",
|
|
},
|
|
{
|
|
id: "description",
|
|
label: "Search by Description",
|
|
icon: <MessageSquare className="h-4 w-4 text-purple-500" />,
|
|
description: "e.g., robbery",
|
|
category: "Search",
|
|
prefix: "",
|
|
regex: /.+/,
|
|
placeholder: "Enter crime description",
|
|
},
|
|
{
|
|
id: "address",
|
|
label: "Search by Address",
|
|
icon: <FolderOpen className="h-4 w-4 text-amber-500" />,
|
|
description: "e.g., Jalan Sudirman",
|
|
category: "Search",
|
|
prefix: "",
|
|
regex: /.+/,
|
|
placeholder: "Enter location or address",
|
|
},
|
|
]
|
|
|
|
// 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
|
|
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)
|
|
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("")
|
|
const [suggestions, setSuggestions] = useState<Array<{ id: string, description: string }>>([])
|
|
const [isInputValid, setIsInputValid] = useState(true)
|
|
const [selectedSuggestion, setSelectedSuggestion] = useState<{
|
|
id: string;
|
|
description: string;
|
|
latitude?: number;
|
|
longitude?: number;
|
|
timestamp?: Date;
|
|
category?: string;
|
|
type?: string;
|
|
address?: string;
|
|
} | 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(() => {
|
|
searchInputRef.current?.focus();
|
|
}, 100);
|
|
}
|
|
}, [showSearch]);
|
|
|
|
const handleSearchTypeSelect = (actionId: string) => {
|
|
const selectedAction = ACTIONS.find(action => action.id === actionId);
|
|
if (selectedAction) {
|
|
setSelectedSearchType(actionId);
|
|
|
|
const prefix = selectedAction.prefix || "";
|
|
setSearchValue(prefix);
|
|
setIsInputValid(true);
|
|
|
|
const initialSuggestions = EXPANDED_SAMPLE_DATA.filter(item => {
|
|
if (actionId === 'crime_id' && item.id.startsWith('CR-')) {
|
|
return true;
|
|
} else if (actionId === 'incident_id' && item.id.startsWith('CI-')) {
|
|
return true;
|
|
} else if (actionId === 'description' || actionId === 'address') {
|
|
return true;
|
|
} else if (actionId === 'coordinates') {
|
|
return false;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
setSuggestions(initialSuggestions);
|
|
|
|
setTimeout(() => {
|
|
if (searchInputRef.current) {
|
|
searchInputRef.current.focus();
|
|
searchInputRef.current.selectionStart = prefix.length;
|
|
searchInputRef.current.selectionEnd = prefix.length;
|
|
}
|
|
}, 50);
|
|
}
|
|
};
|
|
|
|
const handleClearSearchType = () => {
|
|
setSelectedSearchType(null);
|
|
setSearchValue("");
|
|
setSuggestions([]);
|
|
if (searchInputRef.current) {
|
|
setTimeout(() => {
|
|
searchInputRef.current?.focus();
|
|
}, 50);
|
|
}
|
|
};
|
|
|
|
const handleSuggestionSelect = (item: { id: string, description: string }) => {
|
|
setSearchValue(item.id);
|
|
setSuggestions([]);
|
|
const fullIncidentData = {
|
|
...item,
|
|
timestamp: new Date(),
|
|
latitude: -6.2088,
|
|
longitude: 106.8456,
|
|
category: selectedSearchType === 'crime_id' ? "Theft" : "Vandalism",
|
|
type: selectedSearchType === 'crime_id' ? "Property Crime" : "Public Disturbance",
|
|
address: "Jl. Sudirman No. 123, Jakarta"
|
|
};
|
|
setSelectedSuggestion(fullIncidentData);
|
|
setShowInfoBox(true);
|
|
};
|
|
|
|
const handleFlyToIncident = () => {
|
|
if (!selectedSuggestion || !selectedSuggestion.latitude || !selectedSuggestion.longitude) return;
|
|
const flyToEvent = new CustomEvent('fly_to_incident', {
|
|
detail: {
|
|
longitude: selectedSuggestion.longitude,
|
|
latitude: selectedSuggestion.latitude,
|
|
id: selectedSuggestion.id,
|
|
zoom: 15
|
|
},
|
|
bubbles: true
|
|
});
|
|
document.dispatchEvent(flyToEvent);
|
|
toggleSearch();
|
|
};
|
|
|
|
const handleCloseInfoBox = () => {
|
|
setShowInfoBox(false);
|
|
setSelectedSuggestion(null);
|
|
};
|
|
|
|
const handleSearchChange = (value: string) => {
|
|
const currentSearchType = selectedSearchType ?
|
|
ACTIONS.find(action => action.id === selectedSearchType) : null;
|
|
|
|
if (currentSearchType?.prefix && currentSearchType.prefix.length > 0) {
|
|
if (!value.startsWith(currentSearchType.prefix)) {
|
|
value = currentSearchType.prefix;
|
|
}
|
|
}
|
|
|
|
setSearchValue(value);
|
|
|
|
if (currentSearchType?.regex) {
|
|
if (!value || value === currentSearchType.prefix) {
|
|
setIsInputValid(true);
|
|
} else {
|
|
setIsInputValid(currentSearchType.regex.test(value));
|
|
}
|
|
} else {
|
|
setIsInputValid(true);
|
|
}
|
|
|
|
if (!selectedSearchType) {
|
|
setSuggestions([]);
|
|
return;
|
|
}
|
|
|
|
let filteredSuggestions: Array<{ id: string, description: string }> = [];
|
|
const searchText = value.toLowerCase();
|
|
|
|
if (currentSearchType?.id === 'crime_id') {
|
|
filteredSuggestions = EXPANDED_SAMPLE_DATA.filter(item =>
|
|
item.id.startsWith('CR-') &&
|
|
(item.id.toLowerCase().includes(searchText) ||
|
|
item.description.toLowerCase().includes(searchText))
|
|
);
|
|
}
|
|
else if (currentSearchType?.id === 'incident_id') {
|
|
filteredSuggestions = EXPANDED_SAMPLE_DATA.filter(item =>
|
|
item.id.startsWith('CI-') &&
|
|
(item.id.toLowerCase().includes(searchText) ||
|
|
item.description.toLowerCase().includes(searchText))
|
|
);
|
|
}
|
|
else if (currentSearchType?.id === 'description') {
|
|
filteredSuggestions = EXPANDED_SAMPLE_DATA.filter(item =>
|
|
item.description.toLowerCase().includes(searchText)
|
|
);
|
|
}
|
|
else if (currentSearchType?.id === 'address') {
|
|
filteredSuggestions = EXPANDED_SAMPLE_DATA.filter(item =>
|
|
item.description.toLowerCase().includes(searchText)
|
|
);
|
|
}
|
|
else if (currentSearchType?.id === 'coordinates') {
|
|
filteredSuggestions = [];
|
|
}
|
|
|
|
if (!value || (currentSearchType?.prefix && value === currentSearchType.prefix)) {
|
|
if (currentSearchType?.id === 'crime_id') {
|
|
filteredSuggestions = EXPANDED_SAMPLE_DATA.filter(item => item.id.startsWith('CR-'));
|
|
}
|
|
else if (currentSearchType?.id === 'incident_id') {
|
|
filteredSuggestions = EXPANDED_SAMPLE_DATA.filter(item => item.id.startsWith('CI-'));
|
|
}
|
|
else if (currentSearchType?.id === 'description' || currentSearchType?.id === 'address') {
|
|
filteredSuggestions = EXPANDED_SAMPLE_DATA;
|
|
}
|
|
}
|
|
|
|
setSuggestions(filteredSuggestions);
|
|
};
|
|
|
|
const toggleSelectors = () => {
|
|
setShowSelectors(!showSelectors)
|
|
}
|
|
|
|
const toggleSearch = () => {
|
|
setShowSearch(!showSearch)
|
|
if (!showSearch && onControlChange) {
|
|
onControlChange("search" as ITopTooltipsMapId)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div ref={containerRef} className="flex flex-col items-center gap-2">
|
|
<div className="flex items-center gap-2">
|
|
<div className="z-10 bg-background rounded-md p-1 flex items-center space-x-1">
|
|
<TooltipProvider>
|
|
{crimeControls.map((control) => (
|
|
<Tooltip key={control.id}>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant={activeControl === control.id ? "default" : "ghost"}
|
|
size="icon"
|
|
className={`h-8 w-8 rounded-md ${activeControl === control.id
|
|
? "bg-white text-black hover:bg-white/90"
|
|
: "text-white hover:bg-white/10"
|
|
}`}
|
|
onClick={() => onControlChange?.(control.id)}
|
|
>
|
|
{control.icon}
|
|
<span className="sr-only">{control.label}</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
<p>{control.label}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
))}
|
|
</TooltipProvider>
|
|
</div>
|
|
|
|
<div className="z-10 bg-background rounded-md p-1 flex items-center space-x-1">
|
|
<TooltipProvider>
|
|
{additionalControls.map((control) => (
|
|
<Tooltip key={control.id}>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant={activeControl === control.id ? "default" : "ghost"}
|
|
size="icon"
|
|
className={`h-8 w-8 rounded-md ${activeControl === control.id
|
|
? "bg-white text-black hover:bg-white/90"
|
|
: "text-white hover:bg-white/10"
|
|
}`}
|
|
onClick={() => onControlChange?.(control.id)}
|
|
>
|
|
{control.icon}
|
|
<span className="sr-only">{control.label}</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
<p>{control.label}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
))}
|
|
|
|
<Tooltip>
|
|
<Popover open={showSelectors} onOpenChange={setShowSelectors}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 rounded-md text-white hover:bg-white/10"
|
|
onClick={toggleSelectors}
|
|
>
|
|
<ChevronDown size={20} />
|
|
<span className="sr-only">Filters</span>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
container={containerRef.current || container || undefined}
|
|
className="w-auto p-3 bg-black/90 border-gray-700 text-white"
|
|
align="end"
|
|
style={{ zIndex: 2000 }}>
|
|
<div className="flex flex-col gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs w-16">Year:</span>
|
|
<YearSelector
|
|
availableYears={availableYears}
|
|
selectedYear={selectedYear}
|
|
onYearChange={setSelectedYear}
|
|
className="w-[180px]"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs w-16">Month:</span>
|
|
<MonthSelector
|
|
selectedMonth={selectedMonth}
|
|
onMonthChange={setSelectedMonth}
|
|
className="w-[180px]"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs w-16">Category:</span>
|
|
<CategorySelector
|
|
categories={categories}
|
|
selectedCategory={selectedCategory}
|
|
onCategoryChange={setSelectedCategory}
|
|
className="w-[180px]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
|
|
<div className="z-10 bg-background rounded-md p-1 flex items-center space-x-1">
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant={showSearch ? "default" : "ghost"}
|
|
size="icon"
|
|
className={`h-8 w-8 rounded-md ${showSearch
|
|
? "bg-white text-black hover:bg-white/90"
|
|
: "text-white hover:bg-white/10"
|
|
}`}
|
|
onClick={toggleSearch}
|
|
>
|
|
<Search size={20} />
|
|
<span className="sr-only">Search Incidents</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
<p>Search Incidents</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
</div>
|
|
|
|
{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 && (
|
|
<>
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-40"
|
|
onClick={toggleSearch}
|
|
/>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.9 }}
|
|
className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 w-[600px] max-w-[90vw]"
|
|
>
|
|
<div className="bg-background border border-border rounded-lg shadow-xl p-4">
|
|
<div className="flex justify-between items-center mb-3">
|
|
<h3 className="text-lg font-medium">Search Incidents</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7 rounded-full hover:bg-slate-200"
|
|
onClick={toggleSearch}
|
|
>
|
|
<XCircle size={18} />
|
|
</Button>
|
|
</div>
|
|
|
|
{!showInfoBox ? (
|
|
<>
|
|
<ActionSearchBar
|
|
ref={searchInputRef}
|
|
autoFocus
|
|
isFloating
|
|
defaultActions={false}
|
|
actions={ACTIONS}
|
|
onActionSelect={handleSearchTypeSelect}
|
|
onClearAction={handleClearSearchType}
|
|
activeActionId={selectedSearchType}
|
|
value={searchValue}
|
|
onChange={handleSearchChange}
|
|
placeholder={selectedSearchType ?
|
|
ACTIONS.find(a => a.id === selectedSearchType)?.placeholder :
|
|
"Select a search type..."}
|
|
inputClassName={!isInputValid ?
|
|
"border-destructive focus-visible:ring-destructive bg-destructive/50" : ""}
|
|
/>
|
|
|
|
{!isInputValid && selectedSearchType && (
|
|
<div className="mt-1 text-xs text-destructive">
|
|
Invalid format. {ACTIONS.find(a => a.id === selectedSearchType)?.description}
|
|
</div>
|
|
)}
|
|
|
|
{suggestions.length > 0 && (
|
|
<div className="mt-2 max-h-[300px] overflow-y-auto border border-border rounded-md bg-background/80 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
|
|
<div className="sticky top-0 bg-muted/70 backdrop-blur-sm px-3 py-2 border-b border-border">
|
|
<p className="text-xs text-muted-foreground">
|
|
{suggestions.length} results found
|
|
</p>
|
|
</div>
|
|
<ul className="py-1">
|
|
{suggestions.map((item, index) => (
|
|
<li
|
|
key={index}
|
|
className="px-3 py-2 hover:bg-muted cursor-pointer flex items-center justify-between"
|
|
onClick={() => handleSuggestionSelect(item)}
|
|
>
|
|
<span className="font-medium">{item.id}</span>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-muted-foreground text-sm truncate max-w-[300px]">
|
|
{item.description}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0 rounded-full hover:bg-muted"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleSuggestionSelect(item);
|
|
}}
|
|
>
|
|
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
|
</Button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{searchValue.length > (selectedSearchType && ACTIONS.find(a => a.id === selectedSearchType)?.prefix?.length || 0) &&
|
|
suggestions.length === 0 && (
|
|
<div className="mt-2 p-3 border border-border rounded-md bg-background/80 text-center">
|
|
<p className="text-sm text-muted-foreground">No matching incidents found</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-3 px-3 py-2 border-t border-border bg-muted/30 rounded-b-md">
|
|
<p className="flex items-center text-sm text-muted-foreground">
|
|
{selectedSearchType ? (
|
|
<>
|
|
<span className="mr-1">{ACTIONS.find(a => a.id === selectedSearchType)?.icon}</span>
|
|
<span>
|
|
{ACTIONS.find(a => a.id === selectedSearchType)?.description}
|
|
</span>
|
|
</>
|
|
) : (
|
|
<span>
|
|
Select a search type and enter your search criteria
|
|
</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<Card className="p-4 border border-border">
|
|
<div className="flex justify-between items-start mb-4">
|
|
<h3 className="text-lg font-semibold">{selectedSuggestion?.id}</h3>
|
|
{/* <Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleCloseInfoBox}
|
|
className="h-6 w-6 p-0 rounded-full"
|
|
>
|
|
<XCircle size={16} />
|
|
</Button> */}
|
|
</div>
|
|
|
|
{selectedSuggestion && (
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-[20px_1fr] gap-2 items-start">
|
|
<Info className="h-4 w-4 mt-1 text-muted-foreground" />
|
|
<p className="text-sm">{selectedSuggestion.description}</p>
|
|
</div>
|
|
|
|
{selectedSuggestion.timestamp && (
|
|
<div className="grid grid-cols-[20px_1fr] gap-2 items-start">
|
|
<Calendar className="h-4 w-4 mt-1 text-muted-foreground" />
|
|
<p className="text-sm">
|
|
{format(selectedSuggestion.timestamp, 'PPP p')}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{selectedSuggestion.address && (
|
|
<div className="grid grid-cols-[20px_1fr] gap-2 items-start">
|
|
<MapPin className="h-4 w-4 mt-1 text-muted-foreground" />
|
|
<p className="text-sm">{selectedSuggestion.address}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 gap-2 mt-2">
|
|
<div className="bg-muted/50 rounded p-2">
|
|
<p className="text-xs text-muted-foreground font-medium">Category</p>
|
|
<p className="text-sm">{selectedSuggestion.category || 'N/A'}</p>
|
|
</div>
|
|
<div className="bg-muted/50 rounded p-2">
|
|
<p className="text-xs text-muted-foreground font-medium">Type</p>
|
|
<p className="text-sm">{selectedSuggestion.type || 'N/A'}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between items-center pt-3 border-t border-border mt-3">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleCloseInfoBox}
|
|
>
|
|
Close
|
|
</Button>
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
onClick={handleFlyToIncident}
|
|
disabled={!selectedSuggestion.latitude || !selectedSuggestion.longitude}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<span>Fly to Incident</span>
|
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
</>
|
|
)}
|
|
</AnimatePresence>
|
|
</div >
|
|
)
|
|
}
|