added post modal for feed on map

This commit is contained in:
SlimusMinus
2026-08-27 01:30:03 +03:00
parent 69e929efbf
commit 408a97e5b2
11 changed files with 512 additions and 27 deletions

178
src/components/MapView.jsx Normal file
View File

@@ -0,0 +1,178 @@
import { useEffect, useRef, useState } from "react";
import { API_URL, getImageUrl } from "../config/authConstants";
const MARKER_COLORS = {
LOST: "#ff3b30",
LOSS: "#ff3b30",
FOUND: "#34c759",
};
function createMarkerLayout(color, imageUrl) {
const bgStyle = imageUrl
? `background-image:url('${imageUrl}');background-size:cover;background-position:center;`
: "";
return window.ymaps.templateLayoutFactory.createClass(
`<div style="
width:40px;height:40px;
border-radius:50%;
border:3px solid ${color};
box-shadow:0 2px 8px rgba(0,0,0,.35);
overflow:hidden;
background:${imageUrl ? "transparent" : color};
display:flex;align-items:center;justify-content:center;
cursor:pointer;
">
<div style="
width:100%;height:100%;border-radius:50%;
${bgStyle}
"></div>
</div>`,
{
build() {
this.constructor.superclass.build.call(this);
},
}
);
}
const FOCUS_ZOOM = 16;
const COORD_MATCH_TOLERANCE = 0.005;
export default function MapView({ focusPostId, focusLat, focusLng, onPostClick }) {
const mapRef = useRef(null);
const ymapRef = useRef(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const onPostClickRef = useRef(onPostClick);
onPostClickRef.current = onPostClick;
useEffect(() => {
let map;
const destroyed = { current: false };
const init = async () => {
if (!mapRef.current || destroyed.current) return;
try {
const res = await fetch(`${API_URL}/api/v1/posts/map`);
if (!res.ok) throw new Error("Failed to fetch map markers");
const markers = await res.json();
if (destroyed.current) return;
map = new window.ymaps.Map(mapRef.current, {
center: [55.751574, 37.573856],
zoom: 11,
controls: ["zoomControl", "geolocationControl"],
});
ymapRef.current = map;
const placemarksById = new Map();
markers.forEach((m) => {
if (m.latitude == null || m.longitude == null) return;
const color = MARKER_COLORS[m.type] || MARKER_COLORS.LOST;
const typeLabel = m.type === "FOUND" ? "Находка" : "Потеря";
const imgSrc = m.imageUrl ? getImageUrl(m.imageUrl) : null;
const iconLayout = createMarkerLayout(color, imgSrc);
const coords = [m.latitude, m.longitude];
const placemark = new window.ymaps.Placemark(
coords,
{},
{
iconLayout,
iconImageSize: [40, 40],
iconImageOffset: [-20, -20],
// Без явного iconShape карта не может корректно вычислить
// кликабельную область для кастомного HTML-лейаута —
// маркер рисуется, но клики по нему не срабатывают.
iconShape: {
type: "Circle",
coordinates: [20, 20],
radius: 20,
},
hasBalloon: false,
hasHint: true,
hintContent: m.title || typeLabel,
}
);
map.geoObjects.add(placemark);
if (m.id != null) {
const postId = String(m.id);
placemarksById.set(postId, { placemark, coords });
placemark.events.add("click", () => {
if (onPostClickRef.current) {
onPostClickRef.current(postId);
}
});
}
});
let target = focusPostId != null
? placemarksById.get(String(focusPostId))
: null;
if (!target && Number.isFinite(focusLat) && Number.isFinite(focusLng)) {
let best = null;
let bestDist = Infinity;
placemarksById.forEach((entry) => {
const d =
Math.pow(entry.coords[0] - focusLat, 2) +
Math.pow(entry.coords[1] - focusLng, 2);
if (d < bestDist) {
bestDist = d;
best = entry;
}
});
if (best && bestDist < COORD_MATCH_TOLERANCE * COORD_MATCH_TOLERANCE) {
target = best;
}
}
if (target) {
map.setCenter(target.coords, FOCUS_ZOOM);
} else if (placemarksById.size > 0) {
map.setBounds(
map.geoObjects.getBounds(),
{ checkZoomRange: true, zoomMargin: 40 }
);
}
} catch (e) {
if (!destroyed.current) setError(e.message);
} finally {
if (!destroyed.current) setLoading(false);
}
};
if (window.ymaps?.ready) {
window.ymaps.ready(init);
}
return () => {
destroyed.current = true;
ymapRef.current?.destroy();
ymapRef.current = null;
};
}, [focusPostId, focusLat, focusLng]);
return (
<div className="f-mapview">
{loading && (
<div className="f-mapview-status">
<div className="f-spinner" />
</div>
)}
{error && (
<div className="f-mapview-status f-mapview-error">
Ошибка загрузки карты
</div>
)}
<div ref={mapRef} className="f-mapview-canvas" />
</div>
);
}