// MapView — MapLibre GL with custom DOM markers.
// We render markers as React-controlled DOM nodes attached via maplibregl.Marker
// so hover/active states sync with the listing list.
const { useEffect, useRef, useState, useMemo } = React;
const MAP_STYLES = {
warm: {
version: 8,
sources: {
"osm": {
type: "raster",
tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
tileSize: 256,
attribution: "© OpenStreetMap contributors",
maxzoom: 19,
},
},
layers: [
{ id: "bg", type: "background", paint: { "background-color": "#FAF7F2" } },
{
id: "osm",
type: "raster",
source: "osm",
paint: { "raster-opacity": 0.55, "raster-saturation": -0.7, "raster-contrast": -0.1, "raster-brightness-min": 0.15 },
},
],
},
muted: {
version: 8,
sources: {
"osm": {
type: "raster",
tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
tileSize: 256,
attribution: "© OpenStreetMap contributors",
maxzoom: 19,
},
},
layers: [
{ id: "bg", type: "background", paint: { "background-color": "#EEE9E0" } },
{
id: "osm",
type: "raster",
source: "osm",
paint: { "raster-opacity": 0.35, "raster-saturation": -1, "raster-contrast": 0.1, "raster-brightness-min": 0.2 },
},
],
},
stylized: null, // handled separately as SVG
};
function MapView({ listings, activeId, hoverId, onMarkerClick, onMarkerHover, mapStyle, accent }) {
const containerRef = useRef(null);
const mapRef = useRef(null);
const markersRef = useRef({});
const [ready, setReady] = useState(false);
// Init map
useEffect(() => {
if (mapStyle === "stylized") return; // handled by SVG fallback
if (!containerRef.current || mapRef.current) return;
const map = new maplibregl.Map({
container: containerRef.current,
style: MAP_STYLES[mapStyle] || MAP_STYLES.warm,
center: [37.62, 55.752],
zoom: 10.5,
attributionControl: false,
cooperativeGestures: false,
});
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), "top-right");
map.on("load", () => setReady(true));
mapRef.current = map;
return () => {
map.remove();
mapRef.current = null;
markersRef.current = {};
setReady(false);
};
}, [mapStyle]);
// Manage markers
useEffect(() => {
const map = mapRef.current;
if (!map || !ready) return;
// Remove old
Object.values(markersRef.current).forEach((m) => m.remove());
markersRef.current = {};
listings.forEach((l) => {
const el = document.createElement("button");
el.className = "map-marker";
el.dataset.id = l.id;
el.innerHTML = `
${formatPriceShort(l.price)}
`;
el.addEventListener("click", (e) => {
e.stopPropagation();
onMarkerClick && onMarkerClick(l.id);
});
el.addEventListener("mouseenter", () => onMarkerHover && onMarkerHover(l.id));
el.addEventListener("mouseleave", () => onMarkerHover && onMarkerHover(null));
const marker = new maplibregl.Marker({ element: el, anchor: "bottom" })
.setLngLat([l.lon, l.lat])
.addTo(map);
markersRef.current[l.id] = { marker, el };
});
// Fit bounds
if (listings.length > 0) {
const bounds = new maplibregl.LngLatBounds();
listings.forEach((l) => bounds.extend([l.lon, l.lat]));
map.fitBounds(bounds, {
padding: { top: 80, bottom: 80, left: 80, right: 80 },
maxZoom: 14,
duration: 800,
});
} else {
map.flyTo({ center: [37.62, 55.752], zoom: 10.5, duration: 800 });
}
}, [listings, ready]);
// Sync active/hover state on existing markers
useEffect(() => {
Object.entries(markersRef.current).forEach(([id, { el }]) => {
el.classList.toggle("is-active", id === activeId);
el.classList.toggle("is-hover", id === hoverId);
});
}, [activeId, hoverId, listings]);
// Stylized SVG fallback
if (mapStyle === "stylized") {
return (