diff --git a/sigap-website/app/_components/map/controls/top/search-control.tsx b/sigap-website/app/_components/map/controls/top/search-control.tsx index a5cd181..d31b5b9 100644 --- a/sigap-website/app/_components/map/controls/top/search-control.tsx +++ b/sigap-website/app/_components/map/controls/top/search-control.tsx @@ -10,58 +10,35 @@ 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" -// 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" }, -]; - -// 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]; - - // Add more detailed properties for enhanced suggestions - additionalData.push({ - id, - description: `${description} #${i}`, - coordinates: `${-6 - (i % 5) * 0.01}, ${106 + (i % 7) * 0.01}`, - address: `Jl. ${["Sudirman", "Thamrin", "Gatot Subroto", "Rasuna Said", "Asia Afrika"][i % 5]} No. ${i + 10}, Jakarta`, - date: new Date(2022 + (i % 3), i % 12, i % 28 + 1), - type: ["Theft", "Assault", "Vandalism", "Robbery", "Fraud"][i % 5], - category: ["Property Crime", "Violent Crime", "Public Disturbance", "White Collar", "Misdemeanor"][i % 5] - }); +// Define types based on the crime data structure +interface CrimeIncident { + id: string + timestamp: Date + description: string + status: string + latitude?: number + longitude?: number + address?: string + crime_categories?: { + id: string + name: string } - return [...SAMPLE_CRIME_DATA, ...additionalData]; } -const EXPANDED_SAMPLE_DATA = generateSampleData(); +interface Crime { + id: string + district_id: string + month: number + year: number + crime_incidents: CrimeIncident[] + district: { + name: string + } +} +// Actions for the search bar const ACTIONS = [ { id: "incident_id", @@ -108,27 +85,32 @@ const ACTIONS = [ interface SearchTooltipProps { onControlChange?: (controlId: ITooltips) => void activeControl?: string + crimes?: Crime[] } -export default function SearchTooltip({ onControlChange, activeControl }: SearchTooltipProps) { +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 [suggestions, setSuggestions] = useState([]) 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 [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.district?.name || '', + year: crime.year, + month: crime.month + })) + ) + useEffect(() => { if (showSearch && searchInputRef.current) { setTimeout(() => { @@ -146,22 +128,21 @@ export default function SearchTooltip({ onControlChange, activeControl }: Search setSearchValue(prefix); setIsInputValid(true); - // Immediately filter and show suggestions based on the selected search type - let initialSuggestions: Array<{ id: string, description: string }> = []; + // Initial suggestions based on the selected search type + let initialSuggestions: CrimeIncident[] = []; - if (actionId === 'crime_id') { - initialSuggestions = EXPANDED_SAMPLE_DATA.filter(item => item.id.startsWith('CR-')); - } else if (actionId === 'incident_id') { - initialSuggestions = EXPANDED_SAMPLE_DATA.filter(item => item.id.startsWith('CI-')); + if (actionId === 'incident_id') { + initialSuggestions = allIncidents.slice(0, MAX_RESULTS); // Limit to 50 results initially } else if (actionId === 'description' || actionId === 'address') { - initialSuggestions = EXPANDED_SAMPLE_DATA; + initialSuggestions = allIncidents.slice(0, MAX_RESULTS); } - // Force a re-render by setting suggestions in the next tick + // Set suggestions in the next tick setTimeout(() => { setSuggestions(initialSuggestions); }, 0); + // Focus and position cursor after prefix setTimeout(() => { if (searchInputRef.current) { searchInputRef.current.focus(); @@ -172,48 +153,49 @@ export default function SearchTooltip({ onControlChange, activeControl }: Search } } - // Create a helper function for filtering suggestions - const filterSuggestions = (searchType: string, searchText: string): Array<{ id: string, description: string }> => { - let filtered: Array<{ id: string, description: string }> = []; + // Filter suggestions based on search type and search text + const filterSuggestions = (searchType: string, searchText: string): CrimeIncident[] => { + let filtered: CrimeIncident[] = []; - if (searchType === 'crime_id') { - if (!searchText || searchText === 'CR-') { - filtered = EXPANDED_SAMPLE_DATA.filter(item => item.id.startsWith('CR-')); - } else { - filtered = EXPANDED_SAMPLE_DATA.filter(item => - item.id.startsWith('CR-') && - (item.id.toLowerCase().includes(searchText.toLowerCase()) || - item.description.toLowerCase().includes(searchText.toLowerCase())) - ); - } - } - else if (searchType === 'incident_id') { + if (searchType === 'incident_id') { if (!searchText || searchText === 'CI-') { - filtered = EXPANDED_SAMPLE_DATA.filter(item => item.id.startsWith('CI-')); + filtered = allIncidents.slice(0, MAX_RESULTS); } else { - filtered = EXPANDED_SAMPLE_DATA.filter(item => - item.id.startsWith('CI-') && - (item.id.toLowerCase().includes(searchText.toLowerCase()) || - item.description.toLowerCase().includes(searchText.toLowerCase())) - ); + filtered = allIncidents.filter(item => + item.id.toLowerCase().includes(searchText.toLowerCase()) + ).slice(0, MAX_RESULTS); } } else if (searchType === 'description') { if (!searchText) { - filtered = EXPANDED_SAMPLE_DATA; + filtered = allIncidents.slice(0, MAX_RESULTS); } else { - filtered = EXPANDED_SAMPLE_DATA.filter(item => + filtered = allIncidents.filter(item => item.description.toLowerCase().includes(searchText.toLowerCase()) - ); + ).slice(0, MAX_RESULTS); } } else if (searchType === 'address') { if (!searchText) { - filtered = EXPANDED_SAMPLE_DATA; + filtered = allIncidents.slice(0, MAX_RESULTS); } else { - filtered = EXPANDED_SAMPLE_DATA.filter(item => - item.description.toLowerCase().includes(searchText.toLowerCase()) - ); + filtered = allIncidents.filter(item => + item.address && item.address.toLowerCase().includes(searchText.toLowerCase()) + ).slice(0, MAX_RESULTS); + } + } + else if (searchType === 'coordinates') { + if (!searchText) { + filtered = allIncidents.filter(item => item.latitude !== undefined && item.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.latitude !== undefined && + item.longitude !== undefined && + `${item.latitude}, ${item.longitude}`.includes(searchText) + ).slice(0, MAX_RESULTS); } } @@ -247,7 +229,7 @@ export default function SearchTooltip({ onControlChange, activeControl }: Search return; } - // Use the helper function to filter suggestions + // Filter suggestions based on search input setSuggestions(filterSuggestions(selectedSearchType, value)); } @@ -262,33 +244,28 @@ export default function SearchTooltip({ onControlChange, activeControl }: Search } }; - const handleSuggestionSelect = (item: { id: string, description: string }) => { - setSearchValue(item.id); + const handleSuggestionSelect = (incident: CrimeIncident) => { + setSearchValue(incident.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); + setSelectedSuggestion(incident); 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 + zoom: 15, + description: selectedSuggestion.description, + status: selectedSuggestion.status }, bubbles: true }); + document.dispatchEvent(flyToEvent); setShowInfoBox(false); setSelectedSuggestion(null); @@ -299,7 +276,7 @@ export default function SearchTooltip({ onControlChange, activeControl }: Search setShowInfoBox(false); setSelectedSuggestion(null); - // Restore original suggestions for the current search type + // Restore original suggestions if (selectedSearchType) { const initialSuggestions = filterSuggestions(selectedSearchType, searchValue); setSuggestions(initialSuggestions); @@ -316,6 +293,18 @@ export default function SearchTooltip({ onControlChange, activeControl }: Search } } + // Format date for display + const formatIncidentDate = (incident: CrimeIncident) => { + try { + if (incident.timestamp) { + return format(new Date(incident.timestamp), 'PPP p'); + } + return 'N/A'; + } catch (error) { + return 'Invalid date'; + } + }; + return ( <>
@@ -357,7 +346,7 @@ export default function SearchTooltip({ onControlChange, activeControl }: Search initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.9 }} - className="fixed transform top-1/4 left-1/4 z-50 w-full max-w-lg sm:max-w-xl md:max-w-3xl" + className="fixed top-1/4 left-1/4 transform -translate-x-1/4 -translate-y-1/4 z-50 w-full max-w-lg sm:max-w-xl md:max-w-3xl" >
@@ -403,33 +392,33 @@ export default function SearchTooltip({ onControlChange, activeControl }: Search

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

    - {suggestions.map((item, index) => ( + {suggestions.map((incident, index) => (
  • handleSuggestionSelect(item)} + onClick={() => handleSuggestionSelect(incident)} > - {item.id} + {incident.id}
    - {/* Show different information based on search type */} - {selectedSearchType === 'crime_id' || selectedSearchType === 'incident_id' ? ( + {selectedSearchType === 'incident_id' ? ( - {item.description} + {incident.description} - ) : selectedSearchType === 'coordinates' && 'coordinates' in item ? ( + ) : selectedSearchType === 'coordinates' ? ( - {typeof item.coordinates === 'string' ? item.coordinates : 'N/A'} - {item.description} + {incident.latitude}, {incident.longitude} - {incident.description} - ) : selectedSearchType === 'address' && 'address' in item ? ( + ) : selectedSearchType === 'address' ? ( - {'address' in item && typeof item.address === 'string' ? item.address : 'N/A'} + {incident.address || 'N/A'} ) : ( - {item.description} + {incident.description} )}
) diff --git a/sigap-website/app/_components/map/crime-map.tsx b/sigap-website/app/_components/map/crime-map.tsx index 0f9a8bb..4fb2d5c 100644 --- a/sigap-website/app/_components/map/crime-map.tsx +++ b/sigap-website/app/_components/map/crime-map.tsx @@ -109,21 +109,6 @@ export default function CrimeMap() { }) }, [filteredByYearAndMonth, selectedCategory]) - // Handle incident marker click - // const handleIncidentClick = (incident: CrimeIncident) => { - // console.log("Incident clicked directly:", incident); - // if (!incident.longitude || !incident.latitude) { - // console.error("Invalid incident coordinates:", incident); - // return; - // } - - // // When an incident is clicked, clear any selected district - // setSelectedDistrict(null); - - // // Set the selected incident - // setSelectedIncident(incident); - // } - // Set up event listener for incident clicks from the district layer useEffect(() => { const handleIncidentClickEvent = (e: CustomEvent) => { @@ -222,17 +207,6 @@ export default function CrimeMap() { } }, []); - // Handle district click - // const handleDistrictClick = (feature: DistrictFeature) => { - // console.log("District clicked in CrimeMap:", feature.name); - - // // When a district is clicked, clear any selected incident - // setSelectedIncident(null); - - // // Set the selected district (for the sidebar or other components) - // setSelectedDistrict(feature); - // } - // Handle year-month timeline change const handleTimelineChange = useCallback((year: number, month: number, progress: number) => { setSelectedYear(year) @@ -345,22 +319,21 @@ export default function CrimeMap() { {/* Components that are only visible in fullscreen mode */} {isFullscreen && ( <> - {/* */}
- -
- {/*
*/} + +
{/* Pass selectedCategory, selectedYear, and selectedMonth to the sidebar */}