a587736dd3
- Mobile responsiveness improvements across admin and public sections - Admin: bookings modal, open-day page, team page, layout polish - Added rate limiting, CSRF hardening, auth-edge improvements - Scroll reveal, floating contact, back-to-top, Yandex map fixes - Schedule filters refactor, team profile/info component updates - New useTrainerPhotos hook - Added class, team, master-class, and news images
89 lines
2.6 KiB
TypeScript
89 lines
2.6 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
|
||
interface YandexMapProps {
|
||
addresses: string[];
|
||
height?: number;
|
||
}
|
||
|
||
function cleanAddress(addr: string): string {
|
||
return addr
|
||
.replace(/^г\.\s*/i, "")
|
||
.replace(/ул\.\s*/gi, "")
|
||
.replace(/пр-т\.?\s*/gi, "")
|
||
.replace(/пр\.\s*/gi, "")
|
||
.replace(/просп\.\s*/gi, "")
|
||
.replace(/,?\s*к\d+/gi, "")
|
||
.replace(/,+/g, " ")
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
}
|
||
|
||
export function YandexMap({ addresses, height = 380 }: YandexMapProps) {
|
||
const [mapSrc, setMapSrc] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
if (!addresses.length) return;
|
||
let cancelled = false;
|
||
|
||
async function build() {
|
||
// Geocode all addresses in parallel
|
||
const results = await Promise.allSettled(
|
||
addresses.map(async (addr) => {
|
||
const cleaned = cleanAddress(addr);
|
||
const query = cleaned.toLowerCase().includes("минск") ? cleaned : `Минск ${cleaned}`;
|
||
const res = await fetch(
|
||
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=1&countrycodes=by`,
|
||
{ signal: AbortSignal.timeout(5000) }
|
||
);
|
||
const data = await res.json();
|
||
if (data.length > 0) {
|
||
return { lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon) };
|
||
}
|
||
return null;
|
||
})
|
||
);
|
||
|
||
const points = results
|
||
.filter((r): r is PromiseFulfilledResult<{ lat: number; lon: number } | null> => r.status === "fulfilled")
|
||
.map((r) => r.value)
|
||
.filter((p): p is { lat: number; lon: number } => p !== null);
|
||
|
||
if (cancelled || points.length === 0) return;
|
||
|
||
const centerLat = points.reduce((s, p) => s + p.lat, 0) / points.length;
|
||
const centerLon = points.reduce((s, p) => s + p.lon, 0) / points.length;
|
||
const zoom = points.length === 1 ? 15 : 12;
|
||
|
||
const pts = points.map((p) => `${p.lon},${p.lat},pm2ntl`).join("~");
|
||
setMapSrc(`https://yandex.ru/map-widget/v1/?ll=${centerLon},${centerLat}&z=${zoom}&pt=${pts}&l=map&theme=dark`);
|
||
}
|
||
|
||
build();
|
||
return () => { cancelled = true; };
|
||
}, [addresses]);
|
||
|
||
if (!addresses.length) return null;
|
||
|
||
if (!mapSrc) {
|
||
return (
|
||
<div style={{ width: "100%", height }} className="flex items-center justify-center text-neutral-500 text-sm">
|
||
Загрузка карты...
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<iframe
|
||
src={mapSrc}
|
||
width="100%"
|
||
height={height}
|
||
style={{ border: 0 }}
|
||
allowFullScreen
|
||
loading="lazy"
|
||
title="Карта"
|
||
/>
|
||
);
|
||
}
|