'use client' import { useEffect, useRef, useState, useCallback } from 'react' import { createLeafletMapProvider, type MapProvider, type MapBounds } from '@/lib/map' import { useAuth } from '@/hooks/useAuth' import { useWebSocket } from '@/hooks/useWebSocket' import type { Place } from '@/types' import { api } from '@/lib/api' /** * Основной компонент карты. Инициализирует Яндекс.Карту, загружает места, * отображает маркеры и показывает карточку места при клике. * Для неавторизованных пользователей через WebSocket показывает точки других посетителей на карте. */ export default function MapView() { const containerRef = useRef(null) const mapRef = useRef(null) const { user, isLoading } = useAuth() const [places, setPlaces] = useState([]) const [selectedPlace, setSelectedPlace] = useState(null) const [mapError, setMapError] = useState(false) const fetchPlaces = useCallback(async (bounds?: MapBounds) => { try { const params = new URLSearchParams() if (bounds) { params.set('bounds', `${bounds.swLat},${bounds.swLng},${bounds.neLat},${bounds.neLng}`) } params.set('limit', '50') const res = await api.get<{ data: Place[] }>(`/places?${params}`) setPlaces(res.data) } catch { // Ошибка загрузки мест — карта продолжает работать с текущими данными } }, []) /** Реф для актуальной версии fetchPlaces — предотвращает проблему устаревшего замыкания */ const fetchPlacesRef = useRef(fetchPlaces) fetchPlacesRef.current = fetchPlaces useEffect(() => { if (!containerRef.current || mapRef.current) return const provider = createLeafletMapProvider() mapRef.current = provider provider.init(containerRef.current, [37.6173, 55.7558], 10).then(() => { if (!provider.isReady()) { setMapError(true) return } if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (pos) => { provider.setCenter(pos.coords.latitude, pos.coords.longitude) provider.addMarker('_user_location', pos.coords.latitude, pos.coords.longitude, { type: 'location', }) }, () => {}, ) } provider.onMove((center, zoom, bounds) => { fetchPlacesRef.current(bounds) }) const b = provider.getBounds?.() if (b) fetchPlaces(b) }).catch(() => { setMapError(true) }) return () => { provider.destroy() mapRef.current = null } }, []) useEffect(() => { const provider = mapRef.current if (!provider || !provider.isReady()) return const isGuest = !user places.forEach((place) => { provider.addMarker(place.id, place.lat, place.lng, { type: place.type as 'place' | 'studio', title: isGuest ? undefined : place.title, onClick: isGuest ? undefined : () => setSelectedPlace(place), ghost: isGuest, }) }) return () => { places.forEach((place) => provider.removeMarker(place.id)) } }, [places]) const { visitors } = useWebSocket(!user) useEffect(() => { const provider = mapRef.current if (!provider || !provider.isReady()) return visitors.forEach((v, i) => { provider.addMarker(`ws-visitor-${i}`, v.lat, v.lng, { type: 'visitor' }) }) return () => { visitors.forEach((_, i) => provider.removeMarker(`ws-visitor-${i}`)) } }, [visitors]) return (
{mapError ? (

Карта временно недоступна

Попробуйте обновить страницу позже

) : (
)} {!user && !isLoading && !mapError && (

Зарегистрируйтесь, чтобы исследовать места для фотосъёмок

)} {selectedPlace && ( setSelectedPlace(null)} /> )}
) } /** * Модальная карточка места, отображаемая в нижней части экрана. * Показывает название, адрес, описание, теги и рейтинг места. * @param place - Данные выбранного места * @param onClose - Функция закрытия карточки */ function PlaceCardModal({ place, onClose }: { place: Place; onClose: () => void }) { return (

{place.title}

{place.address && (

{place.address}

)} {place.description && (

{place.description}

)}
{place.tags?.map((tag) => ( {tag.name} ))}
★ {place.rating?.toFixed(1) || 'Нет оценок'} {place.type === 'studio' && place.pricing?.hourly_rate && ( {place.pricing.hourly_rate} ₽/час )}
) }