This commit is contained in:
@@ -8,6 +8,13 @@ interface LocationPickerProps {
|
||||
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(() => {
|
||||
@@ -20,6 +27,10 @@ const LocationPicker: FC<LocationPickerProps> = ({ initialPosition, onLocationSe
|
||||
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) {
|
||||
@@ -27,6 +38,75 @@ const LocationPicker: FC<LocationPickerProps> = ({ initialPosition, onLocationSe
|
||||
}
|
||||
}, [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) {
|
||||
@@ -50,7 +130,36 @@ const LocationPicker: FC<LocationPickerProps> = ({ initialPosition, onLocationSe
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-[400px] rounded-xl overflow-hidden">
|
||||
<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}
|
||||
@@ -64,6 +173,7 @@ const LocationPicker: FC<LocationPickerProps> = ({ initialPosition, onLocationSe
|
||||
/>
|
||||
<LocationMarker />
|
||||
</MapContainer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user