Files
dmenu-admin/src/pages/settings/components/LocationPicker.tsx
T
hamid zarghami 67dfc03c7b
deploy to danak / build_and_deploy (push) Has been cancelled
search in maps
2026-06-23 12:38:23 +03:30

183 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { type FC, useState, useEffect } from 'react'
import { MapContainer, TileLayer, Marker, useMapEvents, useMap } from 'react-leaflet'
import { Icon } from 'leaflet'
import type { LatLngTuple } from 'leaflet'
interface LocationPickerProps {
initialPosition?: [number, number]
onLocationSelect: (lat: number, lng: number) => void
}
interface NominatimSearchResult {
place_id: number
display_name: string
lat: string
lon: string
}
const ChangeView = ({ center, zoom }: { center: LatLngTuple; zoom: number }) => {
const map = useMap()
useEffect(() => {
map.setView(center, zoom)
}, [map, center, zoom])
return null
}
const LocationPicker: FC<LocationPickerProps> = ({ initialPosition, onLocationSelect }) => {
const [position, setPosition] = useState<LatLngTuple>(
initialPosition || [35.6892, 51.3890] // تهران به عنوان پیش‌فرض
)
const [searchQuery, setSearchQuery] = useState('')
const [searchResults, setSearchResults] = useState<NominatimSearchResult[]>([])
const [isSearching, setIsSearching] = useState(false)
const [searchError, setSearchError] = useState('')
useEffect(() => {
if (initialPosition) {
setPosition(initialPosition)
}
}, [initialPosition])
useEffect(() => {
const query = searchQuery.trim()
if (!query) {
setSearchResults([])
setSearchError('')
setIsSearching(false)
return
}
const controller = new AbortController()
const timer = window.setTimeout(async () => {
setIsSearching(true)
setSearchError('')
try {
const params = new URLSearchParams({
q: query,
format: 'json',
addressdetails: '1',
limit: '5',
})
const response = await fetch(
`https://nominatim.openstreetmap.org/search?${params.toString()}`,
{
headers: {
Accept: 'application/json',
},
signal: controller.signal,
}
)
if (!response.ok) {
throw new Error('search request failed')
}
const data = (await response.json()) as NominatimSearchResult[]
if (!data.length) {
setSearchResults([])
setSearchError('نتیجه‌ای پیدا نشد')
return
}
setSearchResults(data)
} catch (error) {
if ((error as DOMException).name === 'AbortError') {
return
}
setSearchResults([])
setSearchError('خطا در جستجوی موقعیت')
} finally {
setIsSearching(false)
}
}, 500)
return () => {
controller.abort()
window.clearTimeout(timer)
}
}, [searchQuery])
const handleSelectSearchResult = (result: NominatimSearchResult) => {
const lat = Number(result.lat)
const lng = Number(result.lon)
const newPosition: LatLngTuple = [lat, lng]
setPosition(newPosition)
}
const LocationMarker = () => {
useMapEvents({
click(e) {
const newPosition: LatLngTuple = [e.latlng.lat, e.latlng.lng]
setPosition(newPosition)
onLocationSelect(newPosition[0], newPosition[1])
},
})
const customIcon = new Icon({
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41],
})
return position ? <Marker position={position} icon={customIcon} /> : null
}
return (
<div className='w-full'>
<div className='mb-3'>
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder='جستجوی آدرس یا مکان'
className='w-full h-10 border border-border rounded-xl px-3 text-sm outline-none focus:border-primary'
/>
</div>
{isSearching && <div className='text-xs text-description mb-2'>در حال جستجو...</div>}
{searchError && <div className='text-xs text-red-500 mb-2'>{searchError}</div>}
{searchResults.length > 0 && (
<div className='mb-3 border border-border rounded-xl max-h-32 overflow-auto'>
{searchResults.map((result) => (
<button
key={result.place_id}
type='button'
onClick={() => handleSelectSearchResult(result)}
className='w-full text-right px-3 py-2 text-xs hover:bg-gray-50 border-b border-border last:border-b-0'
>
{result.display_name}
</button>
))}
</div>
)}
<div className='w-full h-[400px] rounded-xl overflow-hidden'>
<MapContainer
center={position}
zoom={13}
style={{ height: '100%', width: '100%' }}
scrollWheelZoom={true}
>
<ChangeView center={position} zoom={13} />
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<LocationMarker />
</MapContainer>
</div>
</div>
)
}
export default LocationPicker