// 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 ( ); } return
; } function formatPriceShort(rub) { const mln = rub / 1_000_000; return mln.toFixed(mln < 10 ? 1 : 0).replace(".", ",") + " млн"; } // --- Stylized SVG map (no tiles) ------------------------------------------- function StylizedMap({ listings, activeId, hoverId, onMarkerClick, onMarkerHover }) { // Project lon/lat to SVG coords using a fixed Moscow-area window. const W = 800, H = 600; const lonMin = 37.45, lonMax = 37.78, latMin = 55.65, latMax = 55.83; const project = (lon, lat) => ({ x: ((lon - lonMin) / (lonMax - lonMin)) * W, y: H - ((lat - latMin) / (latMax - latMin)) * H, }); return (
{/* Moscow river — abstract curved band */} {/* Garden Ring (abstract) */} {/* Park blobs */} Сокольники Лосиный остров Парк Горького {/* Center label */} КРЕМЛЬ
{listings.map((l) => { const { x, y } = project(l.lon, l.lat); const isActive = l.id === activeId; const isHover = l.id === hoverId; return ( ); })}
); } Object.assign(window, { MapView });