diff --git a/src/app/projects/map/page.js b/src/app/projects/map/page.js index 6f1b837..abe74cf 100644 --- a/src/app/projects/map/page.js +++ b/src/app/projects/map/page.js @@ -5,6 +5,7 @@ import Link from "next/link"; import dynamic from "next/dynamic"; import { useSearchParams, useRouter } from "next/navigation"; import Button from "@/components/ui/Button"; +import { mapLayers } from "@/components/ui/mapLayers"; // Dynamically import the map component to avoid SSR issues const DynamicMap = dynamic(() => import("@/components/ui/LeafletMap"), { @@ -29,6 +30,9 @@ export default function ProjectsMapPage() { in_progress_construction: true, fulfilled: true, }); + const [activeBaseLayer, setActiveBaseLayer] = useState("Polish Geoportal Orthophoto"); + const [activeOverlays, setActiveOverlays] = useState([]); + const [showLayerPanel, setShowLayerPanel] = useState(true); // Status configuration with colors and labels const statusConfig = { @@ -68,6 +72,7 @@ export default function ProjectsMapPage() { ); setStatusFilters(newState); }; + // Toggle status filter const toggleStatusFilter = (status) => { setStatusFilters((prev) => ({ @@ -75,6 +80,26 @@ export default function ProjectsMapPage() { [status]: !prev[status], })); }; + + // Layer control functions + const handleBaseLayerChange = (layerName) => { + setActiveBaseLayer(layerName); + }; + + const toggleOverlay = (layerName) => { + setActiveOverlays((prev) => { + if (prev.includes(layerName)) { + return prev.filter((name) => name !== layerName); + } else { + return [...prev, layerName]; + } + }); + }; + + const toggleLayerPanel = () => { + setShowLayerPanel(!showLayerPanel); + }; + // Update URL with current map state (debounced to avoid too many updates) const updateURL = (center, zoom) => { const params = new URLSearchParams(); @@ -96,7 +121,9 @@ export default function ProjectsMapPage() { window.mapUpdateTimeout = setTimeout(() => { updateURL(center, zoom); }, 500); // Wait 500ms after the last move to update URL - };// Hide navigation and ensure full-screen layout + }; + + // Hide navigation and ensure full-screen layout useEffect(() => { // Check for URL parameters for coordinates and zoom const lat = searchParams.get('lat'); @@ -127,6 +154,7 @@ export default function ProjectsMapPage() { // Prevent scrolling on body document.body.style.overflow = "hidden"; document.documentElement.style.overflow = "hidden"; + // Cleanup when leaving page return () => { if (nav) { @@ -141,6 +169,7 @@ export default function ProjectsMapPage() { } }; }, [searchParams]); + useEffect(() => { fetch("/api/projects") .then((res) => res.json()) @@ -182,6 +211,7 @@ export default function ProjectsMapPage() { setLoading(false); }); }, [searchParams]); + // Convert projects to map markers with filtering const markers = projects .filter((project) => project.coordinates) @@ -258,7 +288,7 @@ export default function ProjectsMapPage() { {project.plot} )} - {" "} + {project.project_status && (
Status: @@ -303,6 +333,7 @@ export default function ProjectsMapPage() { }; }) .filter((marker) => marker !== null); + if (loading) { return (
@@ -317,99 +348,54 @@ export default function ProjectsMapPage() { ); } return ( -
- {" "} - {/* Floating Header with Controls */} -
-
-
-
-

- Projects Map -

-
- {markers.length} of {projects.length} projects with coordinates -
+
+ {/* Floating Header - Left Side */} +
+ {/* Title Box */} +
+
+

+ Projects Map +

+
+ {markers.length} of {projects.length} projects with coordinates
-
- {/* Status Filter Panel */} -
-
- - Filters: - +
+
- {/* Toggle All Button */} - - - {/* Individual Status Filters */} - {Object.entries(statusConfig).map(([status, config]) => { - const isActive = statusFilters[status]; - const projectCount = projects.filter( - (p) => p.project_status === status && p.coordinates - ).length; - - return ( - - ); - })} -
-
+ {/* Zoom Controls - Below Title */} +
+
+ +
+
+ {/* Layer Control Panel - Right Side */} +
+ {/* Action Buttons */}
-
{" "} - {/* Stats Panel - Bottom Left */} - {markers.length > 0 && ( -
-
-
-
- - {markers.length} of{" "} - {projects.filter((p) => p.coordinates).length} projects shown - -
- {/* Status breakdown */} -
- {Object.entries(statusConfig).map(([status, config]) => { - const totalCount = projects.filter( - (p) => p.project_status === status && p.coordinates - ).length; - const visibleCount = markers.filter((m) => { - const project = projects.find( - (p) => - p.coordinates && - p.coordinates - .split(",") - .map((c) => parseFloat(c.trim())) - .toString() === m.position.toString() - ); - return project && project.project_status === status; - }).length; - - if (totalCount === 0) return null; - - return ( -
-
- - {config.shortLabel}:{" "} - {statusFilters[status] ? visibleCount : 0}/{totalCount} - -
- ); - })} -
- - {projects.length > projects.filter((p) => p.coordinates).length && ( -
-
- - {projects.length - - projects.filter((p) => p.coordinates).length}{" "} - missing coordinates + {/* Layer Control Panel */} +
+ {/* Layer Control Header */} +
+ +
{/* Layer Control Content */} +
+
+ {/* Base Layers Section */} +
+

+ + + + Base Maps +

+
+ {mapLayers.base.map((layer, index) => ( + + ))} +
+
+ + {/* Overlay Layers Section */} + {mapLayers.overlays && mapLayers.overlays.length > 0 && ( +
+

+ + + + Overlay Layers +

+ {mapLayers.overlays.map((layer, index) => ( + + ))} +
+
+ )} +
+
+
+ + {/* Status Filter Panel - Bottom Left */} +
+
+
+ + Filters: + + + {/* Toggle All Button */} + + + {/* Individual Status Filters */} + {Object.entries(statusConfig).map(([status, config]) => { + const isActive = statusFilters[status]; + const projectCount = projects.filter( + (p) => p.project_status === status && p.coordinates + ).length; + + return ( + + ); + })}
+
+
{/* Status Panel - Bottom Left */} + {markers.length > 0 && (
+
+ + Filters: + + + {/* Toggle All Button */} + + + {/* Individual Status Filters */} + {Object.entries(statusConfig).map(([status, config]) => { + const isActive = statusFilters[status]; + const projectCount = projects.filter( + (p) => p.project_status === status && p.coordinates + ).length; + + return ( + + ); + })}
- )} - {/* Help Panel - Bottom Right */} -
-
- Click markers for details • Use 📚 to switch layers -
-
- {/* Full Screen Map */} + )} {/* Full Screen Map */} {markers.length === 0 ? (
@@ -559,17 +757,18 @@ export default function ProjectsMapPage() {
- ) : (
+ ) : ( +
- )} -
+ )}
); } diff --git a/src/components/ui/LeafletMap.js b/src/components/ui/LeafletMap.js index dd1abdf..c66fa46 100644 --- a/src/components/ui/LeafletMap.js +++ b/src/components/ui/LeafletMap.js @@ -8,7 +8,9 @@ import { Popup, LayersControl, useMapEvents, + useMap, } from "react-leaflet"; +import L from "leaflet"; import "leaflet/dist/leaflet.css"; import { useEffect } from "react"; import { mapLayers } from "./mapLayers"; @@ -70,26 +72,55 @@ function MapEventHandler({ onViewChange }) { return null; } +// Custom zoom control component that handles external events +function CustomZoomHandler() { + const map = useMap(); + + useEffect(() => { + if (!map) return; + + const handleZoomIn = () => { + map.zoomIn(); + }; + + const handleZoomOut = () => { + map.zoomOut(); + }; + + // Listen for custom zoom events + window.addEventListener('mapZoomIn', handleZoomIn); + window.addEventListener('mapZoomOut', handleZoomOut); + + return () => { + window.removeEventListener('mapZoomIn', handleZoomIn); + window.removeEventListener('mapZoomOut', handleZoomOut); + }; + }, [map]); + + return null; +} + export default function EnhancedLeafletMap({ center, zoom = 13, markers = [], showLayerControl = true, defaultLayer = "OpenStreetMap", + activeOverlays = [], onViewChange, }) { useEffect(() => { fixLeafletIcons(); - }, []); - const { BaseLayer, Overlay } = LayersControl; - return ( + }, []); const { BaseLayer, Overlay } = LayersControl; + return ( - {/* Handle map view changes */} + {onViewChange && } {showLayerControl ? ( @@ -135,13 +166,52 @@ export default function EnhancedLeafletMap({ )} ))} - - ) : ( - // Default layer when no layer control - + ) : ( + // Custom layer rendering when no layer control + <> + {/* Base Layer */} + {(() => { + const baseLayer = mapLayers.base.find(layer => layer.name === defaultLayer) || mapLayers.base[0]; + return ( + + ); + })()} + + {/* Active Overlay Layers */} + {mapLayers.overlays && mapLayers.overlays + .filter(layer => activeOverlays.includes(layer.name)) + .map((layer, index) => { + if (layer.type === "wms") { + return ( + + ); + } else { + return ( + + ); + } + }) + } + )}{" "} {markers.map((marker, index) => (