'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import dynamic from 'next/dynamic'
import ConsumerModal from '@/components/consumer/ConsumerModal'
import ConsumerPickerTrigger from '@/components/events/create/ConsumerPickerTrigger'
import CloseLinearIcon from '@/components/icons/CloseLinearIcon'
import MapIcon from '@/components/icons/MapIcon'
import { texts } from '@/texts'
import { addToast } from '@/lib/toast'
import { cn } from '@/lib/cn'
import { getPublicMapApiKey } from '@/helpers/mapir'
import { reverseGeocodeMapIr } from '@/services/mapirReverseGeocode'
const MapirLocationPicker = dynamic(() => import('@/components/map/MapirLocationPicker'), { ssr: false })
interface MapLocationPickerProps {
lat: number
lng: number
onChange: (coords: { lat: number; lng: number }) => void
/** پر کردن خودکار آدرس از reverse geocode؛ کاربر همچنان میتواند آدرس را ویرایش کند */
onAddressResolved?: (address: string) => void
disabled?: boolean
/** Consumer create uses a bottom sheet; admin/edit keep the inline map. */
mode?: 'inline' | 'modal'
/** Optional address shown on the modal trigger once a place is chosen. */
displayAddress?: string
}
const TEHRAN_LAT = 35.6892
const TEHRAN_LNG = 51.389
function MapCanvas({
centerLat,
centerLng,
markerLat,
markerLng,
disabled,
mapApiKey,
isClient,
className,
mapHeight = 320,
onPick,
}: {
centerLat: number
centerLng: number
markerLat: number | null
markerLng: number | null
disabled?: boolean
mapApiKey: string
isClient: boolean
className?: string
/** ارتفاع صریح پیکسلی — درصد داخل مودال اغلب ۰ میشود و مارکر را خراب میکند */
mapHeight?: number
onPick: (lat: number, lng: number) => void
}) {
return (
{isClient && mapApiKey ? (
) : isClient && !mapApiKey ? (
{texts.events.mapKeyMissing}
) : (
{texts.events.mapLoading}
)}
)
}
export default function MapLocationPicker({
lat,
lng,
onChange,
onAddressResolved,
disabled,
mode = 'inline',
displayAddress,
}: MapLocationPickerProps) {
const [isClient, setIsClient] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [markerPosition, setMarkerPosition] = useState<[number, number] | null>(null)
const [draftPosition, setDraftPosition] = useState<[number, number] | null>(null)
const mapApiKey = getPublicMapApiKey()
const reverseRequestIdRef = useRef(0)
useEffect(() => {
setIsClient(true)
}, [])
useEffect(() => {
if (Number.isFinite(lat) && Number.isFinite(lng)) {
setMarkerPosition([lat, lng])
}
}, [lat, lng])
const hasValidCoords = Number.isFinite(lat) && Number.isFinite(lng)
const mapCenterLat = hasValidCoords ? lat : TEHRAN_LAT
const mapCenterLng = hasValidCoords ? lng : TEHRAN_LNG
const reverseGeocode = useCallback(
async (pickedLat: number, pickedLng: number) => {
const key = mapApiKey.trim()
if (!key) return
const rid = ++reverseRequestIdRef.current
try {
const address = await reverseGeocodeMapIr(pickedLat, pickedLng, key)
if (rid !== reverseRequestIdRef.current) return
if (address) {
onAddressResolved?.(address)
} else {
addToast({ title: texts.events.mapAddressNotFound, color: 'warning' })
}
} catch (err) {
if (rid !== reverseRequestIdRef.current) return
const message = err instanceof Error ? err.message : texts.validation.map.mapAddressFailed
addToast({ title: message, color: 'danger' })
}
},
[mapApiKey, onAddressResolved]
)
const handleInlineMapClick = useCallback(
async (pickedLat: number, pickedLng: number) => {
if (disabled) return
setMarkerPosition([pickedLat, pickedLng])
onChange({ lat: pickedLat, lng: pickedLng })
await reverseGeocode(pickedLat, pickedLng)
},
[disabled, onChange, reverseGeocode]
)
const openModal = () => {
if (disabled) return
setDraftPosition(markerPosition ?? (hasValidCoords ? [lat, lng] : null))
setIsOpen(true)
}
const acceptModal = async () => {
if (!draftPosition) return
const [pickedLat, pickedLng] = draftPosition
setIsSubmitting(true)
try {
setMarkerPosition(draftPosition)
onChange({ lat: pickedLat, lng: pickedLng })
await reverseGeocode(pickedLat, pickedLng)
setIsOpen(false)
} finally {
setIsSubmitting(false)
}
}
const triggerLabel = displayAddress?.trim()
? displayAddress.trim()
: markerPosition
? texts.events.mapLocationSelected
: texts.events.mapSelectModalTitle
if (mode === 'modal') {
// مرکز نقشه را روی pick عوض نکن — فقط مارکر جابهجا شود (jumpTo روی هر کلیک مارکر را از دست میدهد/پنهان میکند)
return (
{texts.events.mapTitle}
{triggerLabel}
{disabled ? texts.events.mapLocked : texts.events.mapModalHint}
}
cancelLabel={texts.common.cancel}
isOpen={isOpen}
isSubmitting={isSubmitting}
submitDisabled={!draftPosition}
submitLabel={texts.common.select}
title={texts.events.mapSelectModalTitle}
onOpenChange={setIsOpen}
onSubmit={() => {
void acceptModal()
}}
>
{isOpen ? (
{
if (disabled || isSubmitting) return
setDraftPosition([pickedLat, pickedLng])
}}
/>
) : null}
)
}
return (
{texts.events.mapTitle}
{disabled ? texts.events.mapLocked : texts.events.mapHint}
)
}