From 223195e3fb473273afbbac8be5afcef299771639 Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Thu, 15 May 2025 15:47:56 +0700 Subject: [PATCH] Refactor map utility functions and components - Moved map utility functions from `map.ts` to `map/common.ts` for better organization. - Updated imports in `district-extrusion-layer.tsx`, `district-layer.tsx`, `layers.tsx`, and `distance-info.tsx` to reflect the new location of utility functions. - Enhanced `DistrictFillLineLayer` to improve handling of fill color expressions and opacity based on active controls. - Introduced new types for crime source in `map.ts` and updated relevant components to use these types. - Added a new `IncidentDetailTab` component to display detailed information about incidents, including severity and status. - Improved handling of severity and status information across components for consistency and clarity. --- .../left/sidebar/components/incident-card.tsx | 347 ++++++++---------- .../map/controls/left/sidebar/map-sidebar.tsx | 3 +- .../left/sidebar/tabs/incident-detail-tab.tsx | 284 ++++++++++++++ .../left/sidebar/tabs/incidents-tab.tsx | 65 +++- .../left/sidebar/tabs/statistics-tab.tsx | 10 + .../map/controls/source-type-selector.tsx | 13 +- .../map/controls/top/additional-tooltips.tsx | 5 +- .../_components/map/controls/top/tooltips.tsx | 5 +- .../app/_components/map/crime-map.tsx | 60 +-- .../_components/map/layers/cluster-layer.tsx | 2 +- .../map/layers/district-extrusion-layer.tsx | 2 +- .../_components/map/layers/district-layer.tsx | 106 +++--- .../app/_components/map/layers/layers.tsx | 6 +- .../_components/map/pop-up/distance-info.tsx | 2 +- .../app/_utils/{map.ts => map/common.tsx} | 143 +++++++- sigap-website/app/_utils/types/map.ts | 3 + 16 files changed, 741 insertions(+), 315 deletions(-) create mode 100644 sigap-website/app/_components/map/controls/left/sidebar/tabs/incident-detail-tab.tsx rename sigap-website/app/_utils/{map.ts => map/common.tsx} (65%) diff --git a/sigap-website/app/_components/map/controls/left/sidebar/components/incident-card.tsx b/sigap-website/app/_components/map/controls/left/sidebar/components/incident-card.tsx index 34e028e..0844c6a 100644 --- a/sigap-website/app/_components/map/controls/left/sidebar/components/incident-card.tsx +++ b/sigap-website/app/_components/map/controls/left/sidebar/components/incident-card.tsx @@ -3,6 +3,8 @@ import { Info, Clock, MapPin, AlertTriangle, ChevronRight, AlertCircle, Shield } import { Card, CardContent, CardHeader, CardFooter } from "@/app/_components/ui/card" import { Badge } from "@/app/_components/ui/badge" import { cn } from "@/app/_lib/utils" +import { useRouter } from "next/navigation" +import { getSeverityGradient, getStatusInfo, normalizeSeverity } from '@/app/_utils/map/common' interface IIncidentCardProps { title: string @@ -14,144 +16,158 @@ interface IIncidentCardProps { showTimeAgo?: boolean status?: string | true isUserReport?: boolean + incidentId?: string // Added incident ID for navigation } -export function IncidentCard({ - title, - location, - time, - severity = 1, - onClick, - className, - showTimeAgo = true, - status, - isUserReport = false -}: IIncidentCardProps) { - // Helper to normalize severity to a number - const normalizeSeverity = (sev: number | "Low" | "Medium" | "High" | "Critical"): number => { - if (typeof sev === "number") return sev; - switch (sev) { - case "Critical": - return 4; - case "High": - return 3; - case "Medium": - return 2; - case "Low": - default: - return 1; - } - }; +// export function IncidentCard({ +// title, +// location, +// time, +// severity = 1, +// onClick, +// className, +// showTimeAgo = true, +// status, +// isUserReport = false, +// incidentId +// }: IIncidentCardProps) { +// const router = useRouter() - const getSeverityColor = (severity: number) => { - switch (severity) { - case 4: - return "border-purple-600 bg-purple-50 dark:bg-purple-950/30" - case 3: - return "border-red-500 bg-red-50 dark:bg-red-950/30" - case 2: - return "border-yellow-400 bg-yellow-50 dark:bg-yellow-950/30" - case 1: - default: - return "border-blue-500 bg-blue-50 dark:bg-blue-950/30" - } - } +// // Helper to normalize severity to a number +// const normalizeSeverity = (sev: number | "Low" | "Medium" | "High" | "Critical"): number => { +// if (typeof sev === "number") return sev; +// switch (sev) { +// case "Critical": +// return 4; +// case "High": +// return 3; +// case "Medium": +// return 2; +// case "Low": +// default: +// return 1; +// } +// }; - const getSeverityText = (severity: number) => { - switch (severity) { - case 4: - return "text-purple-600 dark:text-purple-400" - case 3: - return "text-red-500 dark:text-red-400" - case 2: - return "text-yellow-600 dark:text-yellow-400" - case 1: - default: - return "text-blue-500 dark:text-blue-400" - } - } +// const getSeverityColor = (severity: number) => { +// switch (severity) { +// case 4: +// return "border-purple-600 bg-purple-50 dark:bg-purple-950/30" +// case 3: +// return "border-red-500 bg-red-50 dark:bg-red-950/30" +// case 2: +// return "border-yellow-400 bg-yellow-50 dark:bg-yellow-950/30" +// case 1: +// default: +// return "border-blue-500 bg-blue-50 dark:bg-blue-950/30" +// } +// } - const getSeverityLabel = (severity: number) => { - switch (severity) { - case 4: - return "Critical" - case 3: - return "High" - case 2: - return "Medium" - case 1: - default: - return "Low" - } - } +// const getSeverityText = (severity: number) => { +// switch (severity) { +// case 4: +// return "text-purple-600 dark:text-purple-400" +// case 3: +// return "text-red-500 dark:text-red-400" +// case 2: +// return "text-yellow-600 dark:text-yellow-400" +// case 1: +// default: +// return "text-blue-500 dark:text-blue-400" +// } +// } - const getStatusColor = (status: string) => { - switch (status?.toLowerCase()) { - case "resolved": - return "bg-green-100 text-green-700 border-green-300 dark:bg-green-950/30 dark:text-green-400 dark:border-green-800" - case "pending": - default: - return "bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-800" - } - } +// const getSeverityLabel = (severity: number) => { +// switch (severity) { +// case 4: +// return "Critical" +// case 3: +// return "High" +// case 2: +// return "Medium" +// case 1: +// default: +// return "Low" +// } +// } - const normalizedSeverity = normalizeSeverity(severity); +// const getStatusColor = (status: string) => { +// switch (status?.toLowerCase()) { +// case "resolved": +// return "bg-green-100 text-green-700 border-green-300 dark:bg-green-950/30 dark:text-green-400 dark:border-green-800" +// case "pending": +// default: +// return "bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-800" +// } +// } - return ( - - -
-
-
- - {getSeverityLabel(normalizedSeverity)} - - {isUserReport && ( - - User Report - - )} -
+// const normalizedSeverity = normalizeSeverity(severity); -

{title}

+// // Handle card click - either use provided onClick or navigate to incident detail page +// const handleClick = (e: React.MouseEvent) => { +// if (onClick) { +// onClick() +// } else if (incidentId) { +// // Open incident detail in new tab +// window.open(`/incidents/detail/${incidentId}`, '_blank') +// } +// } -
-
- - {time} -
+// return ( +// +// +//
+//
+//
+// +// {getSeverityLabel(normalizedSeverity)} +// +// {isUserReport && ( +// +// User Report +// +// )} +//
-
- - {location} -
-
+//

{title}

- {/* Add status badges for user reports */} - {isUserReport && typeof status === "string" && ( - - {status.charAt(0).toUpperCase() + status.slice(1)} - - )} -
+//
+//
+// +// {time} +//
-
- -
-
-
-
- ) -} +//
+// +// {location} +//
+//
+ +// {/* Add status badges for user reports */} +// {isUserReport && typeof status === "string" && ( +// +// {status.charAt(0).toUpperCase() + status.slice(1)} +// +// )} +//
+ +//
+// +//
+//
+//
+//
+// ) +// } export function IncidentCardV2({ title, @@ -163,75 +179,22 @@ export function IncidentCardV2({ showTimeAgo = true, status, isUserReport = false, + incidentId }: IIncidentCardProps) { - // Helper to normalize severity to a number - const normalizeSeverity = (sev: number | "Low" | "Medium" | "High" | "Critical"): number => { - if (typeof sev === "number") return sev - switch (sev) { - case "Critical": - return 4 - case "High": - return 3 - case "Medium": - return 2 - case "Low": - default: - return 1 - } - } - - const getSeverityGradient = (severity: number) => { - switch (severity) { - case 4: - return "bg-gradient-to-r from-purple-500/10 to-purple-600/5 dark:from-purple-900/20 dark:to-purple-800/10" - case 3: - return "bg-gradient-to-r from-red-500/10 to-red-600/5 dark:from-red-900/20 dark:to-red-800/10" - case 2: - return "bg-gradient-to-r from-yellow-500/10 to-yellow-600/5 dark:from-yellow-900/20 dark:to-yellow-800/10" - case 1: - default: - return "bg-gradient-to-r from-blue-500/10 to-blue-600/5 dark:from-blue-900/20 dark:to-blue-800/10" - } - } - - const getSeverityIconColor = (severity: number) => { - switch (severity) { - case 4: - return "text-purple-600 dark:text-purple-400" - case 3: - return "text-red-500 dark:text-red-400" - case 2: - return "text-yellow-600 dark:text-yellow-400" - case 1: - default: - return "text-blue-500 dark:text-blue-400" - } - } - - const getSeverityIcon = (severity: number) => { - switch (severity) { - case 4: - case 3: - return - case 2: - case 1: - default: - return - } - } - - const getStatusColor = (status: string) => { - switch (status?.toLowerCase()) { - case "resolved": - return "bg-green-100 text-green-700 border-green-300 dark:bg-green-950/30 dark:text-green-400 dark:border-green-800" - case "pending": - default: - return "bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-800" - } - } + const router = useRouter() const normalizedSeverity = normalizeSeverity(severity) + // Handle card click - either use provided onClick or navigate to incident detail page + const handleClick = (e: React.MouseEvent) => { + if (onClick) { + onClick() + } else if (incidentId) { + // Open incident detail in new tab + window.open(`/incidents/detail/${incidentId}`, '_blank') + } + } + return (
@@ -271,7 +234,7 @@ export function IncidentCardV2({ )} {isUserReport && typeof status === "string" && ( - + {status.charAt(0).toUpperCase() + status.slice(1)} )} diff --git a/sigap-website/app/_components/map/controls/left/sidebar/map-sidebar.tsx b/sigap-website/app/_components/map/controls/left/sidebar/map-sidebar.tsx index a626130..8147eee 100644 --- a/sigap-website/app/_components/map/controls/left/sidebar/map-sidebar.tsx +++ b/sigap-website/app/_components/map/controls/left/sidebar/map-sidebar.tsx @@ -35,6 +35,7 @@ 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"; +import { ICrimeSourceTypes } from "@/app/_utils/types/map"; interface CrimeSidebarProps { className?: string; @@ -45,7 +46,7 @@ interface CrimeSidebarProps { crimes: ICrimes[]; recentIncidents?: IIncidentLogs[]; // User reports from last 24 hours isLoading?: boolean; - sourceType?: string; + sourceType?: ICrimeSourceTypes; } export default function CrimeSidebar({ diff --git a/sigap-website/app/_components/map/controls/left/sidebar/tabs/incident-detail-tab.tsx b/sigap-website/app/_components/map/controls/left/sidebar/tabs/incident-detail-tab.tsx new file mode 100644 index 0000000..47c365d --- /dev/null +++ b/sigap-website/app/_components/map/controls/left/sidebar/tabs/incident-detail-tab.tsx @@ -0,0 +1,284 @@ +import React from "react"; +import { ArrowLeft, Calendar, Clock, MapPin, AlertCircle, FileText, Shield, User } from "lucide-react"; +import { Card, CardContent, CardHeader, CardFooter, CardTitle } from "@/app/_components/ui/card"; +import { Button } from "@/app/_components/ui/button"; +import { Badge } from "@/app/_components/ui/badge"; +import { Separator } from "@/app/_components/ui/separator"; +import { format } from 'date-fns'; +import Image from "next/image"; +import { cn } from "@/app/_lib/utils"; + + +// Use the same IIncident interface as in incidents-tab.tsx +interface IIncident { + id: string; + category: string; + address: string; + timestamp: string | Date; + district?: string; + severity?: number | "Low" | "Medium" | "High" | "Critical"; + status?: string | true; + description?: string; + location?: { + lat: number; + lng: number; + }; + reporter?: string; + reporter_id?: string; + images?: string[]; + responding_unit?: string; + resolution?: string; + created_at?: string | Date; + updated_at?: string | Date; +} + +interface IIncidentDetailTabProps { + incident: IIncident; + onBack: () => void; +} + + +export default function IncidentDetailTab({ incident, onBack }: IIncidentDetailTabProps) { + // Format the timestamp for display + const formatDate = (date: string | Date) => { + if (!date) return "Unknown date"; + try { + const dateObj = typeof date === 'string' ? new Date(date) : date; + return format(dateObj, 'MMMM d, yyyy - HH:mm'); + } catch (error) { + return "Invalid date"; + } + }; + + const normalizeSeverity = (sev: number | "Low" | "Medium" | "High" | "Critical" | undefined): string => { + if (typeof sev === "string") return sev; + switch (sev) { + case 4: + return "Critical"; + case 3: + return "High"; + case 2: + return "Medium"; + case 1: + return "Low"; + default: + return "Low"; + } + } + + const getSeverityGradient = (severity: string) => { + switch (severity) { + case "Critical": + return "bg-gradient-to-r from-purple-500/10 to-purple-600/5 dark:from-purple-900/20 dark:to-purple-800/10" + case "High": + return "bg-gradient-to-r from-red-500/10 to-red-600/5 dark:from-red-900/20 dark:to-red-800/10" + case "Medium": + return "bg-gradient-to-r from-yellow-500/10 to-yellow-600/5 dark:from-yellow-900/20 dark:to-yellow-800/10" + case "Low": + default: + return "bg-gradient-to-r from-blue-500/10 to-blue-600/5 dark:from-blue-900/20 dark:to-blue-800/10" + } + } + + const getSeverityIconColor = (severity: number) => { + switch (severity) { + case 4: + return "text-purple-600 dark:text-purple-400" + case 3: + return "text-red-500 dark:text-red-400" + case 2: + return "text-yellow-600 dark:text-yellow-400" + case 1: + default: + return "text-blue-500 dark:text-blue-400" + } + } + + const getStatusInfo = (status?: string | true) => { + if (status === true) { + return { + text: "Verified", + color: "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-400" + }; + } + + if (!status) { + return { + text: "Pending", + color: "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400" + }; + } + + switch (status.toLowerCase()) { + case "resolved": + return { + text: "Resolved", + color: "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-400" + }; + case "in progress": + return { + text: "In Progress", + color: "bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-900/30 dark:text-amber-400" + }; + case "pending": + return { + text: "Pending", + color: "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400" + }; + default: + return { + text: status.charAt(0).toUpperCase() + status.slice(1), + color: "bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-900/30 dark:text-gray-400" + }; + } + }; + // Get appropriate color for severity badge + const getSeverityColor = (severity: string) => { + switch (severity) { + case "Critical": return "bg-red-100 text-red-800 border-red-200 dark:bg-red-900/30 dark:text-red-400"; + case "High": return "bg-orange-100 text-orange-800 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400"; + case "Medium": return "bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400"; + case "Low": return "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-400"; + default: return "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400"; + } + }; + + + const severityText = normalizeSeverity(incident.severity); + const severityColor = getSeverityColor(severityText); + const statusInfo = getStatusInfo(incident.status); + + return ( + <> + {/* Header with back button */} +
+ +
+ + {/* Main incident info card */} + + +
+ {incident.category} +
+ + {severityText} + + + {statusInfo.text} + +
+
+
+ + +
+
+ + {formatDate(incident.timestamp)} +
+ +
+ +
+

{incident.address}

+ {incident.district &&

{incident.district}

} + {incident.location && ( +

+ Coordinates: {incident.location.lat.toFixed(6)}, {incident.location.lng.toFixed(6)} +

+ )} +
+
+ + + +
+

+ Description +

+

+ {incident.description || "No description provided."} +

+
+ + {(incident.reporter || incident.reporter_id) && ( + <> + +
+ + Reported by: {incident.reporter || "Anonymous"} +
+ + )} + + {incident.responding_unit && ( +
+ + Responding unit: {incident.responding_unit} +
+ )} +
+
+ + + ID: {incident.id} + {incident.created_at && ( + Created: {formatDate(incident.created_at)} + )} + +
+ + {/* Display resolution information if available */} + {incident.resolution && ( + + + + + Resolution + + + +

{incident.resolution}

+
+
+ )} + + {/* Display images if available */} + {incident.images && incident.images.length > 0 && ( + + + Evidence Images + + +
+ {incident.images.map((image, index) => ( +
+ {`Evidence +
+ ))} +
+
+
+ )} + + ); +} diff --git a/sigap-website/app/_components/map/controls/left/sidebar/tabs/incidents-tab.tsx b/sigap-website/app/_components/map/controls/left/sidebar/tabs/incidents-tab.tsx index 0ce9031..53131f3 100644 --- a/sigap-website/app/_components/map/controls/left/sidebar/tabs/incidents-tab.tsx +++ b/sigap-website/app/_components/map/controls/left/sidebar/tabs/incidents-tab.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { AlertCircle, AlertTriangle, @@ -28,9 +28,11 @@ import { getTimeAgo, } from "@/app/_utils/common"; import { SystemStatusCard } from "../components/system-status-card"; -import { IncidentCard, IncidentCardV2 } from "../components/incident-card"; +import { IncidentCardV2 } from "../components/incident-card"; import { ICrimeAnalytics } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_hooks/use-crime-analytics"; import { IIncidentLogs } from "@/app/_utils/types/crimes"; +import IncidentDetailTab from "./incident-detail-tab"; +import { ICrimeSourceTypes } from "@/app/_utils/types/map"; interface Incident { id: string; @@ -62,6 +64,7 @@ interface SidebarIncidentsTabProps { activeIncidentTab: string; setActiveIncidentTab: (tab: string) => void; recentIncidents?: IIncidentLogs[]; // User reports from last 24 hours + sourceType?: ICrimeSourceTypes; // Data source type (CBT or CBU) } export function SidebarIncidentsTab({ @@ -80,11 +83,14 @@ export function SidebarIncidentsTab({ setActiveIncidentTab, sourceType = "cbt", recentIncidents = [], -}: SidebarIncidentsTabProps & { sourceType?: string }) { +}: SidebarIncidentsTabProps) { const currentYear = new Date().getFullYear(); const isCurrentYear = selectedYear === currentYear || selectedYear === "all"; + const [selectedIncidentDetail, setSelectedIncidentDetail] = useState(null); + const [showDetailView, setShowDetailView] = useState(false); + const topCategories = crimeStats.categoryCounts ? Object.entries(crimeStats.categoryCounts) .sort((a, b) => b[1] - a[1]) @@ -131,6 +137,33 @@ export function SidebarIncidentsTab({ new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ); + const handleSwicthToCurrentYear = () => { + window.dispatchEvent( + new CustomEvent("set-year", { + detail: currentYear, + }), + ); + } + + const handleSwitchDataSource = () => { + window.dispatchEvent( + new CustomEvent("set-data-source", { + detail: sourceType === "cbt" ? "cbu" : "cbt", + }), + ); + } + + const handleIncidentCardClick = (incident: Incident) => { + setSelectedIncidentDetail(incident); + setShowDetailView(true); + handleIncidentClick(incident); + }; + + const handleBackToList = () => { + setShowDetailView(false); + setSelectedIncidentDetail(null); + }; + // If source type is CBU, display warning instead of regular content if (sourceType === "cbu") { return ( @@ -180,6 +213,7 @@ export function SidebarIncidentsTab({ variant="outline" size="sm" className="border-emerald-500/50 hover:bg-emerald-500/20 text-emerald-300" + onClick={handleSwitchDataSource} > Change Data Source @@ -198,6 +232,15 @@ export function SidebarIncidentsTab({ ); } + if (showDetailView && selectedIncidentDetail) { + return ( + + ); + } + return ( <> {/* Enhanced info card */} @@ -329,12 +372,8 @@ export function SidebarIncidentsTab({ size="sm" variant="outline" className="text-xs border-amber-500/50 bg-amber-500/10 hover:bg-amber-500/20 text-amber-300" - onClick={() => - window.dispatchEvent( - new CustomEvent("set-year", { - detail: currentYear, - }), - )} + onClick={handleSwicthToCurrentYear} + > Switch to {currentYear} @@ -368,6 +407,7 @@ export function SidebarIncidentsTab({ ) => ( - handleIncidentClick( - incident as Incident, - )} + handleIncidentCardClick(incident as Incident)} status={incident.status} isUserReport={true} /> @@ -494,6 +532,7 @@ export function SidebarIncidentsTab({ ) => ( - handleIncidentClick( + handleIncidentCardClick( incident, )} showTimeAgo={false} diff --git a/sigap-website/app/_components/map/controls/left/sidebar/tabs/statistics-tab.tsx b/sigap-website/app/_components/map/controls/left/sidebar/tabs/statistics-tab.tsx index 103fd54..7dee01b 100644 --- a/sigap-website/app/_components/map/controls/left/sidebar/tabs/statistics-tab.tsx +++ b/sigap-website/app/_components/map/controls/left/sidebar/tabs/statistics-tab.tsx @@ -287,6 +287,15 @@ export function SidebarStatisticsTab({ return { type, count, percentage } }) : [] + const handleSwitchDataSource = () => { + window.dispatchEvent( + new CustomEvent("set-data-source", { + detail: sourceType === "cbt" ? "cbu" : "cbt", + }), + ); + } + + // If source type is CBU, display warning instead of regular content if (sourceType === "cbu") { @@ -324,6 +333,7 @@ export function SidebarStatisticsTab({ variant="outline" size="sm" className="border-emerald-500/50 hover:bg-emerald-500/20 text-emerald-300" + onClick={handleSwitchDataSource} > Change Data Source diff --git a/sigap-website/app/_components/map/controls/source-type-selector.tsx b/sigap-website/app/_components/map/controls/source-type-selector.tsx index 96aac60..69e195e 100644 --- a/sigap-website/app/_components/map/controls/source-type-selector.tsx +++ b/sigap-website/app/_components/map/controls/source-type-selector.tsx @@ -10,10 +10,11 @@ import { import { cn } from "@/app/_lib/utils" import { useEffect, useRef, useState } from "react" import { Skeleton } from "../../ui/skeleton" +import { ICrimeSourceTypes } from "@/app/_utils/types/map" interface SourceTypeSelectorProps { - selectedSourceType: string - onSourceTypeChange: (sourceType: string) => void + selectedSourceType: ICrimeSourceTypes + onSourceTypeChange: (sourceType: ICrimeSourceTypes) => void availableSourceTypes: string[] className?: string isLoading?: boolean @@ -46,7 +47,7 @@ export default function SourceTypeSelector({ ) : (