feat: add overlay component and integrate into map controls for improved UI layout

This commit is contained in:
vergiLgood1 2025-04-30 18:39:21 +07:00
parent 2c09cb4d7c
commit 31f12ab88b
12 changed files with 264 additions and 172 deletions

View File

@ -14,13 +14,16 @@ import {
Users, Users,
Siren, Siren,
} from "lucide-react" } from "lucide-react"
import { Overlay } from "../overlay"
import { ControlPosition } from "mapbox-gl"
interface MapControlsProps { interface MapControlsProps {
onControlChange: (control: string) => void onControlChange: (control: string) => void
activeControl: string activeControl: string
position?: ControlPosition
} }
export default function MapControls({ onControlChange, activeControl }: MapControlsProps) { export default function MapControls({ onControlChange, activeControl, position = "top-left" }: MapControlsProps) {
const controls = [ const controls = [
{ id: "crime-rate", icon: <Thermometer size={20} />, label: "Crime Rate" }, { id: "crime-rate", icon: <Thermometer size={20} />, label: "Crime Rate" },
{ id: "theft", icon: <Droplets size={20} />, label: "Theft" }, { id: "theft", icon: <Droplets size={20} />, label: "Theft" },
@ -36,6 +39,7 @@ export default function MapControls({ onControlChange, activeControl }: MapContr
] ]
return ( 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"> <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> <TooltipProvider>
{controls.map((control) => ( {controls.map((control) => (
@ -61,5 +65,6 @@ export default function MapControls({ onControlChange, activeControl }: MapContr
))} ))}
</TooltipProvider> </TooltipProvider>
</div> </div>
</Overlay>
) )
} }

View File

@ -39,15 +39,21 @@ export default function MapFilterControl({
onYearChange, onYearChange,
onMonthChange, onMonthChange,
onApplyFilters, onApplyFilters,
onResetFilters onResetFilters,
}: MapFilterControlProps) { }: MapFilterControlProps) {
const handleYearChange = useCallback((value: string) => { const handleYearChange = useCallback(
onYearChange(Number(value)) (value: string) => {
}, [onYearChange]) onYearChange(Number(value))
},
[onYearChange],
)
const handleMonthChange = useCallback((value: string) => { const handleMonthChange = useCallback(
onMonthChange(value === "all" ? "all" : Number(value)) (value: string) => {
}, [onMonthChange]) onMonthChange(value === "all" ? "all" : Number(value))
},
[onMonthChange],
)
const isDefaultFilter = selectedYear === 2024 && selectedMonth === "all" const isDefaultFilter = selectedYear === 2024 && selectedMonth === "all"
@ -72,10 +78,7 @@ export default function MapFilterControl({
</SelectContent> </SelectContent>
</Select> </Select>
<Select <Select value={selectedMonth.toString()} onValueChange={handleMonthChange}>
value={selectedMonth.toString()}
onValueChange={handleMonthChange}
>
<SelectTrigger className="h-8 w-full"> <SelectTrigger className="h-8 w-full">
<SelectValue placeholder="Month" /> <SelectValue placeholder="Month" />
</SelectTrigger> </SelectTrigger>
@ -93,12 +96,7 @@ export default function MapFilterControl({
<Button className="h-8 text-xs flex-1" variant="default" onClick={onApplyFilters}> <Button className="h-8 text-xs flex-1" variant="default" onClick={onApplyFilters}>
Apply Apply
</Button> </Button>
<Button <Button className="h-8 text-xs" variant="ghost" onClick={onResetFilters} disabled={isDefaultFilter}>
className="h-8 text-xs"
variant="ghost"
onClick={onResetFilters}
disabled={isDefaultFilter}
>
<FilterX className="h-3 w-3 mr-1" /> <FilterX className="h-3 w-3 mr-1" />
Reset Reset
</Button> </Button>

View File

@ -1,109 +1,25 @@
import { CRIME_RATE_COLORS } from "@/app/_utils/const/map" import { CRIME_RATE_COLORS } from "@/app/_utils/const/map";
import { ControlPosition, IControl, Map } from "mapbox-gl" import { Overlay } from "../overlay";
import { useControl } from "react-map-gl/mapbox" import { ControlPosition } from "mapbox-gl";
import React, { useEffect } from "react"
import { createRoot } from "react-dom/client"
interface MapLegendProps { interface MapLegendProps {
position?: ControlPosition position?: ControlPosition
isFullscreen?: boolean
} }
// React component for legend content export default function MapLegend({ position = "bottom-right" }: MapLegendProps) {
const LegendContent = () => {
return ( return (
<div className="flex flex-row text-xs font-semibold"> <Overlay position={position}>
<div className={`flex items-center gap-1.5 py-0 px-8 rounded-l-md border-y border-1 `} style={{ backgroundColor: `${CRIME_RATE_COLORS.low}90` }}> <div className="flex flex-row text-xs font-semibold font-sans text-white">
<span>Low</span> <div className={`flex items-center gap-1.5 py-0 px-8 rounded-l-md border-y border-1 `} style={{ backgroundColor: `${CRIME_RATE_COLORS.low}` }}>
<span>Low</span>
</div>
<div className={`flex items-center gap-1.5 py-0 px-8 border-y border-1 `} style={{ backgroundColor: `${CRIME_RATE_COLORS.medium}` }}>
<span>Medium</span>
</div>
<div className={`flex items-center gap-1.5 py-0 px-8 rounded-r-md border-y border-1 `} style={{ backgroundColor: `${CRIME_RATE_COLORS.high}` }}>
<span>High</span>
</div>
</div> </div>
<div className={`flex items-center gap-1.5 py-0 px-8 border-y border-1 `} style={{ backgroundColor: `${CRIME_RATE_COLORS.medium}90` }}> </Overlay>
<span>Medium</span>
</div>
<div className={`flex items-center gap-1.5 py-0 px-8 rounded-r-md border-y border-1 `} style={{ backgroundColor: `${CRIME_RATE_COLORS.high}90` }}>
<span>High</span>
</div>
</div>
) )
}
class MapLegendControl implements IControl {
private container: HTMLElement;
private map?: Map;
private props: MapLegendProps;
private root: ReturnType<typeof createRoot> | null = null;
private isUnmounting: boolean = false;
constructor(props: MapLegendProps) {
this.props = props;
this.container = document.createElement("div");
this.container.className = "mapboxgl-ctrl";
}
onAdd(map: Map): HTMLElement {
this.map = map;
this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-legend';
this.render();
return this.container;
}
onRemove(): void {
// Prevent multiple unmounting attempts
if (this.isUnmounting) return;
this.isUnmounting = true;
// Schedule unmounting to happen after current render cycle completes
requestAnimationFrame(() => {
if (this.root) {
this.root.unmount();
this.root = null;
}
if (this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
this.map = undefined;
this.isUnmounting = false;
});
}
updateProps(props: MapLegendProps): void {
this.props = props;
this.render();
}
render(): void {
// Only render if in fullscreen or if isFullscreen prop is not provided
if (this.props.isFullscreen === false) {
if (this.container.style.display !== 'none') {
this.container.style.display = 'none';
}
return;
} else {
this.container.style.display = 'block';
}
// Create a root if it doesn't exist
if (!this.root) {
this.root = createRoot(this.container);
}
// Render using the createRoot API
this.root.render(<LegendContent />);
}
}
export function MapLegend({ position = 'bottom-right', isFullscreen }: MapLegendProps) {
const control = useControl<MapLegendControl>(
() => new MapLegendControl({ position, isFullscreen }),
{ position }
);
// Update control when props change
useEffect(() => {
control.updateProps({ position, isFullscreen });
}, [control, position, isFullscreen]);
return null;
} }

View File

@ -2,16 +2,20 @@
import { Button } from "@/app/_components/ui/button" import { Button } from "@/app/_components/ui/button"
import { Menu } from "lucide-react" import { Menu } from "lucide-react"
import { Overlay } from "../overlay"
import { ControlPosition } from "mapbox-gl"
interface SidebarToggleProps { interface SidebarToggleProps {
isOpen: boolean isOpen: boolean
onToggle: () => void onToggle: () => void
position?: ControlPosition
} }
export default function SidebarToggle({ isOpen, onToggle }: SidebarToggleProps) { export default function SidebarToggle({ isOpen, onToggle, position = "left" }: SidebarToggleProps) {
if (isOpen) return null if (isOpen) return null
return ( return (
<Overlay position={position}>
<Button <Button
variant="secondary" variant="secondary"
size="icon" size="icon"
@ -21,5 +25,6 @@ export default function SidebarToggle({ isOpen, onToggle }: SidebarToggleProps)
<Menu className="h-5 w-5" /> <Menu className="h-5 w-5" />
<span className="sr-only">Open sidebar</span> <span className="sr-only">Open sidebar</span>
</Button> </Button>
</Overlay>
) )
} }

View File

@ -1,13 +1,17 @@
"use client" "use client"
import { Checkbox } from "@/app/_components/ui/checkbox" import { Checkbox } from "@/app/_components/ui/checkbox"
import { Label } from "@/app/_components/ui/label" import { Label } from "@/app/_components/ui/label"
import { Overlay } from "../overlay"
import { ControlPosition } from "mapbox-gl"
interface TimeControlsProps { interface TimeControlsProps {
onTimeChange: (time: string) => void onTimeChange: (time: string) => void
activeTime: string activeTime: string
position?: ControlPosition
} }
export default function TimeControls({ onTimeChange, activeTime }: TimeControlsProps) { export default function TimeControls({ onTimeChange, activeTime, position = "bottom-left" }: TimeControlsProps) {
const times = [ const times = [
{ id: "today", label: "Hari ini" }, { id: "today", label: "Hari ini" },
{ id: "yesterday", label: "Kemarin" }, { id: "yesterday", label: "Kemarin" },
@ -16,16 +20,18 @@ export default function TimeControls({ onTimeChange, activeTime }: TimeControlsP
] ]
return ( return (
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-10 bg-black/75 rounded-md p-2 flex items-center space-x-4"> <Overlay position={position}>
<div className="text-white font-medium mr-2">Waktu</div> <div className="bg-black/75 rounded-md p-2 flex items-center space-x-4">
{times.map((time) => ( <div className="text-white font-medium mr-2">Waktu</div>
<div key={time.id} className="flex items-center space-x-2"> {times.map((time) => (
<Checkbox id={time.id} checked={activeTime === time.id} onCheckedChange={() => onTimeChange(time.id)} /> <div key={time.id} className="flex items-center space-x-2">
<Label htmlFor={time.id} className="text-white text-sm cursor-pointer"> <Checkbox id={time.id} checked={activeTime === time.id} onCheckedChange={() => onTimeChange(time.id)} />
{time.label} <Label htmlFor={time.id} className="text-white text-sm cursor-pointer">
</Label> {time.label}
</div> </Label>
))} </div>
</div> ))}
</div>
</Overlay>
) )
} }

View File

@ -13,7 +13,7 @@ import { useState } from "react"
import { CrimePopup } from "./pop-up" import { CrimePopup } from "./pop-up"
import CrimeMarker, { type CrimeIncident } from "./markers/crime-marker" import CrimeMarker, { type CrimeIncident } from "./markers/crime-marker"
import { MapLegend } from "./controls/map-legend"
const months = [ const months = [
{ value: "1", label: "January" }, { value: "1", label: "January" },
@ -165,8 +165,6 @@ export default function CrimeMap() {
onApplyFilters={applyFilters} onApplyFilters={applyFilters}
onResetFilters={resetFilters} onResetFilters={resetFilters}
> >
{/* Show the legend regardless of fullscreen state if showLegend is true */}
{<MapLegend />}
{/* District Layer with crime data */} {/* District Layer with crime data */}
<DistrictLayer <DistrictLayer

View File

@ -59,7 +59,7 @@ export default function DistrictLayer({
// Use district_id (which corresponds to kode_kec in the tileset) as the key // Use district_id (which corresponds to kode_kec in the tileset) as the key
const districtId = crime.distrcit_id || crime.district_name const districtId = crime.distrcit_id || crime.district_name
console.log("Mapping district:", districtId, "level:", crime.level) // console.log("Mapping district:", districtId, "level:", crime.level)
acc[districtId] = { acc[districtId] = {
number_of_crime: crime.number_of_crime, number_of_crime: crime.number_of_crime,
@ -102,7 +102,7 @@ export default function DistrictLayer({
const districtId = feature.properties.kode_kec // Using kode_kec as the unique identifier const districtId = feature.properties.kode_kec // Using kode_kec as the unique identifier
const crimeData = crimeDataByDistrict[districtId] || {} const crimeData = crimeDataByDistrict[districtId] || {}
console.log("Hover district:", districtId, "found data:", crimeData) // console.log("Hover district:", districtId, "found data:", crimeData)
// Enhance feature with crime data // Enhance feature with crime data
feature.properties = { feature.properties = {

View File

@ -3,16 +3,15 @@
import type React from "react" import type React from "react"
import { useState, useCallback, useRef } from "react" import { useState, useCallback, useRef } from "react"
import ReactMapGL, { import {
type ViewState, type ViewState,
NavigationControl, NavigationControl,
ScaleControl,
type MapRef, type MapRef,
FullscreenControl, FullscreenControl,
GeolocateControl, GeolocateControl,
Map,
} from "react-map-gl/mapbox" } from "react-map-gl/mapbox"
import { BASE_LATITUDE, BASE_LONGITUDE, BASE_ZOOM, MAP_STYLE, MAPBOX_STYLES, MapboxStyle } from "@/app/_utils/const/map" import { BASE_LATITUDE, BASE_LONGITUDE, BASE_ZOOM, MAPBOX_STYLES, type MapboxStyle } from "@/app/_utils/const/map"
import { Search } from "lucide-react"
import "mapbox-gl/dist/mapbox-gl.css" import "mapbox-gl/dist/mapbox-gl.css"
import MapSidebar from "./controls/map-sidebar" import MapSidebar from "./controls/map-sidebar"
import SidebarToggle from "./controls/map-toggle" import SidebarToggle from "./controls/map-toggle"
@ -21,6 +20,9 @@ import SeverityIndicator from "./controls/severity-indicator"
import MapFilterControl from "./controls/map-filter-control" import MapFilterControl from "./controls/map-filter-control"
import { useFullscreen } from "@/app/_hooks/use-fullscreen" import { useFullscreen } from "@/app/_hooks/use-fullscreen"
import MapControls from "./controls/map-control" import MapControls from "./controls/map-control"
import { Overlay } from "./overlay"
import MapLegend from "./controls/map-legend"
interface MapViewProps { interface MapViewProps {
children?: React.ReactNode children?: React.ReactNode
@ -117,7 +119,7 @@ export default function MapView({
<div ref={mapContainerRef} className={`relative ${className}`}> <div ref={mapContainerRef} className={`relative ${className}`}>
{/* Main content with left padding when sidebar is open */} {/* 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={`relative h-full transition-all duration-300 ${isFullscreen && sidebarOpen ? "ml-80" : "ml-0"}`}>
<ReactMapGL <Map
ref={(ref) => setMapRef(ref)} ref={(ref) => setMapRef(ref)}
mapStyle={mapStyle} mapStyle={mapStyle}
mapboxAccessToken={mapboxApiAccessToken} mapboxAccessToken={mapboxApiAccessToken}
@ -132,26 +134,20 @@ export default function MapView({
<FullscreenControl position="top-right" /> <FullscreenControl position="top-right" />
<NavigationControl position="top-right" /> <NavigationControl position="top-right" />
{/* Custom controls that depend on fullscreen state */}
{isFullscreen && <GeolocateControl position="top-right" />}
{/* Custom Filter Control with isFullscreen prop */}
{/* {isFullscreen && <MapFilterControl
position="top-right"
selectedYear={Number(selectedYear) || 2024}
selectedMonth={selectedMonth === "all" ? "all" : Number(selectedMonth) || "all"}
availableYears={availableYears}
yearsLoading={yearsLoading}
onYearChange={onYearChange}
onMonthChange={onMonthChange}
onApplyFilters={onApplyFilters}
onResetFilters={onResetFilters}
isFullscreen={isFullscreen}
/>
} */}
{/* Sidebar and other controls only in fullscreen */} {/* Sidebar and other controls only in fullscreen */}
{isFullscreen && ( {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 <MapSidebar
isOpen={sidebarOpen} isOpen={sidebarOpen}
onToggle={toggleSidebar} onToggle={toggleSidebar}
@ -159,19 +155,23 @@ export default function MapView({
selectedYear={selectedYear} selectedYear={selectedYear}
selectedMonth={selectedMonth} selectedMonth={selectedMonth}
/> />
<SidebarToggle isOpen={sidebarOpen} onToggle={toggleSidebar} />
<MapControls onControlChange={handleControlChange} activeControl={activeControl} /> {/* Map Legend - positioned at bottom-right */}
<TimeControls onTimeChange={handleTimeChange} activeTime={activeTime} /> <MapLegend position="bottom-right" />
<SeverityIndicator />
{/* 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" />
</> </>
)} )}
</ReactMapGL> </Map>
</div> </div>
{/* Debug indicator - uncomment for debugging
<div className="absolute bottom-4 left-4 bg-black bg-opacity-70 text-white text-xs p-1 rounded z-50">
Fullscreen: {isFullscreen ? "Yes" : "No"}
</div> */}
</div> </div>
) )
} }

View File

@ -0,0 +1,96 @@
import { IControl, Map } from "mapbox-gl";
import { ControlPosition } from "mapbox-gl";
import { cloneElement, memo, ReactElement, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { useControl } from "react-map-gl/mapbox";
import { v4 as uuidv4 } from 'uuid';
type OverlayProps = {
position: ControlPosition;
children: ReactElement<{ map?: Map }>;
id?: string;
};
// Definisikan custom control untuk overlay
class OverlayControl implements IControl {
_map: Map | null = null;
_container: HTMLElement | null = null;
_position: ControlPosition;
_id: string;
_redraw?: () => void;
constructor({ position, id, redraw }: { position: ControlPosition; id: string; redraw?: () => void }) {
this._position = position;
this._id = id;
this._redraw = redraw;
}
onAdd(map: Map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group';
this._container.id = this._id;
if (this._redraw) {
map.on('move', this._redraw);
this._redraw();
}
return this._container;
}
onRemove() {
if (!this._map || !this._container) return;
if (this._redraw) {
this._map.off('move', this._redraw);
}
this._container.remove();
this._map = null;
}
getDefaultPosition() {
return this._position;
}
getMap() {
return this._map;
}
getElement() {
return this._container;
}
}
// Komponen Overlay yang telah ditingkatkan
function _Overlay({ position, children, id = `overlay-${uuidv4()}` }: OverlayProps) {
const [container, setContainer] = useState<HTMLElement | null>(null);
const [map, setMap] = useState<Map | null>(null)
// Gunakan useControl dengan ID unik untuk menghindari konflik
const ctrl = useControl<OverlayControl>(
() => new OverlayControl({ position, id }),
{ position } // Hanya menggunakan position yang valid dalam ControlOptions
);
// Update container dan map instance ketika control siap
useEffect(() => {
if (ctrl) {
setContainer(ctrl.getElement());
setMap(ctrl.getMap());
}
}, [ctrl]);
// Hanya render jika container sudah siap
if (!container || !map) return null;
// Gunakan createPortal untuk merender children ke container
return createPortal(
cloneElement(children, { map }),
container
);
}
// Export sebagai komponen memoized
export const Overlay = memo(_Overlay);

View File

@ -90,10 +90,6 @@ export function useFullscreen(containerRef: RefObject<HTMLElement | null>) {
} }
}; };
// Cek apakah hook use-fullscreen mengembalikan nilai isFullscreen dengan benar.
// Tambahkan console.log untuk debugging jika diperlukan.
console.log("Fullscreen state:", isFullscreen);
return { return {
isFullscreen, isFullscreen,
enterFullscreen, enterFullscreen,

View File

@ -7,6 +7,7 @@
"dependencies": { "dependencies": {
"@evyweb/ioctopus": "^1.2.0", "@evyweb/ioctopus": "^1.2.0",
"@hookform/resolvers": "^4.1.2", "@hookform/resolvers": "^4.1.2",
"@mapbox/mapbox-gl-draw": "^1.5.0",
"@prisma/client": "^6.4.1", "@prisma/client": "^6.4.1",
"@prisma/instrumentation": "^6.5.0", "@prisma/instrumentation": "^6.5.0",
"@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-alert-dialog": "^1.1.6",
@ -1644,6 +1645,24 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "node_modules/@mapbox/jsonlint-lines-primitives": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
@ -1652,6 +1671,52 @@
"node": ">= 0.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": { "node_modules/@mapbox/mapbox-gl-supported": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz",
@ -14015,6 +14080,12 @@
"node": ">=4.0" "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": { "node_modules/whatwg-url": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",

View File

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