feat: enhance map functionality by adding URL state management and event handling for view changes
This commit is contained in:
@@ -400,29 +400,30 @@ 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 && (
|
||||||
<Button variant="outline" size="sm">
|
<Link href={`/projects/map?lat=${project.coordinates.split(',')[0].trim()}&lng=${project.coordinates.split(',')[1].trim()}&zoom=16`}>
|
||||||
<svg
|
<Button variant="outline" size="sm">
|
||||||
className="w-4 h-4 mr-2"
|
<svg
|
||||||
fill="none"
|
className="w-4 h-4 mr-2"
|
||||||
stroke="currentColor"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
stroke="currentColor"
|
||||||
>
|
viewBox="0 0 24 24"
|
||||||
<path
|
>
|
||||||
strokeLinecap="round"
|
<path
|
||||||
strokeLinejoin="round"
|
strokeLinecap="round"
|
||||||
strokeWidth={2}
|
strokeLinejoin="round"
|
||||||
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"
|
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
|
</svg>
|
||||||
</Button>
|
View on Full Map
|
||||||
</Link>
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -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,35 +134,45 @@ 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);
|
||||||
|
|
||||||
// Calculate center based on projects with coordinates
|
// Only calculate center based on projects if no URL parameters are provided
|
||||||
const projectsWithCoords = data.filter((p) => p.coordinates);
|
const lat = searchParams.get('lat');
|
||||||
if (projectsWithCoords.length > 0) {
|
const lng = searchParams.get('lng');
|
||||||
const avgLat =
|
|
||||||
projectsWithCoords.reduce((sum, p) => {
|
|
||||||
const [lat] = p.coordinates
|
|
||||||
.split(",")
|
|
||||||
.map((coord) => parseFloat(coord.trim()));
|
|
||||||
return sum + lat;
|
|
||||||
}, 0) / projectsWithCoords.length;
|
|
||||||
|
|
||||||
const avgLng =
|
if (!lat || !lng) {
|
||||||
projectsWithCoords.reduce((sum, p) => {
|
// Calculate center based on projects with coordinates
|
||||||
const [, lng] = p.coordinates
|
const projectsWithCoords = data.filter((p) => p.coordinates);
|
||||||
.split(",")
|
if (projectsWithCoords.length > 0) {
|
||||||
.map((coord) => parseFloat(coord.trim()));
|
const avgLat =
|
||||||
return sum + lng;
|
projectsWithCoords.reduce((sum, p) => {
|
||||||
}, 0) / projectsWithCoords.length;
|
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);
|
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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user