feat: enhance map functionality by adding URL state management and event handling for view changes

This commit is contained in:
2025-06-20 23:56:11 +02:00
parent 7b4d5afb90
commit e5d681547d
3 changed files with 129 additions and 50 deletions

View File

@@ -400,29 +400,30 @@ export default async function ProjectViewPage({ params }) {
<div className="mb-8">
{" "}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardHeader> <div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900">
Project Location
</h2>
<Link href="/projects/map">
<Button variant="outline" size="sm">
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m0 0L9 7"
/>
</svg>
View on Full Map
</Button>
</Link>
{project.coordinates && (
<Link href={`/projects/map?lat=${project.coordinates.split(',')[0].trim()}&lng=${project.coordinates.split(',')[1].trim()}&zoom=16`}>
<Button variant="outline" size="sm">
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m0 0L9 7"
/>
</svg>
View on Full Map
</Button>
</Link>
)}
</div>
</CardHeader>
<CardContent>

View File

@@ -3,6 +3,7 @@
import React, { useEffect, useState } from "react";
import Link from "next/link";
import dynamic from "next/dynamic";
import { useSearchParams, useRouter } from "next/navigation";
import Button from "@/components/ui/Button";
// Dynamically import the map component to avoid SSR issues
@@ -16,9 +17,12 @@ const DynamicMap = dynamic(() => import("@/components/ui/LeafletMap"), {
});
export default function ProjectsMapPage() {
const searchParams = useSearchParams();
const router = useRouter();
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true);
const [mapCenter, setMapCenter] = useState([50.0614, 19.9366]); // Default to Krakow, Poland
const [mapZoom, setMapZoom] = useState(10); // Default zoom level
const [statusFilters, setStatusFilters] = useState({
registered: true,
in_progress_design: true,
@@ -64,7 +68,6 @@ export default function ProjectsMapPage() {
);
setStatusFilters(newState);
};
// Toggle status filter
const toggleStatusFilter = (status) => {
setStatusFilters((prev) => ({
@@ -72,8 +75,49 @@ export default function ProjectsMapPage() {
[status]: !prev[status],
}));
};
// Hide navigation and ensure full-screen layout
// Update URL with current map state (debounced to avoid too many updates)
const updateURL = (center, zoom) => {
const params = new URLSearchParams();
params.set('lat', center[0].toFixed(6));
params.set('lng', center[1].toFixed(6));
params.set('zoom', zoom.toString());
// Use replace to avoid cluttering browser history
router.replace(`/projects/map?${params.toString()}`, { scroll: false });
};
// Handle map view changes with debouncing
const handleMapViewChange = (center, zoom) => {
setMapCenter(center);
setMapZoom(zoom);
// Debounce URL updates to avoid too many history entries
clearTimeout(window.mapUpdateTimeout);
window.mapUpdateTimeout = setTimeout(() => {
updateURL(center, zoom);
}, 500); // Wait 500ms after the last move to update URL
};// Hide navigation and ensure full-screen layout
useEffect(() => {
// Check for URL parameters for coordinates and zoom
const lat = searchParams.get('lat');
const lng = searchParams.get('lng');
const zoom = searchParams.get('zoom');
if (lat && lng) {
const latitude = parseFloat(lat);
const longitude = parseFloat(lng);
if (!isNaN(latitude) && !isNaN(longitude)) {
setMapCenter([latitude, longitude]);
}
}
if (zoom) {
const zoomLevel = parseInt(zoom);
if (!isNaN(zoomLevel) && zoomLevel >= 1 && zoomLevel <= 20) {
setMapZoom(zoomLevel);
}
}
// Hide navigation bar for full-screen experience
const nav = document.querySelector("nav");
if (nav) {
@@ -83,7 +127,6 @@ 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) {
@@ -91,35 +134,45 @@ export default function ProjectsMapPage() {
}
document.body.style.overflow = "";
document.documentElement.style.overflow = "";
};
}, []);
// Clear any pending URL updates
if (window.mapUpdateTimeout) {
clearTimeout(window.mapUpdateTimeout);
}
};
}, [searchParams]);
useEffect(() => {
fetch("/api/projects")
.then((res) => res.json())
.then((data) => {
setProjects(data);
// Calculate center based on projects with coordinates
const projectsWithCoords = data.filter((p) => p.coordinates);
if (projectsWithCoords.length > 0) {
const avgLat =
projectsWithCoords.reduce((sum, p) => {
const [lat] = p.coordinates
.split(",")
.map((coord) => parseFloat(coord.trim()));
return sum + lat;
}, 0) / projectsWithCoords.length;
// Only calculate center based on projects if no URL parameters are provided
const lat = searchParams.get('lat');
const lng = searchParams.get('lng');
const avgLng =
projectsWithCoords.reduce((sum, p) => {
const [, lng] = p.coordinates
.split(",")
.map((coord) => parseFloat(coord.trim()));
return sum + lng;
}, 0) / projectsWithCoords.length;
if (!lat || !lng) {
// Calculate center based on projects with coordinates
const projectsWithCoords = data.filter((p) => p.coordinates);
if (projectsWithCoords.length > 0) {
const avgLat =
projectsWithCoords.reduce((sum, p) => {
const [lat] = p.coordinates
.split(",")
.map((coord) => parseFloat(coord.trim()));
return sum + lat;
}, 0) / projectsWithCoords.length;
setMapCenter([avgLat, avgLng]);
const avgLng =
projectsWithCoords.reduce((sum, p) => {
const [, lng] = p.coordinates
.split(",")
.map((coord) => parseFloat(coord.trim()));
return sum + lng;
}, 0) / projectsWithCoords.length;
setMapCenter([avgLat, avgLng]);
}
}
setLoading(false);
@@ -128,7 +181,7 @@ export default function ProjectsMapPage() {
console.error("Error fetching projects:", error);
setLoading(false);
});
}, []);
}, [searchParams]);
// Convert projects to map markers with filtering
const markers = projects
.filter((project) => project.coordinates)
@@ -506,14 +559,14 @@ export default function ProjectsMapPage() {
</div>
</div>
</div>
) : (
<div className="absolute inset-0">
) : ( <div className="absolute inset-0">
<DynamicMap
center={mapCenter}
zoom={10}
zoom={mapZoom}
markers={markers}
showLayerControl={true}
defaultLayer="Polish Geoportal Orthophoto"
onViewChange={handleMapViewChange}
/>
</div>
)}

View File

@@ -6,6 +6,7 @@ import {
Marker,
Popup,
LayersControl,
useMapEvents,
} from "react-leaflet";
import "leaflet/dist/leaflet.css";
import { useEffect } from "react";
@@ -43,23 +44,44 @@ const createColoredMarkerIcon = (color) => {
popupAnchor: [1, -34],
shadowSize: [41, 41],
});
}
return null;
} return null;
};
// Component to handle map events
function MapEventHandler({ onViewChange }) {
const map = useMapEvents({
moveend: () => {
if (onViewChange) {
const center = map.getCenter();
const zoom = map.getZoom();
onViewChange([center.lat, center.lng], zoom);
}
},
zoomend: () => {
if (onViewChange) {
const center = map.getCenter();
const zoom = map.getZoom();
onViewChange([center.lat, center.lng], zoom);
}
},
});
return null;
}
export default function EnhancedLeafletMap({
center,
zoom = 13,
markers = [],
showLayerControl = true,
defaultLayer = "OpenStreetMap",
onViewChange,
}) {
useEffect(() => {
fixLeafletIcons();
}, []);
const { BaseLayer } = LayersControl;
return (
<MapContainer
center={center}
@@ -67,6 +89,9 @@ export default function EnhancedLeafletMap({
style={{ height: "100%", width: "100%" }}
scrollWheelZoom={true}
>
{/* Handle map view changes */}
{onViewChange && <MapEventHandler onViewChange={onViewChange} />}
{showLayerControl ? (
<LayersControl position="topright">
{mapLayers.base.map((layer, index) => (