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,
|
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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
return {
|
||||||
isFullscreen,
|
isFullscreen,
|
||||||
enterFullscreen,
|
enterFullscreen,
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue