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:
parent
0f617b45c9
commit
0b874dbd57
@ -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 && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user