feat: contact page improvements, Yandex map from addresses

- Instagram field: @username input with API validation (like team page)
- Phone validation: blocks auto-save when incomplete, shows warning
- SectionEditor: validate prop to conditionally block saves
- Yandex Map: auto-generated from addresses via Nominatim geocoding,
  dark theme, no API key needed
- Schedule: address hint linking to Contacts
- Renamed "Всплывающие окна" → "Формы записи", moved after Записи
This commit is contained in:
2026-03-30 16:59:24 +03:00
parent 22bd117dae
commit 06be6b48ce
7 changed files with 191 additions and 60 deletions
+82
View File
@@ -0,0 +1,82 @@
"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() {
const points: { lat: number; lon: number }[] = [];
for (const addr of addresses) {
try {
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`
);
const data = await res.json();
if (data.length > 0) {
points.push({ lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon) });
}
} catch { /* skip */ }
}
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="Карта"
/>
);
}