"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 { ITooltips } from "./tooltips" import { $Enums } from "@prisma/client" // 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: ITooltips) => void activeControl?: string crimes?: ICrime[] } export default function SearchTooltip({ onControlChange, activeControl, crimes = [] }: 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) // 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; const flyToEvent = new CustomEvent('incident_click', { detail: { longitude: selectedSuggestion.locations.longitude, latitude: selectedSuggestion.locations.latitude, id: selectedSuggestion.id, zoom: 15, description: selectedSuggestion.description, status: selectedSuggestion.status }, bubbles: true }); document.dispatchEvent(flyToEvent); 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 = () => { setShowSearch(!showSearch) if (!showSearch && onControlChange) { onControlChange("search" as ITooltips) 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 ( <>

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'}

)}
)}
)}
) }