feat: Enhance Overlay component with customizable styles and addControl method

- Updated OverlayProps to include className and style for customization.
- Modified OverlayControl to apply custom styles and handle control addition.
- Improved rendering logic in _Overlay to pass addControl method to children.
- Refactored SelectContent to accept a container prop for better positioning.
- Cleaned up useFullscreen hook by removing unnecessary comments and improving readability.
- Updated global CSS for better styling of map popups and controls.
- Removed unused dependencies related to mapbox-gl-draw from package.json and package-lock.json.
- Added CustomControl and MonthSelector components for enhanced map functionality.
This commit is contained in:
vergiLgood1 2025-05-01 21:15:31 +07:00
parent 31f12ab88b
commit 897a130ff7
15 changed files with 673 additions and 648 deletions

View File

@ -1,2 +1,30 @@
// Ensure no usage of `supabase/server.ts` here
// If needed, replace with `supabase/client.ts` for client-side functionality
import { useQuery } from '@tanstack/react-query';
import {
getAvailableYears,
getCrimeByYearAndMonth,
getCrimes,
} from '../action';
export const useGetAvailableYears = () => {
return useQuery({
queryKey: ['available-years'],
queryFn: () => getAvailableYears(),
});
};
export const useGetCrimeByYearAndMonth = (
year: number,
month: number | 'all'
) => {
return useQuery({
queryKey: ['crimes', year, month],
queryFn: () => getCrimeByYearAndMonth(year, month),
});
};
export const useGetCrimes = () => {
return useQuery({
queryKey: ['crimes'],
queryFn: () => getCrimes(),
});
};

View File

@ -0,0 +1,66 @@
import { Map } from "mapbox-gl";
/* Idea from Stack Overflow https://stackoverflow.com/a/51683226 */
export class CustomControl {
private _className: string;
private _title: string;
private _eventHandler: (event: MouseEvent) => void;
private _btn!: HTMLButtonElement;
private _container!: HTMLDivElement;
private _map?: Map;
private _root: any; // React root for rendering our component
constructor({
className = "",
title = "",
eventHandler = () => { }
}: {
className?: string;
title?: string;
eventHandler?: (event: MouseEvent) => void;
}) {
this._className = className;
this._title = title;
this._eventHandler = eventHandler;
}
onAdd(map: Map) {
this._map = map;
this._btn = document.createElement("button");
this._btn.className = "mapboxgl-ctrl-icon" + " " + this._className;
this._btn.type = "button";
this._btn.title = this._title;
this._btn.onclick = this._eventHandler;
// Apply pointer-events: auto; style dynamically
this._btn.style.pointerEvents = "auto";
// Dynamically append the style to the auto-generated className
const styleSheet = document.styleSheets[0];
styleSheet.insertRule(
`.${this._className} { pointer-events: auto; }`,
styleSheet.cssRules.length
);
this._container = document.createElement("div");
this._container.className = "mapboxgl-ctrl-group mapboxgl-ctrl";
this._container.appendChild(this._btn);
return this._container;
}
onRemove() {
if (this._container && this._container.parentNode) {
this._container.parentNode.removeChild(this._container);
}
// Defer unmounting React component to prevent race conditions
if (this._root) {
setTimeout(() => {
this._root.unmount();
});
}
this._map = undefined;
}
}

View File

@ -2,47 +2,38 @@
import { Button } from "@/app/_components/ui/button"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/app/_components/ui/tooltip"
import {
Thermometer,
Droplets,
Wind,
Cloud,
Eye,
Clock,
AlertTriangle,
MapIcon,
BarChart3,
Users,
Siren,
Building,
Skull,
} from "lucide-react"
import { Overlay } from "../overlay"
import { ControlPosition } from "mapbox-gl"
import { IconBriefcaseOff, IconCategory, IconCategoryFilled } from "@tabler/icons-react"
interface MapControlsProps {
interface MapMenusProps {
onControlChange: (control: string) => void
activeControl: string
position?: ControlPosition
position: ControlPosition
}
export default function MapControls({ onControlChange, activeControl, position = "top-left" }: MapControlsProps) {
const controls = [
{ id: "crime-rate", icon: <Thermometer size={20} />, label: "Crime Rate" },
{ id: "theft", icon: <Droplets size={20} />, label: "Theft" },
{ id: "violence", icon: <Wind size={20} />, label: "Violence" },
{ id: "vandalism", icon: <Cloud size={20} />, label: "Vandalism" },
{ id: "traffic", icon: <Eye size={20} />, label: "Traffic" },
{ id: "time", icon: <Clock size={20} />, label: "Time Analysis" },
export default function MapMenus({ onControlChange, activeControl, position = "top-left" }: MapMenusProps) {
const menus = [
{ id: "crime-rate", icon: <Skull size={20} />, label: "Crime Rate" },
{ id: "population", icon: <Users size={20} />, label: "Population" },
{ id: "unemployment", icon: <IconBriefcaseOff size={20} />, label: "Unemployment" },
{ id: "alerts", icon: <AlertTriangle size={20} className="text-amber-500" />, label: "Alerts" },
{ id: "districts", icon: <MapIcon size={20} />, label: "Districts" },
{ id: "statistics", icon: <BarChart3 size={20} />, label: "Statistics" },
{ id: "demographics", icon: <Users size={20} />, label: "Demographics" },
{ id: "emergency", icon: <Siren size={20} />, label: "Emergency" },
{ id: "time", icon: <Clock size={20} />, label: "Time Analysis" },
{ id: "unit", icon: <Building size={20} />, label: "Unit" },
{ id: "category", icon: <IconCategory size={20} />, label: "Category" },
]
return (
<Overlay position={position}>
<div className="absolute top-0 left-0 z-10 bg-black/75 rounded-md m-2 p-1 flex items-center space-x-1">
<TooltipProvider>
{controls.map((control) => (
{menus.map((control) => (
<Tooltip key={control.id}>
<TooltipTrigger asChild>
<Button
@ -65,6 +56,6 @@ export default function MapControls({ onControlChange, activeControl, position =
))}
</TooltipProvider>
</div>
</Overlay>
)
}

View File

@ -1,10 +1,11 @@
"use client"
import { ChevronLeft, ChevronRight, Cloud, Droplets, Wind } from "lucide-react"
import { Button } from "@/app/_components/ui/button"
import { ChevronLeft, Filter, Map, BarChart3, Info } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/app/_components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/tabs"
import { ScrollArea } from "@/app/_components/ui/scroll-area"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/app/_components/ui/card"
import { Separator } from "@/app/_components/ui/separator"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/app/_components/ui/collapsible"
import { cn } from "@/app/_lib/utils"
interface MapSidebarProps {
isOpen: boolean
@ -12,297 +13,170 @@ interface MapSidebarProps {
crimes?: Array<{
id: string
district_name: string
distrcit_id?: string
district_id?: string
number_of_crime?: number
level?: "low" | "medium" | "high" | "critical"
incidents: any[]
}>
selectedYear?: number | string
selectedMonth?: number | string
weatherData?: {
temperature: number
condition: string
humidity: number
windSpeed: number
forecast: Array<{
time: string
temperature: number
condition: string
}>
}
}
export default function MapSidebar({ isOpen, onToggle, crimes = [], selectedYear, selectedMonth }: MapSidebarProps) {
// Calculate some statistics for the sidebar
const totalIncidents = crimes.reduce((total, district) => total + (district.number_of_crime || 0), 0)
const highRiskDistricts = crimes.filter(
(district) => district.level === "high" || district.level === "critical",
).length
const districtCount = crimes.length
export default function MapSidebar({
isOpen,
onToggle,
crimes = [],
selectedYear,
selectedMonth,
weatherData = {
temperature: 78,
condition: "Mostly cloudy",
humidity: 65,
windSpeed: 8,
forecast: [
{ time: "Now", temperature: 78, condition: "Cloudy" },
{ time: "9:00 PM", temperature: 75, condition: "Cloudy" },
{ time: "10:00 PM", temperature: 73, condition: "Cloudy" },
{ time: "11:00 PM", temperature: 72, condition: "Cloudy" },
{ time: "12:00 AM", temperature: 70, condition: "Cloudy" },
],
},
}: MapSidebarProps) {
return (
<div
className={`absolute top-0 left-0 h-full bg-white dark:bg-gray-900 shadow-lg z-20 transition-all duration-300 ease-in-out ${
isOpen ? "w-80" : "w-0"
} overflow-hidden`}
className={cn(
"h-full bg-background border-r transition-all duration-300 flex flex-col",
isOpen ? "w-80" : "w-0 overflow-hidden",
)}
>
<div className="flex flex-col h-full">
<div className="flex items-center justify-between p-4 border-b">
<h2 className="font-semibold text-lg">Crime Map Explorer</h2>
<Button variant="ghost" size="icon" onClick={onToggle}>
<ChevronLeft className="h-5 w-5" />
<h2 className="text-lg font-semibold">Weather Information</h2>
<Button variant="ghost" size="icon" onClick={onToggle} className="h-8 w-8">
<ChevronLeft className="h-4 w-4" />
<span className="sr-only">Close sidebar</span>
</Button>
</div>
<Tabs defaultValue="overview" className="flex-1 flex flex-col">
<TabsList className="grid grid-cols-4 mx-2 mt-2">
<TabsTrigger value="overview">
<Map className="h-4 w-4 mr-1" />
<span className="sr-only sm:not-sr-only sm:inline-block">Overview</span>
</TabsTrigger>
<TabsTrigger value="filters">
<Filter className="h-4 w-4 mr-1" />
<span className="sr-only sm:not-sr-only sm:inline-block">Filters</span>
</TabsTrigger>
<TabsTrigger value="stats">
<BarChart3 className="h-4 w-4 mr-1" />
<span className="sr-only sm:not-sr-only sm:inline-block">Stats</span>
</TabsTrigger>
<TabsTrigger value="info">
<Info className="h-4 w-4 mr-1" />
<span className="sr-only sm:not-sr-only sm:inline-block">Info</span>
</TabsTrigger>
<div className="flex-1 overflow-auto p-4">
<Tabs defaultValue="current">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="current">Current</TabsTrigger>
<TabsTrigger value="forecast">Forecast</TabsTrigger>
</TabsList>
<ScrollArea className="flex-1 p-4">
<TabsContent value="overview" className="mt-0 space-y-4">
<TabsContent value="current" className="space-y-4 mt-4">
<Card>
<CardHeader className="pb-2">
<CardTitle>Crime Summary</CardTitle>
<CardDescription>
{selectedYear}
{selectedMonth !== "all" ? ` - Month ${selectedMonth}` : ""}
</CardDescription>
<CardTitle className="text-2xl font-bold flex items-center justify-between">
<span>{weatherData.temperature}°F</span>
<span className="text-sm font-normal">{weatherData.condition}</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col">
<span className="text-sm text-muted-foreground">Total Incidents</span>
<span className="text-2xl font-bold">{totalIncidents}</span>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center gap-2">
<Droplets className="h-4 w-4 text-blue-500" />
<span>Humidity: {weatherData.humidity}%</span>
</div>
<div className="flex flex-col">
<span className="text-sm text-muted-foreground">High Risk Areas</span>
<span className="text-2xl font-bold">{highRiskDistricts}</span>
</div>
<div className="flex flex-col">
<span className="text-sm text-muted-foreground">Districts</span>
<span className="text-2xl font-bold">{districtCount}</span>
</div>
<div className="flex flex-col">
<span className="text-sm text-muted-foreground">Data Points</span>
<span className="text-2xl font-bold">
{crimes.reduce((total, district) => total + district.incidents.length, 0)}
</span>
<div className="flex items-center gap-2">
<Wind className="h-4 w-4 text-gray-500" />
<span>Wind: {weatherData.windSpeed} mph</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle>District Overview</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="max-h-64 overflow-y-auto">
<table className="w-full">
<thead className="sticky top-0 bg-white dark:bg-gray-900">
<tr className="border-b">
<th className="text-left p-2 text-sm">District</th>
<th className="text-right p-2 text-sm">Incidents</th>
<th className="text-right p-2 text-sm">Level</th>
</tr>
</thead>
<tbody>
{crimes
.sort((a, b) => (b.number_of_crime || 0) - (a.number_of_crime || 0))
.map((district) => (
<tr key={district.id} className="border-b hover:bg-muted/50">
<td className="p-2 text-sm">{district.district_name}</td>
<td className="text-right p-2 text-sm">{district.number_of_crime || 0}</td>
<td className="text-right p-2 text-sm">
<div className="space-y-2">
<h3 className="text-sm font-medium">Today's Recommendations</h3>
<div className="grid grid-cols-2 gap-2">
<Card className="bg-muted/50">
<CardContent className="p-3">
<div className="flex flex-col items-center text-center">
<div className="mb-1">🌂</div>
<div className="text-xs font-medium">Umbrella</div>
<div className="text-xs">No need</div>
</div>
</CardContent>
</Card>
<Card className="bg-muted/50">
<CardContent className="p-3">
<div className="flex flex-col items-center text-center">
<div className="mb-1">🏞</div>
<div className="text-xs font-medium">Outdoors</div>
<div className="text-xs text-red-500">Very poor</div>
</div>
</CardContent>
</Card>
</div>
</div>
<Collapsible className="w-full">
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="flex w-full justify-between p-0 h-8">
<span>Crime Statistics</span>
<ChevronRight className="h-4 w-4 transition-transform ui-open:rotate-90" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-2 mt-2">
{crimes.length > 0 ? (
crimes.map((crime) => (
<Card key={crime.id} className="bg-muted/50">
<CardContent className="p-3">
<div className="flex justify-between items-center">
<span className="text-sm">{crime.district_name}</span>
<span
className={`inline-block px-2 py-0.5 rounded-full text-xs ${
district.level === "low"
? "bg-green-100 text-green-800"
: district.level === "medium"
? "bg-yellow-100 text-yellow-800"
: district.level === "high"
? "bg-orange-100 text-orange-800"
: district.level === "critical"
? "bg-red-100 text-red-800"
: "bg-gray-100 text-gray-800"
}`}
className={cn(
"text-xs px-2 py-0.5 rounded-full",
crime.level === "low" && "bg-green-100 text-green-800",
crime.level === "medium" && "bg-yellow-100 text-yellow-800",
crime.level === "high" && "bg-orange-100 text-orange-800",
crime.level === "critical" && "bg-red-100 text-red-800",
)}
>
{district.level || "N/A"}
{crime.number_of_crime}
</span>
</td>
</tr>
</div>
</CardContent>
</Card>
))
) : (
<div className="text-sm text-muted-foreground">No crime data available</div>
)}
</CollapsibleContent>
</Collapsible>
</TabsContent>
<TabsContent value="forecast" className="mt-4">
<div className="space-y-3">
{weatherData.forecast.map((item, index) => (
<Card key={index}>
<CardContent className="p-3 flex justify-between items-center">
<div className="flex items-center gap-2">
<Cloud className="h-5 w-5 text-blue-500" />
<span>{item.time}</span>
</div>
<div className="flex items-center gap-2">
<span>{item.condition}</span>
<span className="font-medium">{item.temperature}°</span>
</div>
</CardContent>
</Card>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="filters" className="mt-0 space-y-4">
<Card>
<CardHeader>
<CardTitle>Filter Options</CardTitle>
<CardDescription>Customize what you see on the map</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h3 className="text-sm font-medium">Crime Types</h3>
<div className="grid grid-cols-2 gap-2">
<Button variant="outline" size="sm" className="justify-start">
<input type="checkbox" className="mr-2" />
Theft
</Button>
<Button variant="outline" size="sm" className="justify-start">
<input type="checkbox" className="mr-2" />
Violence
</Button>
<Button variant="outline" size="sm" className="justify-start">
<input type="checkbox" className="mr-2" />
Vandalism
</Button>
<Button variant="outline" size="sm" className="justify-start">
<input type="checkbox" className="mr-2" />
Traffic
</Button>
</div>
</div>
<Separator />
<div className="space-y-2">
<h3 className="text-sm font-medium">Severity Levels</h3>
<div className="grid grid-cols-2 gap-2">
<Button variant="outline" size="sm" className="justify-start">
<input type="checkbox" className="mr-2" />
Low
</Button>
<Button variant="outline" size="sm" className="justify-start">
<input type="checkbox" className="mr-2" />
Medium
</Button>
<Button variant="outline" size="sm" className="justify-start">
<input type="checkbox" className="mr-2" />
High
</Button>
<Button variant="outline" size="sm" className="justify-start">
<input type="checkbox" className="mr-2" />
Critical
</Button>
</div>
</div>
<Separator />
<div className="space-y-2">
<h3 className="text-sm font-medium">Display Options</h3>
<div className="grid grid-cols-1 gap-2">
<Button variant="outline" size="sm" className="justify-start">
<input type="checkbox" className="mr-2" />
Show District Labels
</Button>
<Button variant="outline" size="sm" className="justify-start">
<input type="checkbox" className="mr-2" />
Show Incident Markers
</Button>
<Button variant="outline" size="sm" className="justify-start">
<input type="checkbox" className="mr-2" />
Show Heatmap
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="stats" className="mt-0 space-y-4">
<Card>
<CardHeader>
<CardTitle>Crime Statistics</CardTitle>
<CardDescription>Analysis of crime data</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium mb-2">Crime by Type</h3>
<div className="h-40 bg-muted rounded-md flex items-center justify-center">
<span className="text-sm text-muted-foreground">Chart Placeholder</span>
</div>
</div>
<Separator />
<div>
<h3 className="text-sm font-medium mb-2">Crime by Time of Day</h3>
<div className="h-40 bg-muted rounded-md flex items-center justify-center">
<span className="text-sm text-muted-foreground">Chart Placeholder</span>
</div>
</div>
<Separator />
<div>
<h3 className="text-sm font-medium mb-2">Monthly Trend</h3>
<div className="h-40 bg-muted rounded-md flex items-center justify-center">
<span className="text-sm text-muted-foreground">Chart Placeholder</span>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="info" className="mt-0 space-y-4">
<Card>
<CardHeader>
<CardTitle>About This Map</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
This interactive crime map visualizes crime data across different districts. Use the controls to
explore different aspects of the data.
</p>
<h3 className="text-sm font-medium mb-2">Legend</h3>
<div className="space-y-2 mb-4">
<div className="flex items-center">
<div className="w-4 h-4 bg-green-500 rounded-sm mr-2"></div>
<span className="text-sm">Low Crime Rate</span>
</div>
<div className="flex items-center">
<div className="w-4 h-4 bg-yellow-500 rounded-sm mr-2"></div>
<span className="text-sm">Medium Crime Rate</span>
</div>
<div className="flex items-center">
<div className="w-4 h-4 bg-orange-500 rounded-sm mr-2"></div>
<span className="text-sm">High Crime Rate</span>
</div>
<div className="flex items-center">
<div className="w-4 h-4 bg-red-500 rounded-sm mr-2"></div>
<span className="text-sm">Critical Crime Rate</span>
</div>
</div>
<h3 className="text-sm font-medium mb-2">Data Sources</h3>
<p className="text-sm text-muted-foreground mb-4">
Crime data is collected from official police reports and updated monthly. District boundaries are
based on administrative regions.
</p>
<h3 className="text-sm font-medium mb-2">Help & Support</h3>
<p className="text-sm text-muted-foreground">
For questions or support regarding this map, please contact the system administrator.
</p>
</CardContent>
</Card>
</TabsContent>
</ScrollArea>
</Tabs>
</div>
</div>

View File

@ -1,29 +1,39 @@
"use client"
import { Button } from "@/app/_components/ui/button"
import { Menu } from "lucide-react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { Button } from "../../ui/button"
import { cn } from "@/app/_lib/utils"
import { Overlay } from "../overlay"
import { ControlPosition } from "mapbox-gl"
interface SidebarToggleProps {
isOpen: boolean
onToggle: () => void
position?: ControlPosition
position?: "left" | "right"
className?: string
}
export default function SidebarToggle({ isOpen, onToggle, position = "left" }: SidebarToggleProps) {
if (isOpen) return null
export default function SidebarToggle({ isOpen, onToggle, position = "left", className }: SidebarToggleProps) {
return (
<Overlay position={position}>
<Button
variant="secondary"
size="icon"
className="absolute top-4 left-4 z-20 bg-white shadow-md hover:bg-gray-100"
onClick={onToggle}
className={cn(
"absolute z-10 shadow-md h-8 w-8 bg-background border"
)}
>
<Menu className="h-5 w-5" />
<span className="sr-only">Open sidebar</span>
{isOpen ? (
position === "left" ? (
<ChevronLeft className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)
) : position === "left" ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
<span className="sr-only">{isOpen ? "Close sidebar" : "Open sidebar"}</span>
</Button>
</Overlay>
)

View File

@ -0,0 +1,71 @@
"use client"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
import { useEffect, useRef, useState } from "react"
// Month options
const months = [
{ value: "1", label: "January" },
{ value: "2", label: "February" },
{ value: "3", label: "March" },
{ value: "4", label: "April" },
{ value: "5", label: "May" },
{ value: "6", label: "June" },
{ value: "7", label: "July" },
{ value: "8", label: "August" },
{ value: "9", label: "September" },
{ value: "10", label: "October" },
{ value: "11", label: "November" },
{ value: "12", label: "December" },
]
interface MonthSelectorProps {
selectedMonth: number | "all"
onMonthChange: (month: number | "all") => void
className?: string
includeAllOption?: boolean
}
export default function MonthSelector({
selectedMonth,
onMonthChange,
className = "w-[120px]",
includeAllOption = true
}: MonthSelectorProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [isClient, setIsClient] = useState(false)
useEffect(() => {
// This will ensure that the document is only used in the client-side context
setIsClient(true)
})
const container = isClient ? document.getElementById("root") : null
return (
<div ref={containerRef} className="mapboxgl-month-selector">
<Select
value={selectedMonth.toString()}
onValueChange={(value) => onMonthChange(value === "all" ? "all" : Number(value))}
>
<SelectTrigger className={className}>
<SelectValue placeholder="Month" />
</SelectTrigger>
<SelectContent
container={containerRef.current || container || undefined}
style={{ zIndex: 2000 }}
>
{includeAllOption && <SelectItem value="all">All Months</SelectItem>}
{months.map((month) => (
<SelectItem key={month.value} value={month.value}>
{month.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
// Export months constant so it can be reused elsewhere
export { months }

View File

@ -1,29 +1,122 @@
"use client"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
import { createRoot } from "react-dom/client"
import { useRef, useEffect, useState } from "react"
interface YearSelectorProps {
years: string[]
selectedYear: string
onChange: (year: string) => void
availableYears?: (number | null)[]
selectedYear: number
onYearChange: (year: number) => void
isLoading?: boolean
className?: string
}
export default function YearSelector({ years, selectedYear, onChange }: YearSelectorProps) {
interface YearSelectorProps {
availableYears?: (number | null)[];
selectedYear: number;
onYearChange: (year: number) => void;
isLoading?: boolean;
className?: string;
}
// React component for the year selector UI
function YearSelectorUI({
availableYears = [],
selectedYear,
onYearChange,
isLoading = false,
className = "w-[120px]"
}: YearSelectorProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [isClient, setIsClient] = useState(false);
useEffect(() => {
// This will ensure that the document is only used in the client-side context
setIsClient(true);
}, []);
// Conditionally access the document only when running on the client
const container = isClient ? document.getElementById("root") : null;
return (
<div className="flex items-center space-x-2">
<span className="text-sm font-medium">Year:</span>
<Select value={selectedYear} onValueChange={onChange}>
<SelectTrigger className="h-8 w-[100px]">
<SelectValue placeholder="Select Year" />
<div ref={containerRef} className="mapboxgl-year-selector">
{isLoading ? (
<div className={`${className} h-9 rounded-md bg-gradient-to-r from-gray-200 via-gray-100 to-gray-200 animate-pulse`} />
) : (
<Select
value={selectedYear.toString()}
onValueChange={(value) => onYearChange(Number(value))}
disabled={isLoading}
>
<SelectTrigger className={className}>
<SelectValue placeholder="Year" />
</SelectTrigger>
<SelectContent>
{years.map((year) => (
<SelectItem key={year} value={year}>
{/* Ensure that the dropdown content renders correctly only on the client side */}
<SelectContent
container={containerRef.current || container || undefined}
style={{ zIndex: 2000 }}
>
{availableYears
?.filter((year) => year !== null)
.map((year) => (
<SelectItem key={year} value={year!.toString()}>
{year}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
)
);
}
// Mapbox GL control class implementation
export class YearSelectorControl {
private _map: any;
private _container!: HTMLElement;
private _root: any;
private props: YearSelectorProps;
constructor(props: YearSelectorProps) {
this.props = props;
}
onAdd(map: any) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group';
this._container.style.padding = '5px';
// Set position to relative to keep dropdown content in context
this._container.style.position = 'relative';
// Higher z-index to ensure dropdown appears above map elements
this._container.style.zIndex = '50';
// Create React root for rendering our component
this._root = createRoot(this._container);
this._root.render(<YearSelectorUI {...this.props} />);
return this._container;
}
onRemove() {
if (this._container && this._container.parentNode) {
this._container.parentNode.removeChild(this._container);
}
// Unmount React component properly
if (this._root) {
this._root.unmount();
}
this._map = undefined;
}
}
// Export original React component as default for backward compatibility
export default function YearSelector(props: YearSelectorProps) {
// This wrapper allows the component to be used both as a React component
// and to help create a MapboxGL control
return <YearSelectorUI {...props} />;
}

View File

@ -4,31 +4,20 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/app/_components/ui/c
import { Skeleton } from "@/app/_components/ui/skeleton"
import DistrictLayer, { type DistrictFeature } from "./layers/district-layer"
import MapView from "./map"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
import { Button } from "@/app/_components/ui/button"
import { AlertCircle, FilterX } from "lucide-react"
import { getMonthName } from "@/app/_utils/common"
import { useCrimeMapHandler } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_handlers/crime-map-handlers"
import { useState } from "react"
import { useRef, useState } from "react"
import { CrimePopup } from "./pop-up"
import CrimeMarker, { type CrimeIncident } from "./markers/crime-marker"
const months = [
{ value: "1", label: "January" },
{ value: "2", label: "February" },
{ value: "3", label: "March" },
{ value: "4", label: "April" },
{ value: "5", label: "May" },
{ value: "6", label: "June" },
{ value: "7", label: "July" },
{ value: "8", label: "August" },
{ value: "9", label: "September" },
{ value: "10", label: "October" },
{ value: "11", label: "November" },
{ value: "12", label: "December" },
]
import type { CrimeIncident } from "./markers/crime-marker"
import YearSelector from "./controls/year-selector"
import MonthSelector from "./controls/month-selector"
import { useFullscreen } from "@/app/_hooks/use-fullscreen"
import { Overlay } from "./overlay"
import { useGetAvailableYears, useGetCrimeByYearAndMonth } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries"
import MapLegend from "./controls/map-legend"
export default function CrimeMap() {
// Set default year to 2024 instead of "all"
@ -38,8 +27,14 @@ export default function CrimeMap() {
const [selectedIncident, setSelectedIncident] = useState<CrimeIncident | null>(null)
const [showLegend, setShowLegend] = useState<boolean>(true)
const { availableYears, yearsLoading, yearsError, crimes, crimesLoading, crimesError, refetchCrimes } =
useCrimeMapHandler(selectedYear, selectedMonth)
const mapContainerRef = useRef<HTMLDivElement>(null)
// Use the custom fullscreen hook
const { isFullscreen } = useFullscreen(mapContainerRef)
const { data: availableYears, isLoading: isYearsLoading, error: yearsError } = useGetAvailableYears()
const { data: crimes, isLoading: isCrimesLoading, error: isCrimesError, refetch: refetchCrimes } = useGetCrimeByYearAndMonth(selectedYear, selectedMonth)
// Extract all incidents from all districts for marker display
const allIncidents =
@ -93,79 +88,40 @@ export default function CrimeMap() {
<CardHeader className="flex flex-row pb-2 pt-0 px-0 items-center justify-between">
<CardTitle>Crime Map {getMapTitle()}</CardTitle>
<div className="flex items-center gap-2">
{/* Regular (non-fullscreen) controls */}
<Select value={selectedYear.toString()} onValueChange={(value) => setSelectedYear(Number(value))}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="Year" />
</SelectTrigger>
<SelectContent>
{!yearsLoading &&
availableYears
?.filter((year) => year !== null)
.map((year) => (
<SelectItem key={year} value={year!.toString()}>
{year}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Year selector component */}
<YearSelector
availableYears={availableYears}
selectedYear={selectedYear}
onYearChange={setSelectedYear}
isLoading={isYearsLoading}
/>
<Select
value={selectedMonth.toString()}
onValueChange={(value) => setSelectedMonth(value === "all" ? "all" : Number(value))}
>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="Month" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Months</SelectItem>
{months.map((month) => (
<SelectItem key={month.value} value={month.value}>
{month.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Month selector component */}
<MonthSelector selectedMonth={selectedMonth} onMonthChange={setSelectedMonth} />
<Button variant="outline" onClick={applyFilters}>
Apply
</Button>
<Button variant="ghost" onClick={resetFilters} disabled={selectedYear === 2024 && selectedMonth === "all"}>
<FilterX className="h-4 w-4" />
Reset
</Button>
{/* <Button variant="outline" onClick={() => setShowLegend(!showLegend)}>
{showLegend ? "Hide Legend" : "Show Legend"}
</Button> */}
</div>
</CardHeader>
<CardContent className="p-0">
{crimesLoading ? (
{isCrimesLoading ? (
<div className="flex items-center justify-center h-96">
<Skeleton className="h-full w-full rounded-md" />
</div>
) : crimesError ? (
) : isCrimesError ? (
<div className="flex flex-col items-center justify-center h-96 gap-4">
<AlertCircle className="h-10 w-10 text-destructive" />
<p className="text-center">Failed to load crime data. Please try again later.</p>
<Button onClick={() => refetchCrimes()}>Retry</Button>
</div>
) : (
<div className="relative h-[600px]">
<div className="relative h-[600px]" ref={mapContainerRef}>
<MapView
mapStyle="mapbox://styles/mapbox/dark-v11"
className="h-[600px] w-full rounded-md"
crimes={crimes}
selectedYear={selectedYear}
selectedMonth={selectedMonth}
availableYears={availableYears}
yearsLoading={yearsLoading}
onYearChange={setSelectedYear}
onMonthChange={setSelectedMonth}
onApplyFilters={applyFilters}
onResetFilters={resetFilters}
>
{/* District Layer with crime data */}
<DistrictLayer
onClick={handleDistrictClick}
@ -183,6 +139,42 @@ export default function CrimeMap() {
crime={selectedIncident}
/>
)}
{isFullscreen && (
<>
<Overlay
position="top-left"
className="m-2 bg-transparent shadow-none p-0 border-none"
>
<div className="flex items-center gap-2 rounded-md p-0 shadow-lg">
<YearSelector
availableYears={availableYears}
selectedYear={selectedYear}
onYearChange={setSelectedYear}
isLoading={isYearsLoading}
className=" gap-2 text-white"
/>
<MonthSelector
selectedMonth={selectedMonth}
onMonthChange={setSelectedMonth}
className=" gap-2 hover:bg-red text-white"
/>
<Button
variant="secondary"
className=" hover:bg-red text-white"
onClick={resetFilters}
disabled={selectedYear === 2024 && selectedMonth === "all"}
size="sm"
>
<FilterX className="h-4 w-4 mr-1" />
Reset
</Button>
</div>
</Overlay>
<MapLegend position="bottom-right" />
</>
)}
</MapView>
</div>
)}

View File

@ -1,28 +1,16 @@
"use client"
import type React from "react"
import { useState, useCallback, useRef } from "react"
import {
type ViewState,
NavigationControl,
type MapRef,
FullscreenControl,
GeolocateControl,
Map,
} from "react-map-gl/mapbox"
import { useState, useCallback, useRef, useEffect } from "react"
import { type ViewState, Map, type MapRef, NavigationControl } from "react-map-gl/mapbox"
import { FullscreenControl } from "react-map-gl/mapbox"
import { BASE_LATITUDE, BASE_LONGITUDE, BASE_ZOOM, MAPBOX_STYLES, type MapboxStyle } from "@/app/_utils/const/map"
import "mapbox-gl/dist/mapbox-gl.css"
import MapSidebar from "./controls/map-sidebar"
import SidebarToggle from "./controls/map-toggle"
import TimeControls from "./controls/time-control"
import SeverityIndicator from "./controls/severity-indicator"
import MapFilterControl from "./controls/map-filter-control"
import { createRoot } from "react-dom/client"
import { YearSelectorControl } from "./controls/year-selector"
import { useFullscreen } from "@/app/_hooks/use-fullscreen"
import MapControls from "./controls/map-control"
import { Overlay } from "./overlay"
import MapLegend from "./controls/map-legend"
import { CustomControl } from "./controls/example"
import { toast } from "sonner"
interface MapViewProps {
children?: React.ReactNode
@ -34,22 +22,6 @@ interface MapViewProps {
mapboxApiAccessToken?: string
onMoveEnd?: (viewState: ViewState) => void
customControls?: React.ReactNode
crimes?: Array<{
id: string
district_name: string
district_id?: string
number_of_crime?: number
level?: "low" | "medium" | "high" | "critical"
incidents: any[]
}>
selectedYear?: number | string
selectedMonth?: number | string
availableYears?: (number | null)[]
yearsLoading?: boolean
onYearChange?: (year: number) => void
onMonthChange?: (month: number | "all") => void
onApplyFilters?: () => void
onResetFilters?: () => void
}
export default function MapView({
@ -61,24 +33,9 @@ export default function MapView({
height = "100%",
mapboxApiAccessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN,
onMoveEnd,
customControls,
crimes = [],
selectedYear,
selectedMonth,
availableYears = [],
yearsLoading = false,
onYearChange = () => { },
onMonthChange = () => { },
onApplyFilters = () => { },
onResetFilters = () => { },
}: MapViewProps) {
const [mapRef, setMapRef] = useState<MapRef | null>(null)
const [activeControl, setActiveControl] = useState<string>("crime-rate")
const [activeTime, setActiveTime] = useState<string>("today")
const [sidebarOpen, setSidebarOpen] = useState<boolean>(false)
const mapContainerRef = useRef<HTMLDivElement>(null)
// Use the custom fullscreen hook instead of manual event listeners
const { isFullscreen } = useFullscreen(mapContainerRef)
const defaultViewState: Partial<ViewState> = {
@ -90,88 +47,33 @@ export default function MapView({
...initialViewState,
}
const handleMapLoad = useCallback((event: any) => {
setMapRef(event.target)
}, [])
const handleMoveEnd = useCallback(
(event: any) => {
if (onMoveEnd) {
onMoveEnd(event.viewState)
}
},
[onMoveEnd],
[onMoveEnd]
)
const handleControlChange = (control: string) => {
setActiveControl(control)
}
const handleTimeChange = (time: string) => {
setActiveTime(time)
}
const toggleSidebar = () => {
setSidebarOpen(!sidebarOpen)
}
return (
<div ref={mapContainerRef} className={`relative ${className}`}>
{/* Main content with left padding when sidebar is open */}
<div className={`relative h-full transition-all duration-300 ${isFullscreen && sidebarOpen ? "ml-80" : "ml-0"}`}>
<div className="flex h-full">
<div className="relative flex-grow h-full transition-all duration-300">
<Map
ref={(ref) => setMapRef(ref)}
mapStyle={mapStyle}
mapboxAccessToken={mapboxApiAccessToken}
initialViewState={defaultViewState}
onLoad={handleMapLoad}
onMoveEnd={handleMoveEnd}
interactiveLayerIds={["district-fill", "clusters", "unclustered-point"]}
attributionControl={false}
style={{ width: "100%", height: "100%" }}
attributionControl={false}
>
{children}
<FullscreenControl position="top-right" />
<NavigationControl position="top-right" />
{/* Sidebar and other controls only in fullscreen */}
{isFullscreen && (
<>
{/* <MapFilterControl
selectedYear={Number(selectedYear) || 2024}
selectedMonth={selectedMonth === "all" ? "all" : Number(selectedMonth) || "all"}
availableYears={availableYears}
yearsLoading={yearsLoading}
onYearChange={onYearChange}
onMonthChange={onMonthChange}
onApplyFilters={onApplyFilters}
onResetFilters={onResetFilters}
/> */}
<MapSidebar
isOpen={sidebarOpen}
onToggle={toggleSidebar}
crimes={crimes}
selectedYear={selectedYear}
selectedMonth={selectedMonth}
/>
{/* Map Legend - positioned at bottom-right */}
<MapLegend position="bottom-right" />
{/* Sidebar toggle - positioned on the left */}
<SidebarToggle isOpen={sidebarOpen} onToggle={toggleSidebar} position="left" />
{/* Map Control - positioned at top-left */}
<MapControls onControlChange={handleControlChange} activeControl={activeControl} position="top-left" />
{/* Time/year selector - positioned at bottom-left */}
<TimeControls onTimeChange={handleTimeChange} activeTime={activeTime} position="bottom-left" />
</>
)}
<NavigationControl position="top-right" showCompass={false} />
{children}
</Map>
</div>
</div>
</div>
)
}

View File

@ -5,32 +5,72 @@ import { createPortal } from "react-dom";
import { useControl } from "react-map-gl/mapbox";
import { v4 as uuidv4 } from 'uuid';
// Updated props type to include addControl in children props
type OverlayProps = {
position: ControlPosition;
children: ReactElement<{ map?: Map }>;
children: ReactElement<{
map?: Map;
addControl?: (control: IControl, position?: ControlPosition) => void;
}>;
id?: string;
className?: string;
style?: React.CSSProperties;
};
// Definisikan custom control untuk overlay
// Custom control for overlay
class OverlayControl implements IControl {
_map: Map | null = null;
_container: HTMLElement | null = null;
_position: ControlPosition;
_id: string;
_redraw?: () => void;
_className?: string;
_style?: React.CSSProperties;
constructor({ position, id, redraw }: { position: ControlPosition; id: string; redraw?: () => void }) {
constructor({
position,
id,
redraw,
className,
style,
}: {
position: ControlPosition;
id: string;
redraw?: () => void;
className?: string;
style?: React.CSSProperties;
}) {
this._position = position;
this._id = id;
this._redraw = redraw;
this._className = className;
this._style = style;
}
onAdd(map: Map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group';
// Apply base classes but keep it minimal to avoid layout conflicts
this._container.className = `mapboxgl-ctrl ${this._className || ''}`;
this._container.id = this._id;
// Important: These styles make the overlay adapt to content
this._container.style.pointerEvents = 'auto';
this._container.style.display = 'inline-block'; // Allow container to size to content
this._container.style.maxWidth = 'none'; // Remove any max-width constraints
this._container.style.width = 'auto'; // Let width be determined by content
this._container.style.height = 'auto'; // Let height be determined by content
this._container.style.overflow = 'visible'; // Allow content to overflow if needed
// Apply any custom styles passed as props
if (this._style) {
Object.entries(this._style).forEach(([key, value]) => {
// @ts-ignore - dynamically setting style properties
this._container.style[key] = value;
});
}
if (this._redraw) {
map.on('move', this._redraw);
this._redraw();
@ -61,20 +101,34 @@ class OverlayControl implements IControl {
getElement() {
return this._container;
}
// Method to add other controls to the map
addControl(control: IControl, position?: ControlPosition) {
if (this._map) {
this._map.addControl(control, position);
}
return this;
}
}
// Komponen Overlay yang telah ditingkatkan
function _Overlay({ position, children, id = `overlay-${uuidv4()}` }: OverlayProps) {
// Enhanced Overlay component
function _Overlay({ position, children, id = `overlay-${uuidv4()}`, className, style }: OverlayProps) {
const [container, setContainer] = useState<HTMLElement | null>(null);
const [map, setMap] = useState<Map | null>(null)
const [map, setMap] = useState<Map | null>(null);
// Gunakan useControl dengan ID unik untuk menghindari konflik
// Use useControl with unique ID to avoid conflicts
const ctrl = useControl<OverlayControl>(
() => new OverlayControl({ position, id }),
{ position } // Hanya menggunakan position yang valid dalam ControlOptions
() =>
new OverlayControl({
position,
id,
className,
style,
}),
{ position }
);
// Update container dan map instance ketika control siap
// Update container and map instance when control is ready
useEffect(() => {
if (ctrl) {
setContainer(ctrl.getElement());
@ -82,15 +136,20 @@ function _Overlay({ position, children, id = `overlay-${uuidv4()}` }: OverlayPro
}
}, [ctrl]);
// Hanya render jika container sudah siap
// Only render if container is ready
if (!container || !map) return null;
// Gunakan createPortal untuk merender children ke container
// Use createPortal to render children to container and pass addControl method
// return createPortal(
// cloneElement(children, { map, addControl: ctrl.addControl.bind(ctrl) }),
// container
// );
return createPortal(
cloneElement(children, { map }),
container
);
)
}
// Export sebagai komponen memoized
// Export as memoized component
export const Overlay = memo(_Overlay);

View File

@ -67,11 +67,15 @@ const SelectScrollDownButton = React.forwardRef<
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
interface SelectContentProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> {
container?: HTMLElement
}
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
SelectContentProps
>(({ className, children, position = "popper", container, ...props }, ref) => (
<SelectPrimitive.Portal container={container}>
<SelectPrimitive.Content
ref={ref}
className={cn(

View File

@ -2,11 +2,6 @@
import { useState, useEffect, RefObject } from 'react';
/**
* Hook for detecting fullscreen state changes
* @param containerRef Reference to the container element
* @returns Object containing the fullscreen state and functions to control it
*/
export function useFullscreen(containerRef: RefObject<HTMLElement | null>) {
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
@ -21,13 +16,11 @@ export function useFullscreen(containerRef: RefObject<HTMLElement | null>) {
setIsFullscreen(!!fullscreenElement);
};
// Add event listeners for fullscreen changes across browsers
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
document.addEventListener('MSFullscreenChange', handleFullscreenChange);
// Cleanup function to remove event listeners
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
document.removeEventListener(
@ -45,9 +38,6 @@ export function useFullscreen(containerRef: RefObject<HTMLElement | null>) {
};
}, []);
/**
* Requests fullscreen for the container element
*/
const enterFullscreen = () => {
if (!containerRef.current) return;
@ -64,9 +54,6 @@ export function useFullscreen(containerRef: RefObject<HTMLElement | null>) {
}
};
/**
* Exits fullscreen mode
*/
const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen();
@ -79,9 +66,6 @@ export function useFullscreen(containerRef: RefObject<HTMLElement | null>) {
}
};
/**
* Toggles fullscreen mode
*/
const toggleFullscreen = () => {
if (isFullscreen) {
exitFullscreen();

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,6 @@
"dependencies": {
"@evyweb/ioctopus": "^1.2.0",
"@hookform/resolvers": "^4.1.2",
"@mapbox/mapbox-gl-draw": "^1.5.0",
"@prisma/client": "^6.4.1",
"@prisma/instrumentation": "^6.5.0",
"@radix-ui/react-alert-dialog": "^1.1.6",
@ -1645,24 +1644,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mapbox/geojson-area": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz",
"integrity": "sha512-bBqqFn1kIbLBfn7Yq1PzzwVkPYQr9lVUeT8Dhd0NL5n76PBuXzOcuLV7GOSbEB1ia8qWxH4COCvFpziEu/yReA==",
"license": "BSD-2-Clause",
"dependencies": {
"wgs84": "0.0.0"
}
},
"node_modules/@mapbox/geojson-normalize": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@mapbox/geojson-normalize/-/geojson-normalize-0.0.1.tgz",
"integrity": "sha512-82V7YHcle8lhgIGqEWwtXYN5cy0QM/OHq3ypGhQTbvHR57DF0vMHMjjVSQKFfVXBe/yWCBZTyOuzvK7DFFnx5Q==",
"license": "ISC",
"bin": {
"geojson-normalize": "geojson-normalize"
}
},
"node_modules/@mapbox/jsonlint-lines-primitives": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
@ -1671,52 +1652,6 @@
"node": ">= 0.6"
}
},
"node_modules/@mapbox/mapbox-gl-draw": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-draw/-/mapbox-gl-draw-1.5.0.tgz",
"integrity": "sha512-uchQbTa8wiv6GWWTbxW1g5b8H6VySz4t91SmduNH6jjWinPze7cjcmsPUEzhySXsYpYr2/50gRJLZz3bx7O88A==",
"license": "ISC",
"dependencies": {
"@mapbox/geojson-area": "^0.2.2",
"@mapbox/geojson-normalize": "^0.0.1",
"@mapbox/point-geometry": "^1.1.0",
"fast-deep-equal": "^3.1.3",
"nanoid": "^5.0.9"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/@mapbox/mapbox-gl-draw/node_modules/@mapbox/point-geometry": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
"license": "ISC"
},
"node_modules/@mapbox/mapbox-gl-draw/node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/@mapbox/mapbox-gl-draw/node_modules/nanoid": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
"integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/@mapbox/mapbox-gl-supported": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz",
@ -14080,12 +14015,6 @@
"node": ">=4.0"
}
},
"node_modules/wgs84": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/wgs84/-/wgs84-0.0.0.tgz",
"integrity": "sha512-ANHlY4Rb5kHw40D0NJ6moaVfOCMrp9Gpd1R/AIQYg2ko4/jzcJ+TVXYYF6kXJqQwITvEZP4yEthjM7U6rYlljQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",

View File

@ -13,7 +13,6 @@
"dependencies": {
"@evyweb/ioctopus": "^1.2.0",
"@hookform/resolvers": "^4.1.2",
"@mapbox/mapbox-gl-draw": "^1.5.0",
"@prisma/client": "^6.4.1",
"@prisma/instrumentation": "^6.5.0",
"@radix-ui/react-alert-dialog": "^1.1.6",