Skip to content

Commit 1b51c6b

Browse files
committed
fix(carte): clustering des profils et recentrage fiable au clic
Les profils géocodés à la ville partageaient des coordonnées identiques et se superposaient : un seul marqueur était cliquable par ville. Ajout du clustering Leaflet (badge de comptage, spiderfy à l'ouverture) pour que chaque talent soit accessible. Le clic sur un profil recentre la carte sur sa ville et ouvre sa fiche, avec invalidateSize via ResizeObserver pour corriger le centrage quand le conteneur change de taille (bascule liste/carte, plein écran). https://claude.ai/code/session_01BTmNNAkZ7gyRc63kwgMbrx
1 parent 71df470 commit 1b51c6b

3 files changed

Lines changed: 88 additions & 13 deletions

File tree

package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"clsx": "^2.1.1",
1919
"date-fns": "^4.1.0",
2020
"leaflet": "^1.9.4",
21+
"leaflet.markercluster": "^1.5.3",
2122
"lucide-react": "^1.6.0",
2223
"next-themes": "^0.4.6",
2324
"radix-ui": "^1.4.3",
@@ -37,6 +38,7 @@
3738
},
3839
"devDependencies": {
3940
"@types/leaflet": "^1.9.21",
41+
"@types/leaflet.markercluster": "^1.5.6",
4042
"@types/node": "^24.12.0",
4143
"@types/react": "^19.2.14",
4244
"@types/react-dom": "^19.2.3",

src/components/map/congo-map.tsx

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import React, { useEffect, useRef } from "react";
22
import L from "leaflet";
33
import "leaflet/dist/leaflet.css";
4+
import "leaflet.markercluster";
5+
import "leaflet.markercluster/dist/MarkerCluster.css";
6+
import "leaflet.markercluster/dist/MarkerCluster.Default.css";
47
import type { Profile } from "@/types";
58
import { useTheme } from "@/components/theme-provider";
69
import { escapeHtml } from "@/lib/profile-service";
@@ -48,7 +51,7 @@ export const CongoMap = React.memo(function CongoMap({ profiles, onProfileClick,
4851
const mapRef = useRef<L.Map | null>(null);
4952
const containerRef = useRef<HTMLDivElement>(null);
5053
const tileLayerRef = useRef<L.TileLayer | null>(null);
51-
const markersRef = useRef<L.LayerGroup | null>(null);
54+
const markersRef = useRef<L.MarkerClusterGroup | null>(null);
5255
const markersMapRef = useRef<Map<string, L.Marker>>(new Map());
5356
const { theme } = useTheme();
5457

@@ -89,6 +92,22 @@ export const CongoMap = React.memo(function CongoMap({ profiles, onProfileClick,
8992
.leaflet-popup-content {
9093
margin: 12px !important;
9194
}
95+
.congo-cluster { background: transparent; }
96+
.congo-cluster-inner {
97+
display: flex;
98+
align-items: center;
99+
justify-content: center;
100+
border-radius: 50%;
101+
background: rgba(16, 185, 129, 0.9);
102+
color: #ffffff;
103+
font-weight: 700;
104+
font-family: system-ui, sans-serif;
105+
font-size: 13px;
106+
border: 2px solid #ffffff;
107+
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.22), 0 2px 8px rgba(0, 0, 0, 0.45);
108+
transition: transform 0.15s ease;
109+
}
110+
.congo-cluster:hover .congo-cluster-inner { transform: scale(1.08); }
92111
`;
93112
document.head.appendChild(style);
94113
}
@@ -122,11 +141,33 @@ export const CongoMap = React.memo(function CongoMap({ profiles, onProfileClick,
122141
maxZoom: 13,
123142
}).addTo(mapRef.current);
124143

125-
markersRef.current = L.layerGroup().addTo(mapRef.current);
144+
markersRef.current = L.markerClusterGroup({
145+
showCoverageOnHover: false,
146+
maxClusterRadius: 50,
147+
spiderfyDistanceMultiplier: 1.6,
148+
iconCreateFunction: (cluster) => {
149+
const count = cluster.getChildCount();
150+
const size = count < 10 ? 36 : count < 50 ? 44 : 52;
151+
return L.divIcon({
152+
html: `<div class="congo-cluster-inner" style="width:${size}px;height:${size}px;">${count}</div>`,
153+
className: "congo-cluster",
154+
iconSize: L.point(size, size),
155+
});
156+
},
157+
}).addTo(mapRef.current);
158+
159+
// Leaflet mis-positions tiles and markers when its container is resized
160+
// while hidden (mobile list↔map toggle, sidebar, fullscreen), which throws
161+
// off centering. Recompute the size on any resize so flyTo lands correctly.
162+
const resizeObserver = new ResizeObserver(() => {
163+
mapRef.current?.invalidateSize();
164+
});
165+
resizeObserver.observe(containerRef.current);
126166

127167
onMapReady?.(mapRef.current);
128168

129169
return () => {
170+
resizeObserver.disconnect();
130171
mapRef.current?.remove();
131172
mapRef.current = null;
132173
};
@@ -199,21 +240,32 @@ export const CongoMap = React.memo(function CongoMap({ profiles, onProfileClick,
199240
}, [profiles, onProfileClick, focusedProfileId]);
200241

201242
useEffect(() => {
202-
if (!focusedProfileId || !mapRef.current || !markersMapRef.current) return;
243+
const map = mapRef.current;
244+
const cluster = markersRef.current;
245+
if (!focusedProfileId || !map || !cluster) return;
203246

204247
const marker = markersMapRef.current.get(focusedProfileId);
205-
if (marker) {
206-
mapRef.current.flyTo(marker.getLatLng(), 11, {
207-
animate: true,
208-
duration: 1.5,
209-
});
248+
if (!marker) return;
210249

211-
const timer = setTimeout(() => {
212-
marker.openPopup();
213-
}, 800);
250+
// The container may have just become visible (mobile/fullscreen toggle), so
251+
// make sure Leaflet knows its real size before computing the centering.
252+
map.invalidateSize();
214253

215-
return () => clearTimeout(timer);
216-
}
254+
const reveal = () => {
255+
// Profiles share a single city coordinate, so the focused one is usually
256+
// inside a cluster — zoomToShowLayer spiderfies it open, then we show its card.
257+
cluster.zoomToShowLayer(marker, () => marker.openPopup());
258+
};
259+
260+
map.flyTo(marker.getLatLng(), Math.max(map.getZoom(), 11), {
261+
animate: true,
262+
duration: 1.2,
263+
});
264+
map.once("moveend", reveal);
265+
266+
return () => {
267+
map.off("moveend", reveal);
268+
};
217269
}, [focusedProfileId]);
218270

219271
return (

0 commit comments

Comments
 (0)