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,12 +400,12 @@ export default async function ProjectViewPage({ params }) {
<div className="mb-8"> <div className="mb-8">
{" "} {" "}
<Card> <Card>
<CardHeader> <CardHeader> <div className="flex items-center justify-between">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900"> <h2 className="text-xl font-semibold text-gray-900">
Project Location Project Location
</h2> </h2>
<Link href="/projects/map"> {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"> <Button variant="outline" size="sm">
<svg <svg
className="w-4 h-4 mr-2" className="w-4 h-4 mr-2"
@@ -423,6 +423,7 @@ export default async function ProjectViewPage({ params }) {
View on Full Map View on Full Map
</Button> </Button>
</Link> </Link>
)}
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>

View File

@@ -3,6 +3,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useSearchParams, useRouter } from "next/navigation";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
// Dynamically import the map component to avoid SSR issues // Dynamically import the map component to avoid SSR issues
@@ -16,9 +17,12 @@ const DynamicMap = dynamic(() => import("@/components/ui/LeafletMap"), {
}); });
export default function ProjectsMapPage() { export default function ProjectsMapPage() {
const searchParams = useSearchParams();
const router = useRouter();
const [projects, setProjects] = useState([]); const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [mapCenter, setMapCenter] = useState([50.0614, 19.9366]); // Default to Krakow, Poland const [mapCenter, setMapCenter] = useState([50.0614, 19.9366]); // Default to Krakow, Poland
const [mapZoom, setMapZoom] = useState(10); // Default zoom level
const [statusFilters, setStatusFilters] = useState({ const [statusFilters, setStatusFilters] = useState({
registered: true, registered: true,
in_progress_design: true, in_progress_design: true,
@@ -64,7 +68,6 @@ export default function ProjectsMapPage() {
); );
setStatusFilters(newState); setStatusFilters(newState);
}; };
// Toggle status filter // Toggle status filter
const toggleStatusFilter = (status) => { const toggleStatusFilter = (status) => {
setStatusFilters((prev) => ({ setStatusFilters((prev) => ({
@@ -72,8 +75,49 @@ export default function ProjectsMapPage() {
[status]: !prev[status], [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(() => { 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 // Hide navigation bar for full-screen experience
const nav = document.querySelector("nav"); const nav = document.querySelector("nav");
if (nav) { if (nav) {
@@ -83,7 +127,6 @@ export default function ProjectsMapPage() {
// Prevent scrolling on body // Prevent scrolling on body
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
document.documentElement.style.overflow = "hidden"; document.documentElement.style.overflow = "hidden";
// Cleanup when leaving page // Cleanup when leaving page
return () => { return () => {
if (nav) { if (nav) {
@@ -91,15 +134,24 @@ export default function ProjectsMapPage() {
} }
document.body.style.overflow = ""; document.body.style.overflow = "";
document.documentElement.style.overflow = ""; document.documentElement.style.overflow = "";
};
}, []);
// Clear any pending URL updates
if (window.mapUpdateTimeout) {
clearTimeout(window.mapUpdateTimeout);
}
};
}, [searchParams]);
useEffect(() => { useEffect(() => {
fetch("/api/projects") fetch("/api/projects")
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
setProjects(data); setProjects(data);
// Only calculate center based on projects if no URL parameters are provided
const lat = searchParams.get('lat');
const lng = searchParams.get('lng');
if (!lat || !lng) {
// Calculate center based on projects with coordinates // Calculate center based on projects with coordinates
const projectsWithCoords = data.filter((p) => p.coordinates); const projectsWithCoords = data.filter((p) => p.coordinates);
if (projectsWithCoords.length > 0) { if (projectsWithCoords.length > 0) {
@@ -121,6 +173,7 @@ export default function ProjectsMapPage() {
setMapCenter([avgLat, avgLng]); setMapCenter([avgLat, avgLng]);
} }
}
setLoading(false); setLoading(false);
}) })
@@ -128,7 +181,7 @@ export default function ProjectsMapPage() {
console.error("Error fetching projects:", error); console.error("Error fetching projects:", error);
setLoading(false); setLoading(false);
}); });
}, []); }, [searchParams]);
// Convert projects to map markers with filtering // Convert projects to map markers with filtering
const markers = projects const markers = projects
.filter((project) => project.coordinates) .filter((project) => project.coordinates)
@@ -506,14 +559,14 @@ export default function ProjectsMapPage() {
</div> </div>
</div> </div>
</div> </div>
) : ( ) : ( <div className="absolute inset-0">
<div className="absolute inset-0">
<DynamicMap <DynamicMap
center={mapCenter} center={mapCenter}
zoom={10} zoom={mapZoom}
markers={markers} markers={markers}
showLayerControl={true} showLayerControl={true}
defaultLayer="Polish Geoportal Orthophoto" defaultLayer="Polish Geoportal Orthophoto"
onViewChange={handleMapViewChange}
/> />
</div> </div>
)} )}

View File

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