feat(address-map): click-to-select + "use my current location"

- Click-to-select instead of geocoding on every move: pan the map freely
  (drag), then tap to drop the pin — reverse geocodes that point ONCE.
  Saves Map.ir reverse tokens. Shows a "tap to select" hint until a point
  is chosen; a draggable-style red marker marks the selection (and the
  existing point in edit mode, without re-geocoding).
- "موقعیت فعلی من" button: uses the device geolocation to fly to and
  select the current position (with an error toast if denied/unsupported).
- removed the old center-pin + moveend auto-geocode and the unused
  user-location-found event/pulse effect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Arda Samadi 2026-06-28 16:43:55 +03:30
parent 0f617b45c9
commit 0b874dbd57

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef, useCallback } from "react";
import "maplibre-gl/dist/maplibre-gl.css";
import type { Map as MaplibreMap } from "maplibre-gl";
import { Search, X, ChevronLeft, MapPin } from "lucide-react";
import type { Map as MaplibreMap, Marker as MaplibreMarker } from "maplibre-gl";
import { Search, X, ChevronLeft, MapPin, Crosshair } from "lucide-react";
import AppInput from "./AppInput";
import AppTextArea from "./AppTextArea";
import {
@ -156,7 +156,10 @@ export default function InteractiveMap({
// MapLibre refs
const mapContainerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<MaplibreMap | null>(null);
const moveDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const markerRef = useRef<MaplibreMarker | null>(null);
// select(lat,lng): drop/move the pin and reverse-geocode that point once.
const selectRef = useRef<((lat: number, lng: number) => void) | null>(null);
const [isLocating, setIsLocating] = useState<boolean>(false);
// Initialize with the existing address data when in editing mode
useEffect(() => {
@ -277,22 +280,37 @@ export default function InteractiveMap({
new maplibregl.NavigationControl({ showCompass: false }),
"top-left"
);
// Reverse-geocode the centre whenever the map settles.
map.on("moveend", () => {
if (moveDebounceRef.current) clearTimeout(moveDebounceRef.current);
moveDebounceRef.current = setTimeout(() => {
const c = map.getCenter();
applyLocationRef.current([c.lat, c.lng]);
}, 350);
});
map.on("load", () => {
const c = map.getCenter();
applyLocationRef.current([c.lat, c.lng]);
});
// Drop/move the pin at a point and reverse-geocode it once (saves
// tokens — we only geocode on an explicit tap, not on every move).
const select = (lat: number, lng: number) => {
if (markerRef.current) {
markerRef.current.setLngLat([lng, lat]);
} else {
markerRef.current = new maplibregl.Marker({ color: "#ff3b30" })
.setLngLat([lng, lat])
.addTo(map);
}
applyLocationRef.current([lat, lng]);
};
selectRef.current = select;
// Pan with drag; a tap selects that point.
map.on("click", (e) => select(e.lngLat.lat, e.lngLat.lng));
// Edit mode: show the existing pin without re-geocoding.
if (selectedPosition) {
markerRef.current = new maplibregl.Marker({ color: "#ff3b30" })
.setLngLat([selectedPosition[1], selectedPosition[0]])
.addTo(map);
}
})();
return () => {
cancelled = true;
if (moveDebounceRef.current) clearTimeout(moveDebounceRef.current);
if (markerRef.current) {
markerRef.current.remove();
markerRef.current = null;
}
if (mapRef.current) {
mapRef.current.remove();
mapRef.current = null;
@ -322,6 +340,35 @@ export default function InteractiveMap({
setIsCityDrawerOpen(false);
};
// Use the device's current location: fly there and select it.
const handleUseMyLocation = () => {
if (typeof navigator === "undefined" || !navigator.geolocation) {
toast({
title: "موقعیت مکانی پشتیبانی نمی‌شود",
variant: "destructive",
});
return;
}
setIsLocating(true);
navigator.geolocation.getCurrentPosition(
(pos) => {
const { latitude, longitude } = pos.coords;
mapRef.current?.flyTo({ center: [longitude, latitude], zoom: 16 });
selectRef.current?.(latitude, longitude);
setIsLocating(false);
},
() => {
setIsLocating(false);
toast({
title: "دسترسی به موقعیت مکانی امکان‌پذیر نشد",
description: "لطفاً دسترسی موقعیت مکانی را در مرورگر فعال کنید.",
variant: "destructive",
});
},
{ enableHighAccuracy: true, timeout: 10000 }
);
};
const handleOpenDrawer = () => {
setIsDrawerOpen(true);
};
@ -473,49 +520,6 @@ export default function InteractiveMap({
}
}, [isEditing, initialAddress]);
// Add keyframes for the pulse animation
useEffect(() => {
const styleTag = document.createElement("style");
styleTag.innerHTML = `
@keyframes pulse {
0% {
transform: translate(-50%, -50%) scale(0.5);
opacity: 1;
}
100% {
transform: translate(-50%, -50%) scale(1.5);
opacity: 0;
}
}
`;
document.head.appendChild(styleTag);
// Listen for the custom location event
const handleLocationFound = (event: Event) => {
const customEvent = event as CustomEvent<{ lat: number; lng: number }>;
const { lat, lng } = customEvent.detail;
if (mapRef.current) {
mapRef.current.flyTo({ center: [lng, lat], zoom: 15 });
} else {
applyLocationRef.current([lat, lng]);
}
};
window.addEventListener(
"user-location-found",
handleLocationFound as EventListener
);
return () => {
document.head.removeChild(styleTag);
window.removeEventListener(
"user-location-found",
handleLocationFound as EventListener
);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// If formOnly mode, render only the form
if (formOnly) {
return (
@ -840,22 +844,38 @@ export default function InteractiveMap({
className="h-full w-full"
/>
{/* Fixed center pin — the map moves under it; its tip marks the point */}
<div className="pointer-events-none absolute left-1/2 top-1/2 z-[5] -translate-x-1/2 -translate-y-full drop-shadow-md">
<MapPin size={40} className="fill-RED text-RED" strokeWidth={1.5} />
</div>
{/* Use my current location */}
<button
type="button"
onClick={handleUseMyLocation}
disabled={isLocating}
dir="rtl"
className="absolute top-3 right-3 z-[5] flex items-center gap-1.5 bg-WHITE/95 backdrop-blur rounded-full shadow-md px-3.5 h-10 text-R12 font-bold text-BLACK disabled:opacity-60"
>
<Crosshair size={18} className="text-VITROWN_BLUE" />
{isLocating ? "در حال یافتن..." : "موقعیت فعلی من"}
</button>
{/* Selected-address preview */}
{selectedAddress && (
{/* Hint / selected-address preview */}
<div
dir="rtl"
className="absolute left-3 right-3 bottom-3 z-[5] bg-WHITE/95 backdrop-blur rounded-xl shadow-md px-3.5 py-2.5"
>
{selectedAddress ? (
<>
<p className="text-R10 text-GRAY mb-0.5">موقعیت انتخابشده</p>
<p className="text-R12 font-bold line-clamp-2">{selectedAddress}</p>
</div>
<p className="text-R12 font-bold line-clamp-2">
{selectedAddress}
</p>
</>
) : (
<p className="text-R12 font-medium text-BLACK2 flex items-center gap-1.5">
<MapPin size={16} className="text-RED shrink-0" />
برای انتخاب موقعیت، روی نقشه بزنید
</p>
)}
</div>
</div>
{/* Select Location Button */}
{!onLocationSelected && selectedPosition && !isDrawerOpen && (