Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
132 lines
4.0 KiB
TypeScript
132 lines
4.0 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
|
|
import MarkerIcon from '@/components/icons/MarkerIcon'
|
|
import { getMapirMapComponent, MAPIR_STYLE_URL } from '@/components/map/mapirMapFactory'
|
|
import { getPublicMapApiKey, toMapirLngLat } from '@/helpers/mapir'
|
|
|
|
interface MapirLocationPickerProps {
|
|
centerLat: number
|
|
centerLng: number
|
|
markerLat?: number | null
|
|
markerLng?: number | null
|
|
zoom?: number
|
|
scrollZoom?: boolean
|
|
height?: number | string
|
|
onLocationPick: (lat: number, lng: number) => void
|
|
}
|
|
|
|
interface MapLike {
|
|
resize: () => void
|
|
jumpTo: (opts: { center: [number, number] }) => void
|
|
}
|
|
|
|
const DEFAULT_ZOOM = 13
|
|
|
|
const MapirLocationPicker = ({
|
|
centerLat,
|
|
centerLng,
|
|
markerLat,
|
|
markerLng,
|
|
zoom = DEFAULT_ZOOM,
|
|
scrollZoom = true,
|
|
height = '100%',
|
|
onLocationPick,
|
|
}: MapirLocationPickerProps) => {
|
|
const apiKey = getPublicMapApiKey()
|
|
const Map = useMemo(() => {
|
|
const k = apiKey.trim()
|
|
|
|
if (!k) return null
|
|
|
|
return getMapirMapComponent(k)
|
|
}, [apiKey])
|
|
|
|
// اگر مارکر انتخاب شده، همان نقطه مرکز نقشه باشد تا پین وسط روی موقعیت درست بنشیند
|
|
const viewLat = markerLat != null && Number.isFinite(markerLat) ? markerLat : centerLat
|
|
const viewLng = markerLng != null && Number.isFinite(markerLng) ? markerLng : centerLng
|
|
const center = useMemo(() => toMapirLngLat(viewLat, viewLng), [viewLat, viewLng])
|
|
const zoomArr = useMemo(() => [zoom] as [number], [zoom])
|
|
const mapRef = useRef<MapLike | null>(null)
|
|
const onPickRef = useRef(onLocationPick)
|
|
const rootRef = useRef<HTMLDivElement | null>(null)
|
|
const hasSelection = markerLat != null && markerLng != null && Number.isFinite(markerLat) && Number.isFinite(markerLng)
|
|
|
|
useEffect(() => {
|
|
onPickRef.current = onLocationPick
|
|
}, [onLocationPick])
|
|
|
|
useEffect(() => {
|
|
const root = rootRef.current
|
|
|
|
if (!root || typeof ResizeObserver === 'undefined') return
|
|
|
|
const resize = () => {
|
|
mapRef.current?.resize()
|
|
}
|
|
const observer = new ResizeObserver(resize)
|
|
|
|
observer.observe(root)
|
|
const timer = window.setTimeout(resize, 320)
|
|
|
|
return () => {
|
|
observer.disconnect()
|
|
window.clearTimeout(timer)
|
|
}
|
|
}, [])
|
|
|
|
const onStyleLoad = useCallback((map: MapLike) => {
|
|
mapRef.current = map
|
|
map.resize()
|
|
}, [])
|
|
|
|
// mapir-react listener اول onClick را نگه میدارد؛ pick را از ref میخوانیم
|
|
const onClick = useCallback((_map: MapLike, evt: { lngLat?: { lat: number; lng: number } }) => {
|
|
const ll = evt?.lngLat
|
|
|
|
if (!ll) return
|
|
|
|
// پین CSS همیشه وسط است؛ با jumpTo نقطهٔ کلیک زیر پین میآید
|
|
mapRef.current?.jumpTo({ center: [ll.lng, ll.lat] })
|
|
onPickRef.current(ll.lat, ll.lng)
|
|
}, [])
|
|
|
|
if (!Map) {
|
|
return null
|
|
}
|
|
|
|
// Map.ir Map is memoized/cached by API key in getMapirMapComponent — not recreated each render.
|
|
/* eslint-disable react-hooks/static-components -- Map component comes from mapir setToken cache */
|
|
return (
|
|
<div
|
|
ref={rootRef}
|
|
className="relative h-full w-full min-h-[240px]"
|
|
data-testid="mapir-location-picker"
|
|
>
|
|
<Map
|
|
center={center}
|
|
containerStyle={{ height, position: 'relative', width: '100%' }}
|
|
maxZoom={22}
|
|
minZoom={0}
|
|
movingMethod="jumpTo"
|
|
scrollZoom={scrollZoom}
|
|
style={MAPIR_STYLE_URL}
|
|
zoom={zoomArr}
|
|
onClick={onClick}
|
|
onStyleLoad={onStyleLoad}
|
|
/>
|
|
{/* پین ثابت وسط نقشه — بدون وابستگی به Mapir.Marker که در مودال نامرئی میماند */}
|
|
<div
|
|
aria-hidden
|
|
className={`pointer-events-none absolute inset-0 z-20 flex items-center justify-center ${hasSelection ? 'opacity-100' : 'opacity-70'}`}
|
|
>
|
|
<MarkerIcon className="size-9 -translate-y-1/2 text-primary drop-shadow-md" />
|
|
</div>
|
|
</div>
|
|
)
|
|
/* eslint-enable react-hooks/static-components */
|
|
}
|
|
|
|
export default MapirLocationPicker
|