diff --git a/client/src/features/address-map-picker/ui/AddressMapPicker.tsx b/client/src/features/address-map-picker/ui/AddressMapPicker.tsx index 2d46d42..5eeb5f8 100644 --- a/client/src/features/address-map-picker/ui/AddressMapPicker.tsx +++ b/client/src/features/address-map-picker/ui/AddressMapPicker.tsx @@ -1,30 +1,24 @@ import { useEffect, useMemo, useRef, useState } from 'react' -import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined' import Box from '@mui/material/Box' import Button from '@mui/material/Button' import CircularProgress from '@mui/material/CircularProgress' -import IconButton from '@mui/material/IconButton' import List from '@mui/material/List' import ListItemButton from '@mui/material/ListItemButton' import ListItemText from '@mui/material/ListItemText' import Stack from '@mui/material/Stack' import TextField from '@mui/material/TextField' -import Tooltip from '@mui/material/Tooltip' import Typography from '@mui/material/Typography' -import * as maplibregl from 'maplibre-gl' -import Map, { Marker, type MapMouseEvent, type MapRef } from 'react-map-gl/maplibre' import { reverseGeocode, searchPlaces } from '../api/map-geocoding' import type { LatLng, NominatimItem } from '../model/types' +import { MapPickerMap } from './MapPickerMap' export function AddressMapPicker(props: { value: { lat: number; lng: number } | null onChange: (v: { lat: number; lng: number; addressLine?: string | null }) => void }) { const { value, onChange } = props - const mapRef = useRef(null) const [q, setQ] = useState('') const [searching, setSearching] = useState(false) - const [locating, setLocating] = useState(false) const [results, setResults] = useState([]) const [hint, setHint] = useState(null) const abortRef = useRef(null) @@ -36,7 +30,7 @@ export function AddressMapPicker(props: { const center = useMemo(() => { if (value) return { lat: value.lat, lng: value.lng } - return { lat: 55.751244, lng: 37.618423 } // Москва (fallback) + return { lat: 55.751244, lng: 37.618423 } }, [value]) const pick = async (pos: LatLng) => { @@ -60,7 +54,6 @@ export function AddressMapPicker(props: { } const t = window.setTimeout(async () => { - // throttle: не чаще 1 запроса в 900ms const now = Date.now() if (now - lastRequestAtRef.current < 900) return if (s === lastQueryRef.current) return @@ -128,7 +121,6 @@ export function AddressMapPicker(props: { const lat = Number(r.lat) const lng = Number(r.lon) if (!Number.isFinite(lat) || !Number.isFinite(lng)) return - mapRef.current?.flyTo({ center: [lng, lat], zoom: 13, duration: 800 }) void pick({ lat, lng }) }} > @@ -138,103 +130,7 @@ export function AddressMapPicker(props: { )} - - { - const { lng, lat } = e.lngLat - void pick({ lat, lng }) - }} - > - {value && ( - { - const { lng, lat } = e.lngLat - void pick({ lat, lng }) - }} - anchor="bottom" - > - - - )} - - - - - { - if (!('geolocation' in navigator)) return - setLocating(true) - navigator.geolocation.getCurrentPosition( - (pos) => { - const lat = pos.coords.latitude - const lng = pos.coords.longitude - mapRef.current?.flyTo({ center: [lng, lat], zoom: 15, duration: 800 }) - void pick({ lat, lng }) - setLocating(false) - }, - () => { - setLocating(false) - }, - { enableHighAccuracy: true, timeout: 8000, maximumAge: 60_000 }, - ) - }} - sx={{ - position: 'absolute', - top: 10, - right: 10, - bgcolor: 'background.paper', - border: 1, - borderColor: 'divider', - boxShadow: 2, - '&:hover': { bgcolor: 'background.paper' }, - }} - aria-label="Моё местоположение" - > - {locating ? : } - - - - + {hint && ( diff --git a/client/src/features/address-map-picker/ui/MapPickerMap.tsx b/client/src/features/address-map-picker/ui/MapPickerMap.tsx new file mode 100644 index 0000000..e7e38eb --- /dev/null +++ b/client/src/features/address-map-picker/ui/MapPickerMap.tsx @@ -0,0 +1,178 @@ +import { useEffect, useRef, useState } from 'react' +import Box from '@mui/material/Box' +import CircularProgress from '@mui/material/CircularProgress' +import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined' +import IconButton from '@mui/material/IconButton' +import Tooltip from '@mui/material/Tooltip' +import Map, { Marker, type MapMouseEvent, type MapRef } from 'react-map-gl/maplibre' +import type * as maplibregl from 'maplibre-gl' +import { reverseGeocode } from '../api/map-geocoding' +import type { LatLng } from '../model/types' + +let maplibreglPromise: Promise | null = null + +function loadMaplibre() { + if (!maplibreglPromise) { + maplibreglPromise = Promise.all([ + import('maplibre-gl'), + import('maplibre-gl/dist/maplibre-gl.css'), + ]).then(([mod]) => mod) + } + return maplibreglPromise +} + +type MapPickerMapProps = { + value: { lat: number; lng: number } | null + onChange: (v: { lat: number; lng: number; addressLine?: string | null }) => void + center: { lat: number; lng: number } +} + +export function MapPickerMap({ value, onChange, center }: MapPickerMapProps) { + const mapRef = useRef(null) + const [maplibre, setMaplibre] = useState(null) + const [loading, setLoading] = useState(true) + const [locating, setLocating] = useState(false) + + useEffect(() => { + let cancelled = false + loadMaplibre().then((mod) => { + if (!cancelled) { + setMaplibre(mod) + setLoading(false) + } + }) + return () => { + cancelled = true + } + }, []) + + const pick = async (pos: LatLng) => { + onChange({ lat: pos.lat, lng: pos.lng }) + try { + const addr = await reverseGeocode(pos) + if (addr) { + onChange({ lat: pos.lat, lng: pos.lng, addressLine: addr }) + } + } catch { + // ignore + } + } + + if (loading || !maplibre) { + return ( + + + + ) + } + + return ( + + { + const { lng, lat } = e.lngLat + void pick({ lat, lng }) + }} + > + {value && ( + { + const { lng, lat } = e.lngLat + void pick({ lat, lng }) + }} + anchor="bottom" + > + + + )} + + + + + { + if (!('geolocation' in navigator)) return + setLocating(true) + navigator.geolocation.getCurrentPosition( + (pos) => { + const lat = pos.coords.latitude + const lng = pos.coords.longitude + mapRef.current?.flyTo({ center: [lng, lat], zoom: 15, duration: 800 }) + void pick({ lat, lng }) + setLocating(false) + }, + () => { + setLocating(false) + }, + { enableHighAccuracy: true, timeout: 8000, maximumAge: 60_000 }, + ) + }} + sx={{ + position: 'absolute', + top: 10, + right: 10, + bgcolor: 'background.paper', + border: 1, + borderColor: 'divider', + boxShadow: 2, + '&:hover': { bgcolor: 'background.paper' }, + }} + aria-label="Моё местоположение" + > + {locating ? : } + + + + + ) +} diff --git a/client/src/main.tsx b/client/src/main.tsx index 8802b4e..7e48bfd 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -2,7 +2,6 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { App } from '@/app/App' import '@/app/styles/global.css' -import 'maplibre-gl/dist/maplibre-gl.css' import { readStoredToken, tokenSet } from '@/shared/model/auth' tokenSet(readStoredToken())