Przeglądaj źródła

fix: graceful fallback for Yandex Maps init failure + add geocoder API key

neyrogovnarik 1 miesiąc temu
rodzic
commit
bf8b04517b

+ 1 - 0
deploy/docker-compose.prod.yml

@@ -124,6 +124,7 @@ services:
         NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
         NEXT_PUBLIC_MAP_PROVIDER: yandex
         NEXT_PUBLIC_YANDEX_MAPS_API_KEY: ${YANDEX_MAPS_API_KEY}
+        NEXT_PUBLIC_YANDEX_GEOCODER_API_KEY: ${YANDEX_GEOCODER_API_KEY}
     depends_on:
       backend:
         condition: service_healthy

+ 3 - 0
deploy/env.prod

@@ -18,6 +18,9 @@ S3_SECRET_KEY=CHANGE_ME_MINIO_PASSWORD
 CLOUDPAYMENTS_PUBLIC_ID=
 CLOUDPAYMENTS_API_SECRET=
 
+# Yandex Geocoder (отдельный ключ для HTTP Геокодера)
+YANDEX_GEOCODER_API_KEY=d72d4331-2d3f-4c77-aa29-ef005896af00
+
 # Фронтенд
 NEXT_PUBLIC_API_URL=https://api.photoplaces.ru/api/v1
 NEXT_PUBLIC_MAP_PROVIDER=yandex

+ 2 - 0
frontend/Dockerfile

@@ -3,9 +3,11 @@ FROM node:20-alpine AS builder
 ARG NEXT_PUBLIC_API_URL
 ARG NEXT_PUBLIC_MAP_PROVIDER
 ARG NEXT_PUBLIC_YANDEX_MAPS_API_KEY
+ARG NEXT_PUBLIC_YANDEX_GEOCODER_API_KEY
 ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
 ENV NEXT_PUBLIC_MAP_PROVIDER=$NEXT_PUBLIC_MAP_PROVIDER
 ENV NEXT_PUBLIC_YANDEX_MAPS_API_KEY=$NEXT_PUBLIC_YANDEX_MAPS_API_KEY
+ENV NEXT_PUBLIC_YANDEX_GEOCODER_API_KEY=$NEXT_PUBLIC_YANDEX_GEOCODER_API_KEY
 
 WORKDIR /app
 COPY package.json package-lock.json* ./

+ 31 - 14
frontend/src/components/MapView.tsx

@@ -19,6 +19,7 @@ export default function MapView() {
   const { user, isLoading } = useAuth()
   const [places, setPlaces] = useState<Place[]>([])
   const [selectedPlace, setSelectedPlace] = useState<Place | null>(null)
+  const [mapError, setMapError] = useState(false)
 
   const fetchPlaces = useCallback(async (bounds?: MapBounds) => {
     if (!user) return
@@ -45,18 +46,26 @@ export default function MapView() {
     if (!containerRef.current || mapRef.current) return
 
     const provider = createYandexMapProvider()
-    provider.init(containerRef.current, [37.6173, 55.7558], 10)
     mapRef.current = provider
 
-    if (navigator.geolocation) {
-      navigator.geolocation.getCurrentPosition(
-        (pos) => provider.setCenter(pos.coords.latitude, pos.coords.longitude),
-        () => {},
-      )
-    }
+    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.onMove((center, zoom, bounds) => {
-      fetchPlacesRef.current(bounds)
+      provider.onMove((center, zoom, bounds) => {
+        fetchPlacesRef.current(bounds)
+      })
+    }).catch(() => {
+      setMapError(true)
     })
 
     return () => {
@@ -66,9 +75,8 @@ export default function MapView() {
   }, [])
 
   useEffect(() => {
-    if (!mapRef.current) return
-
     const provider = mapRef.current
+    if (!provider || !provider.isReady()) return
 
     places.forEach((place) => {
       provider.addMarker(place.id, place.lat, place.lng, {
@@ -86,8 +94,8 @@ export default function MapView() {
   const { visitors } = useWebSocket(!user)
 
   useEffect(() => {
-    if (!mapRef.current) return
     const provider = mapRef.current
+    if (!provider || !provider.isReady()) return
 
     visitors.forEach((v, i) => {
       provider.addMarker(`ws-visitor-${i}`, v.lat, v.lng, { type: 'visitor' })
@@ -100,9 +108,18 @@ export default function MapView() {
 
   return (
     <div className="relative h-full w-full">
-      <div ref={containerRef} className="h-full w-full" />
+      {mapError ? (
+        <div className="flex h-full w-full items-center justify-center">
+          <div className="text-center">
+            <p className="mb-2 text-lg text-white/60">Карта временно недоступна</p>
+            <p className="text-sm text-white/40">Попробуйте обновить страницу позже</p>
+          </div>
+        </div>
+      ) : (
+        <div ref={containerRef} className="h-full w-full" />
+      )}
 
-      {!user && !isLoading && (
+      {!user && !isLoading && !mapError && (
         <div className="absolute left-1/2 top-4 z-10 -translate-x-1/2 rounded-lg bg-white/10 px-6 py-3 text-center text-white backdrop-blur-md">
           <p className="text-sm">Зарегистрируйтесь, чтобы исследовать места для фотосъёмок</p>
         </div>

+ 25 - 8
frontend/src/lib/map.ts

@@ -13,9 +13,11 @@ declare global {
  */
 export interface MapProvider {
   /** Инициализирует карту в указанном контейнере */
-  init(container: HTMLElement, center: [number, number], zoom: number): void
+  init(container: HTMLElement, center: [number, number], zoom: number): Promise<void>
   /** Уничтожает карту и очищает ресурсы */
   destroy(): void
+  /** Проверяет, готова ли карта к использованию */
+  isReady(): boolean
   /** Добавляет маркер на карту */
   addMarker(id: string, lat: number, lng: number, options?: MarkerOptions): void
   /** Удаляет маркер с карты по идентификатору */
@@ -64,13 +66,19 @@ export function createYandexMapProvider(): MapProvider {
   let ymaps3: any = null
   let markers = new Map<string, any>()
   let moveHandler: ((center: [number, number], zoom: number, bounds: MapBounds) => void) | null = null
+  let initError: string | null = null
 
   return {
     async init(container, center, zoom) {
-      await loadYandexScript()
-
-      ymaps3 = await (window as any).ymaps3.ready
-      await ymaps3.import('@yandex/ymaps3-markers@0.0.1')
+      try {
+        await loadYandexScript()
+
+        ymaps3 = await (window as any).ymaps3.ready
+        await ymaps3.import('@yandex/ymaps3-markers@0.0.1')
+      } catch (e) {
+        initError = 'Failed to load Yandex Maps'
+        return
+      }
 
       map = new ymaps3.YMap(container, {
         location: { center, zoom },
@@ -109,13 +117,17 @@ export function createYandexMapProvider(): MapProvider {
       updateBounds()
     },
 
+    isReady() {
+      return map !== null
+    },
+
     destroy() {
       markers.clear()
       map?.destroy()
     },
 
     addMarker(id, lat, lng, options) {
-      if (!ymaps3) return
+      if (!ymaps3 || !map) return
 
       const color = options?.type === 'visitor' ? '#22c55e'
         : options?.type === 'studio' ? '#a855f7'
@@ -187,12 +199,17 @@ async function loadYandexScript(): Promise<void> {
   if (document.querySelector('script[src*="yandexmaps"]')) return
 
   const apiKey = process.env.NEXT_PUBLIC_YANDEX_MAPS_API_KEY
-  return new Promise((resolve, reject) => {
+  if (!apiKey) return
+
+  return new Promise((resolve) => {
     const script = document.createElement('script')
     script.src = `https://api-maps.yandex.ru/v3/?apikey=${apiKey}&lang=ru_RU`
     script.async = true
     script.onload = () => resolve()
-    script.onerror = () => reject(new Error('Failed to load Yandex Maps'))
+    script.onerror = () => {
+      console.warn('Yandex Maps script failed to load')
+      resolve()
+    }
     document.head.appendChild(script)
   })
 }