This commit is contained in:
@@ -8,6 +8,13 @@ interface LocationPickerProps {
|
|||||||
onLocationSelect: (lat: number, lng: number) => void
|
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 ChangeView = ({ center, zoom }: { center: LatLngTuple; zoom: number }) => {
|
||||||
const map = useMap()
|
const map = useMap()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -20,6 +27,10 @@ const LocationPicker: FC<LocationPickerProps> = ({ initialPosition, onLocationSe
|
|||||||
const [position, setPosition] = useState<LatLngTuple>(
|
const [position, setPosition] = useState<LatLngTuple>(
|
||||||
initialPosition || [35.6892, 51.3890] // تهران به عنوان پیشفرض
|
initialPosition || [35.6892, 51.3890] // تهران به عنوان پیشفرض
|
||||||
)
|
)
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [searchResults, setSearchResults] = useState<NominatimSearchResult[]>([])
|
||||||
|
const [isSearching, setIsSearching] = useState(false)
|
||||||
|
const [searchError, setSearchError] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialPosition) {
|
if (initialPosition) {
|
||||||
@@ -27,6 +38,75 @@ const LocationPicker: FC<LocationPickerProps> = ({ initialPosition, onLocationSe
|
|||||||
}
|
}
|
||||||
}, [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 = () => {
|
const LocationMarker = () => {
|
||||||
useMapEvents({
|
useMapEvents({
|
||||||
click(e) {
|
click(e) {
|
||||||
@@ -50,7 +130,36 @@ const LocationPicker: FC<LocationPickerProps> = ({ initialPosition, onLocationSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<MapContainer
|
||||||
center={position}
|
center={position}
|
||||||
zoom={13}
|
zoom={13}
|
||||||
@@ -64,6 +173,7 @@ const LocationPicker: FC<LocationPickerProps> = ({ initialPosition, onLocationSe
|
|||||||
/>
|
/>
|
||||||
<LocationMarker />
|
<LocationMarker />
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user