diff --git a/src/app/messages/page.tsx b/src/app/messages/page.tsx
index 36e9a69..f76272c 100644
--- a/src/app/messages/page.tsx
+++ b/src/app/messages/page.tsx
@@ -1,4 +1,5 @@
"use client";
+import { Suspense } from "react";
import { useConfig } from "@/components/ConfigContext";
import ChatBox from "@/components/ChatBox";
@@ -12,7 +13,9 @@ export default function MessagesPage() {
{/* ChatBox component with all messages tab enabled and expanded behavior */}
diff --git a/src/components/MapView.tsx b/src/components/MapView.tsx
index 5b56ac5..f0ff48e 100644
--- a/src/components/MapView.tsx
+++ b/src/components/MapView.tsx
@@ -14,6 +14,7 @@ import { renderToString } from "react-dom/server";
import { buildApiUrl } from "../lib/api";
import { NodePosition } from "../types/map";
import { useNeighbors, type Neighbor } from "../hooks/useNeighbors";
+import { useQueryParams } from "../hooks/useQueryParams";
const DEFAULT = {
lat: 46.56, // Center between Seattle and Portland
@@ -21,6 +22,12 @@ const DEFAULT = {
zoom: 7, // Zoom level to show both cities
};
+interface MapQuery {
+ lat?: number;
+ lng?: number;
+ zoom?: number;
+}
+
type ClusteredMarkersProps = {
nodes: NodePosition[];
@@ -30,7 +37,7 @@ type ClusteredMarkersProps = {
};
// Individual marker component
-function IndividualMarker({
+const IndividualMarker = React.memo(function IndividualMarker({
node,
showNodeNames,
selectedNodeId,
@@ -89,17 +96,13 @@ function IndividualMarker({
map.removeLayer(markerRef.current);
}
};
- }, [map, node, showNodeNames, selectedNodeId, isLoadingNeighbors]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally omitting selectedNodeId, showNodeNames, isLoadingNeighbors to prevent marker recreation
+ }, [map, node.node_id, node.latitude, node.longitude, node.type]);
- // Update marker when node data changes
+ // Update marker when visual properties change (but don't recreate marker)
useEffect(() => {
if (markerRef.current) {
- const currentPos = markerRef.current.getLatLng();
- if (currentPos.lat !== node.latitude || currentPos.lng !== node.longitude) {
- markerRef.current.setLatLng([node.latitude, node.longitude]);
- }
-
- // Update icon and popup
+ // Update icon and popup content only
const isSelected = selectedNodeId === node.node_id;
const icon = L.divIcon({
className: 'custom-node-marker-container',
@@ -119,11 +122,21 @@ function IndividualMarker({
}
}, [node, showNodeNames, selectedNodeId, isLoadingNeighbors]);
+ // Handle position updates separately to avoid recreating marker
+ useEffect(() => {
+ if (markerRef.current) {
+ const currentPos = markerRef.current.getLatLng();
+ if (currentPos.lat !== node.latitude || currentPos.lng !== node.longitude) {
+ markerRef.current.setLatLng([node.latitude, node.longitude]);
+ }
+ }
+ }, [node.latitude, node.longitude]);
+
return null;
-}
+});
// Clustered markers component
-function ClusteredMarkersGroup({
+const ClusteredMarkersGroup = React.memo(function ClusteredMarkersGroup({
nodes,
showNodeNames,
selectedNodeId,
@@ -145,6 +158,7 @@ function ClusteredMarkersGroup({
onNodeClickRef.current = onNodeClick;
}, [onNodeClick]);
+ // Create cluster group only when map or nodes array changes
useEffect(() => {
if (!map) return;
@@ -201,12 +215,40 @@ function ClusteredMarkersGroup({
map.removeLayer(clusterGroupRef.current);
}
};
- }, [map, nodes, showNodeNames, selectedNodeId, isLoadingNeighbors]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally omitting selectedNodeId, showNodeNames, isLoadingNeighbors to prevent cluster recreation
+ }, [map, nodes]);
+
+ // Update marker appearances when visual properties change
+ useEffect(() => {
+ if (!clusterGroupRef.current) return;
+
+ clusterGroupRef.current.eachLayer((marker: any) => {
+ const nodeData = marker.options.nodeData;
+ if (nodeData) {
+ const isSelected = selectedNodeId === nodeData.node_id;
+ const icon = L.divIcon({
+ className: 'custom-node-marker-container',
+ iconSize: [16, 32],
+ iconAnchor: [8, 8],
+ html: renderToString(
+
+ ),
+ });
+ marker.setIcon(icon);
+ marker.getPopup()?.setContent(renderToString());
+ }
+ });
+ }, [showNodeNames, selectedNodeId, isLoadingNeighbors]);
return null;
-}
+});
-function ClusteredMarkers({ nodes, selectedNodeId, onNodeClick, isLoadingNeighbors = false }: ClusteredMarkersProps) {
+const ClusteredMarkers = React.memo(function ClusteredMarkers({ nodes, selectedNodeId, onNodeClick, isLoadingNeighbors = false }: ClusteredMarkersProps) {
const configResult = useConfig();
const config = configResult?.config;
const showNodeNames = config?.showNodeNames !== false;
@@ -239,7 +281,7 @@ function ClusteredMarkers({ nodes, selectedNodeId, onNodeClick, isLoadingNeighbo
/>
);
}
-}
+});
// Component to render neighbor lines with directional arrows
function NeighborLines({
@@ -314,6 +356,16 @@ export default function MapView() {
const configResult = useConfig();
const config = configResult?.config;
+ // Use query params to persist map position
+ const { query: mapQuery, updateQuery: updateMapQuery } = useQueryParams({
+ lat: DEFAULT.lat,
+ lng: DEFAULT.lng,
+ zoom: DEFAULT.zoom,
+ });
+
+ const mapCenter: [number, number] = [mapQuery.lat ?? DEFAULT.lat, mapQuery.lng ?? DEFAULT.lng];
+ const mapZoom = mapQuery.zoom ?? DEFAULT.zoom;
+
// Neighbor-related state
const [selectedNodeId, setSelectedNodeId] = useState(null);
@@ -409,7 +461,18 @@ export default function MapView() {
function MapEventCatcher() {
useMapEvents({
moveend: (e) => {
- const b = e.target.getBounds();
+ const map = e.target;
+ const center = map.getCenter();
+ const zoom = map.getZoom();
+
+ // Update URL with new map position
+ updateMapQuery({
+ lat: Math.round(center.lat * 100000) / 100000, // Round to 5 decimal places
+ lng: Math.round(center.lng * 100000) / 100000,
+ zoom: zoom
+ });
+
+ const b = map.getBounds();
const buffer = 0.2; // 20% buffer
const latDiff = b.getNorthEast().lat - b.getSouthWest().lat;
const lngDiff = b.getNorthEast().lng - b.getSouthWest().lng;
@@ -433,7 +496,18 @@ export default function MapView() {
}
},
zoomend: (e) => {
- const b = e.target.getBounds();
+ const map = e.target;
+ const center = map.getCenter();
+ const zoom = map.getZoom();
+
+ // Update URL with new map position
+ updateMapQuery({
+ lat: Math.round(center.lat * 100000) / 100000, // Round to 5 decimal places
+ lng: Math.round(center.lng * 100000) / 100000,
+ zoom: zoom
+ });
+
+ const b = map.getBounds();
const buffer = 0.2; // 20% buffer
const latDiff = b.getNorthEast().lat - b.getSouthWest().lat;
const lngDiff = b.getNorthEast().lng - b.getSouthWest().lng;
@@ -502,9 +576,9 @@ export default function MapView() {
ariaLabel="Refresh map nodes"
/>
-
@@ -529,7 +603,6 @@ export default function MapView() {
/>
)}
({ x: 140, y: 100 }), []);
const handleToggle = useCallback(() => {
setExpanded(prev => !prev);
@@ -319,7 +319,7 @@ export default function PathVisualization({
);
- }, [showGraph, pathsCount, treeData, graphFullscreen, handleFullscreenToggle, renderCustomNodeElement]);
+ }, [showGraph, pathsCount, treeData, graphFullscreen, handleFullscreenToggle, renderCustomNodeElement, fixedNodeSize]);
if (!showDropdown) {
return (