"use client" import { Button } from "@/app/_components/ui/button" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/app/_components/ui/tooltip" 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 ActionSearchBar from "@/app/_components/action-search-bar" import { Card } from "@/app/_components/ui/card" import { format } from "date-fns" import type { ITooltipsControl } from "./tooltips" // Define types based on the crime data structure interface ICrimeIncident { id: string timestamp: Date description: string status: string locations: { address: string longitude: number latitude: number } crime_categories: { id: string name: string } } interface ICrime { id: string district_id: string month: number year: number crime_incidents: ICrimeIncident[] districts: { name: string } } // Actions for the search bar const ACTIONS = [ { id: "incident_id", label: "Search by Incident ID", icon: , 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: , 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: , description: "e.g., robbery", category: "Search", prefix: "", regex: /.+/, placeholder: "Enter crime description", }, { id: "locations.address", label: "Search by locations.Address", icon: , description: "e.g., Jalan Sudirman", category: "Search", prefix: "", regex: /.+/, placeholder: "Enter location or locations.address", }, ] interface SearchTooltipProps { onControlChange?: (controlId: ITooltipsControl) => void activeControl?: string crimes?: ICrime[] sourceType?: string } export default function SearchTooltip({ onControlChange, activeControl, crimes = [], sourceType = "cbt", }: SearchTooltipProps) { const [showSearch, setShowSearch] = useState(false) const searchInputRef = useRef(null) const [selectedSearchType, setSelectedSearchType] = useState(null) const [searchValue, setSearchValue] = useState("") const [suggestions, setSuggestions] = useState([]) const [isInputValid, setIsInputValid] = useState(true) const [selectedSuggestion, setSelectedSuggestion] = useState(null) const [showInfoBox, setShowInfoBox] = useState(false) // Check if search is disabled based on source type const isSearchDisabled = sourceType === "cbu" // Limit results to prevent performance issues const MAX_RESULTS = 50 // Extract all incidents from crimes data const allIncidents = crimes.flatMap((crime) => crime.crime_incidents.map((incident) => ({ ...incident, district: crime.districts?.name || "", year: crime.year, month: crime.month, })), ) 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) // Initial suggestions based on the selected search type let initialSuggestions: ICrimeIncident[] = [] if (actionId === "incident_id") { initialSuggestions = allIncidents.slice(0, MAX_RESULTS) // Limit to 50 results initially } else if (actionId === "description" || actionId === "locations.address") { initialSuggestions = allIncidents.slice(0, MAX_RESULTS) } // Set suggestions in the next tick setTimeout(() => { setSuggestions(initialSuggestions) }, 0) // Focus and position cursor after prefix setTimeout(() => { if (searchInputRef.current) { searchInputRef.current.focus() searchInputRef.current.selectionStart = prefix.length searchInputRef.current.selectionEnd = prefix.length } }, 50) } } // Filter suggestions based on search type and search text const filterSuggestions = (searchType: string, searchText: string): ICrimeIncident[] => { let filtered: ICrimeIncident[] = [] if (searchType === "incident_id") { if (!searchText || searchText === "CI-") { filtered = allIncidents.slice(0, MAX_RESULTS) } else { filtered = allIncidents .filter((item) => item.id.toLowerCase().includes(searchText.toLowerCase())) .slice(0, MAX_RESULTS) } } else if (searchType === "description") { if (!searchText) { filtered = allIncidents.slice(0, MAX_RESULTS) } else { filtered = allIncidents .filter((item) => item.description.toLowerCase().includes(searchText.toLowerCase())) .slice(0, MAX_RESULTS) } } else if (searchType === "locations.address") { if (!searchText) { filtered = allIncidents.slice(0, MAX_RESULTS) } else { filtered = allIncidents .filter( (item) => item.locations.address && item.locations.address.toLowerCase().includes(searchText.toLowerCase()), ) .slice(0, MAX_RESULTS) } } else if (searchType === "coordinates") { if (!searchText) { filtered = allIncidents .filter((item) => item.locations.latitude !== undefined && item.locations.longitude !== undefined) .slice(0, MAX_RESULTS) } else { // For coordinates, we'd typically do a proximity search // This is a simple implementation for demo purposes filtered = allIncidents .filter( (item) => item.locations.latitude !== undefined && item.locations.longitude !== undefined && `${item.locations.latitude}, ${item.locations.longitude}`.includes(searchText), ) .slice(0, MAX_RESULTS) } } return filtered } 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 } // Filter suggestions based on search input setSuggestions(filterSuggestions(selectedSearchType, value)) } const handleClearSearchType = () => { setSelectedSearchType(null) setSearchValue("") setSuggestions([]) if (searchInputRef.current) { setTimeout(() => { searchInputRef.current?.focus() }, 50) } } const handleSuggestionSelect = (incident: ICrimeIncident) => { setSearchValue(incident.id) setSuggestions([]) setSelectedSuggestion(incident) setShowInfoBox(true) } const handleFlyToIncident = () => { if (!selectedSuggestion || !selectedSuggestion.locations.latitude || !selectedSuggestion.locations.longitude) return // Dispatch mapbox_fly_to event to the main map canvas only const flyToMapEvent = new CustomEvent("mapbox_fly_to", { detail: { longitude: selectedSuggestion.locations.longitude, latitude: selectedSuggestion.locations.latitude, zoom: 15, bearing: 0, pitch: 45, duration: 2000, }, bubbles: true, }) // Find the main map canvas and dispatch event there const mapCanvas = document.querySelector(".mapboxgl-canvas") if (mapCanvas) { mapCanvas.dispatchEvent(flyToMapEvent) } // Wait for the fly animation to complete before showing the popup setTimeout(() => { // Then trigger the incident_click event to show the popup const incidentEvent = new CustomEvent("incident_click", { detail: { id: selectedSuggestion.id, longitude: selectedSuggestion.locations.longitude, latitude: selectedSuggestion.locations.latitude, description: selectedSuggestion.description, status: selectedSuggestion.status, timestamp: selectedSuggestion.timestamp, crime_categories: selectedSuggestion.crime_categories, }, bubbles: true, }) document.dispatchEvent(incidentEvent) }, 2100) // Slightly longer than the fly animation duration setShowInfoBox(false) setSelectedSuggestion(null) toggleSearch() } const handleCloseInfoBox = () => { setShowInfoBox(false) setSelectedSuggestion(null) // Restore original suggestions if (selectedSearchType) { const initialSuggestions = filterSuggestions(selectedSearchType, searchValue) setSuggestions(initialSuggestions) } } const toggleSearch = () => { if (isSearchDisabled) return setShowSearch(!showSearch) if (!showSearch && onControlChange) { onControlChange("search" as ITooltipsControl) setSelectedSearchType(null) setSearchValue("") setSuggestions([]) } } // Format date for display const formatIncidentDate = (incident: ICrimeIncident) => { try { if (incident.timestamp) { return format(new Date(incident.timestamp), "PPP p") } return "N/A" } catch (error) { return "Invalid date" } } return ( <>

{isSearchDisabled ? "Not available for CBU data" : "Search Incidents"}

{showSearch && ( <>

Search Incidents

{!showInfoBox ? ( <> a.id === selectedSearchType)?.placeholder : "Select a search type..." } inputClassName={ !isInputValid ? "border-destructive focus-visible:ring-destructive bg-destructive/50" : "" } /> {!isInputValid && selectedSearchType && (
Invalid format. {ACTIONS.find((a) => a.id === selectedSearchType)?.description}
)} {suggestions.length > 0 && selectedSearchType && (

{suggestions.length} results found {suggestions.length === 50 && " (showing top 50)"}

    {suggestions.map((incident, index) => (
  • handleSuggestionSelect(incident)} > {incident.id}
    {selectedSearchType === "incident_id" ? ( {incident.description} ) : selectedSearchType === "coordinates" ? ( {incident.locations.latitude}, {incident.locations.longitude} -{" "} {incident.description} ) : selectedSearchType === "locations.address" ? ( {incident.locations.address || "N/A"} ) : ( {incident.description} )}
  • ))}
)} {selectedSearchType && searchValue.length > (ACTIONS.find((a) => a.id === selectedSearchType)?.prefix?.length || 0) && suggestions.length === 0 && (

No matching incidents found

)}

{selectedSearchType ? ( <> {ACTIONS.find((a) => a.id === selectedSearchType)?.icon} {ACTIONS.find((a) => a.id === selectedSearchType)?.description} ) : ( Select a search type and enter your search criteria )}

) : (

{selectedSuggestion?.id}

{selectedSuggestion && (

{selectedSuggestion.description}

{selectedSuggestion.timestamp && (

{formatIncidentDate(selectedSuggestion)}

)} {selectedSuggestion.locations.address && (

{selectedSuggestion.locations.address}

)}

Category

{selectedSuggestion.crime_categories?.name || "N/A"}

Status

{selectedSuggestion.status || "N/A"}

)}
)}
)}
) }