feat: add overlay component and integrate into map controls for improved UI layout
This commit is contained in:
parent
2c09cb4d7c
commit
31f12ab88b
|
@ -14,13 +14,16 @@ import {
|
|||
Users,
|
||||
Siren,
|
||||
} from "lucide-react"
|
||||
import { Overlay } from "../overlay"
|
||||
import { ControlPosition } from "mapbox-gl"
|
||||
|
||||
interface MapControlsProps {
|
||||
onControlChange: (control: string) => void
|
||||
activeControl: string
|
||||
position?: ControlPosition
|
||||
}
|
||||
|
||||
export default function MapControls({ onControlChange, activeControl }: MapControlsProps) {
|
||||
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" },
|
||||
|
@ -36,6 +39,7 @@ export default function MapControls({ onControlChange, activeControl }: MapContr
|
|||
]
|
||||
|
||||
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) => (
|
||||
|
@ -61,5 +65,6 @@ export default function MapControls({ onControlChange, activeControl }: MapContr
|
|||
))}
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</Overlay>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -39,15 +39,21 @@ export default function MapFilterControl({
|
|||
onYearChange,
|
||||
onMonthChange,
|
||||
onApplyFilters,
|
||||
onResetFilters
|
||||
onResetFilters,
|
||||
}: MapFilterControlProps) {
|
||||
const handleYearChange = useCallback((value: string) => {
|
||||
onYearChange(Number(value))
|
||||
}, [onYearChange])
|
||||
const handleYearChange = useCallback(
|
||||
(value: string) => {
|
||||
onYearChange(Number(value))
|
||||
},
|
||||
[onYearChange],
|
||||
)
|
||||
|
||||
const handleMonthChange = useCallback((value: string) => {
|
||||
onMonthChange(value === "all" ? "all" : Number(value))
|
||||
}, [onMonthChange])
|
||||
const handleMonthChange = useCallback(
|
||||
(value: string) => {
|
||||
onMonthChange(value === "all" ? "all" : Number(value))
|
||||
},
|
||||
[onMonthChange],
|
||||
)
|
||||
|
||||
const isDefaultFilter = selectedYear === 2024 && selectedMonth === "all"
|
||||
|
||||
|
@ -72,10 +78,7 @@ export default function MapFilterControl({
|
|||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={selectedMonth.toString()}
|
||||
onValueChange={handleMonthChange}
|
||||
>
|
||||
<Select value={selectedMonth.toString()} onValueChange={handleMonthChange}>
|
||||
<SelectTrigger className="h-8 w-full">
|
||||
<SelectValue placeholder="Month" />
|
||||
</SelectTrigger>
|
||||
|
@ -93,12 +96,7 @@ export default function MapFilterControl({
|
|||
<Button className="h-8 text-xs flex-1" variant="default" onClick={onApplyFilters}>
|
||||
Apply
|
||||
</Button>
|
||||
<Button
|
||||
className="h-8 text-xs"
|
||||
variant="ghost"
|
||||
onClick={onResetFilters}
|
||||
disabled={isDefaultFilter}
|
||||
>
|
||||
<Button className="h-8 text-xs" variant="ghost" onClick={onResetFilters} disabled={isDefaultFilter}>
|
||||
<FilterX className="h-3 w-3 mr-1" />
|
||||
Reset
|
||||
</Button>
|
||||
|
|
|
@ -1,109 +1,25 @@
|
|||
import { CRIME_RATE_COLORS } from "@/app/_utils/const/map"
|
||||
import { ControlPosition, IControl, Map } from "mapbox-gl"
|
||||
import { useControl } from "react-map-gl/mapbox"
|
||||
import React, { useEffect } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { CRIME_RATE_COLORS } from "@/app/_utils/const/map";
|
||||
import { Overlay } from "../overlay";
|
||||
import { ControlPosition } from "mapbox-gl";
|
||||
|
||||
interface MapLegendProps {
|
||||
position?: ControlPosition
|
||||
isFullscreen?: boolean
|
||||
}
|
||||
|
||||
// React component for legend content
|
||||
const LegendContent = () => {
|
||||
export default function MapLegend({ position = "bottom-right" }: MapLegendProps) {
|
||||
return (
|
||||
<div className="flex flex-row text-xs font-semibold">
|
||||
<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` }}>
|
||||
<span>Low</span>
|
||||
<Overlay position={position}>
|
||||
<div className="flex flex-row text-xs font-semibold font-sans text-white">
|
||||
<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 className={`flex items-center gap-1.5 py-0 px-8 border-y border-1 `} style={{ backgroundColor: `${CRIME_RATE_COLORS.medium}90` }}>
|
||||
<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>
|
||||
</Overlay>
|
||||
)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -2,16 +2,20 @@
|
|||
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
import { Menu } from "lucide-react"
|
||||
import { Overlay } from "../overlay"
|
||||
import { ControlPosition } from "mapbox-gl"
|
||||
|
||||
interface SidebarToggleProps {
|
||||
isOpen: boolean
|
||||
onToggle: () => void
|
||||
position?: ControlPosition
|
||||
}
|
||||
|
||||
export default function SidebarToggle({ isOpen, onToggle }: SidebarToggleProps) {
|
||||
export default function SidebarToggle({ isOpen, onToggle, position = "left" }: SidebarToggleProps) {
|
||||
if (isOpen) return null
|
||||
|
||||
return (
|
||||
<Overlay position={position}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
|
@ -21,5 +25,6 @@ export default function SidebarToggle({ isOpen, onToggle }: SidebarToggleProps)
|
|||
<Menu className="h-5 w-5" />
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
</Button>
|
||||
</Overlay>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
"use client"
|
||||
import { Checkbox } from "@/app/_components/ui/checkbox"
|
||||
import { Label } from "@/app/_components/ui/label"
|
||||
import { Overlay } from "../overlay"
|
||||
import { ControlPosition } from "mapbox-gl"
|
||||
|
||||
|
||||
interface TimeControlsProps {
|
||||
onTimeChange: (time: string) => void
|
||||
activeTime: string
|
||||
position?: ControlPosition
|
||||
}
|
||||
|
||||
export default function TimeControls({ onTimeChange, activeTime }: TimeControlsProps) {
|
||||
export default function TimeControls({ onTimeChange, activeTime, position = "bottom-left" }: TimeControlsProps) {
|
||||
const times = [
|
||||
{ id: "today", label: "Hari ini" },
|
||||
{ id: "yesterday", label: "Kemarin" },
|
||||
|
@ -16,16 +20,18 @@ export default function TimeControls({ onTimeChange, activeTime }: TimeControlsP
|
|||
]
|
||||
|
||||
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">
|
||||
<div className="text-white font-medium mr-2">Waktu</div>
|
||||
{times.map((time) => (
|
||||
<div key={time.id} className="flex items-center space-x-2">
|
||||
<Checkbox id={time.id} checked={activeTime === time.id} onCheckedChange={() => onTimeChange(time.id)} />
|
||||
<Label htmlFor={time.id} className="text-white text-sm cursor-pointer">
|
||||
{time.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Overlay position={position}>
|
||||
<div className="bg-black/75 rounded-md p-2 flex items-center space-x-4">
|
||||
<div className="text-white font-medium mr-2">Waktu</div>
|
||||
{times.map((time) => (
|
||||
<div key={time.id} className="flex items-center space-x-2">
|
||||
<Checkbox id={time.id} checked={activeTime === time.id} onCheckedChange={() => onTimeChange(time.id)} />
|
||||
<Label htmlFor={time.id} className="text-white text-sm cursor-pointer">
|
||||
{time.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Overlay>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { useState } from "react"
|
|||
|
||||
import { CrimePopup } from "./pop-up"
|
||||
import CrimeMarker, { type CrimeIncident } from "./markers/crime-marker"
|
||||
import { MapLegend } from "./controls/map-legend"
|
||||
|
||||
|
||||
const months = [
|
||||
{ value: "1", label: "January" },
|
||||
|
@ -165,8 +165,6 @@ export default function CrimeMap() {
|
|||
onApplyFilters={applyFilters}
|
||||
onResetFilters={resetFilters}
|
||||
>
|
||||
{/* Show the legend regardless of fullscreen state if showLegend is true */}
|
||||
{<MapLegend />}
|
||||
|
||||
{/* District Layer with crime data */}
|
||||
<DistrictLayer
|
||||
|
|
|
@ -59,7 +59,7 @@ export default function DistrictLayer({
|
|||
// Use district_id (which corresponds to kode_kec in the tileset) as the key
|
||||
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] = {
|
||||
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 crimeData = crimeDataByDistrict[districtId] || {}
|
||||
|
||||
console.log("Hover district:", districtId, "found data:", crimeData)
|
||||
// console.log("Hover district:", districtId, "found data:", crimeData)
|
||||
|
||||
// Enhance feature with crime data
|
||||
feature.properties = {
|
||||
|
|
|
@ -3,16 +3,15 @@
|
|||
import type React from "react"
|
||||
|
||||
import { useState, useCallback, useRef } from "react"
|
||||
import ReactMapGL, {
|
||||
import {
|
||||
type ViewState,
|
||||
NavigationControl,
|
||||
ScaleControl,
|
||||
type MapRef,
|
||||
FullscreenControl,
|
||||
GeolocateControl,
|
||||
Map,
|
||||
} from "react-map-gl/mapbox"
|
||||
import { BASE_LATITUDE, BASE_LONGITUDE, BASE_ZOOM, MAP_STYLE, MAPBOX_STYLES, MapboxStyle } from "@/app/_utils/const/map"
|
||||
import { Search } from "lucide-react"
|
||||
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"
|
||||
|
@ -21,6 +20,9 @@ import SeverityIndicator from "./controls/severity-indicator"
|
|||
import MapFilterControl from "./controls/map-filter-control"
|
||||
import { useFullscreen } from "@/app/_hooks/use-fullscreen"
|
||||
import MapControls from "./controls/map-control"
|
||||
import { Overlay } from "./overlay"
|
||||
import MapLegend from "./controls/map-legend"
|
||||
|
||||
|
||||
interface MapViewProps {
|
||||
children?: React.ReactNode
|
||||
|
@ -117,7 +119,7 @@ export default function MapView({
|
|||
<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"}`}>
|
||||
<ReactMapGL
|
||||
<Map
|
||||
ref={(ref) => setMapRef(ref)}
|
||||
mapStyle={mapStyle}
|
||||
mapboxAccessToken={mapboxApiAccessToken}
|
||||
|
@ -132,26 +134,20 @@ export default function MapView({
|
|||
<FullscreenControl 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 */}
|
||||
{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}
|
||||
|
@ -159,19 +155,23 @@ export default function MapView({
|
|||
selectedYear={selectedYear}
|
||||
selectedMonth={selectedMonth}
|
||||
/>
|
||||
<SidebarToggle isOpen={sidebarOpen} onToggle={toggleSidebar} />
|
||||
<MapControls onControlChange={handleControlChange} activeControl={activeControl} />
|
||||
<TimeControls onTimeChange={handleTimeChange} activeTime={activeTime} />
|
||||
<SeverityIndicator />
|
||||
|
||||
{/* 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" />
|
||||
|
||||
</>
|
||||
)}
|
||||
</ReactMapGL>
|
||||
</Map>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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 {
|
||||
isFullscreen,
|
||||
enterFullscreen,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"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",
|
||||
|
@ -1644,6 +1645,24 @@
|
|||
"@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",
|
||||
|
@ -1652,6 +1671,52 @@
|
|||
"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",
|
||||
|
@ -14015,6 +14080,12 @@
|
|||
"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",
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"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",
|
||||
|
|
Loading…
Reference in New Issue