import React, { useState, useEffect, useRef } from "react"; import { MapContainer, TileLayer, Marker, Popup, useMapEvents, useMap, } from "react-leaflet"; import "leaflet/dist/leaflet.css"; import L from "leaflet"; import { Search, X, ChevronLeft } from "lucide-react"; import AppInput from "./AppInput"; import AppTextArea from "./AppTextArea"; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, } from "../components/ui/drawer"; import { useServiceCreateAddress, useServiceUpdateAddress, } from "../utils/RequestHandler"; import { useToast } from "../hooks/use-toast"; import { useNavigate, useSearchParams } from "@remix-run/react"; import { Address } from "../../src/api/types"; import { iranianCities, iranianProvinces, getCitiesByProvince, type IranianCity, } from "../data/iranian-cities"; // Form data interface interface FormData { title: string; receiver_name: string; phone_number: string; main_address: string; postal_code: string; entry_reference: string; details: string; latitude: number; longitude: number; is_default: boolean; } // Props interface interface InteractiveMapProps { onLocationSelected?: (location: { position: [number, number]; address: string; postalCode?: string; }) => void; initialAddress?: Address; isEditing?: boolean; addressId?: string; formOnly?: boolean; // Show only the form without map } // Convert Persian/Arabic digits to English function convertPersianToEnglish(str: string): string { const persianDigits = "۰۱۲۳۴۵۶۷۸۹"; const arabicDigits = "٠١٢٣٤٥٦٧٨٩"; let result = str; for (let i = 0; i < 10; i++) { result = result.replace(new RegExp(persianDigits[i], "g"), i.toString()); result = result.replace(new RegExp(arabicDigits[i], "g"), i.toString()); } return result; } // Component to change map view when city is selected function CityChangeView({ center }: { center: [number, number] }) { const map = useMap(); useEffect(() => { map.setView(center, 13); }, [center, map]); return null; } // Component to handle map clicks function MapClickHandler({ onMapClick, }: { onMapClick: (latlng: [number, number]) => void; }) { useMapEvents({ click: (e) => { const { lat, lng } = e.latlng; onMapClick([lat, lng]); }, }); return null; } export default function InteractiveMap({ onLocationSelected, initialAddress, isEditing = false, addressId, formOnly = false, }: InteractiveMapProps) { const [selectedPosition, setSelectedPosition] = useState< [number, number] | null >(null); const [selectedAddress, setSelectedAddress] = useState(""); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isCityDrawerOpen, setIsCityDrawerOpen] = useState(false); const [selectedCity, setSelectedCity] = useState( isEditing ? "" : "تهران" ); const [currentCenter, setCurrentCenter] = useState<[number, number]>( iranianCities[0].position ); const [mounted, setMounted] = useState(false); const [formData, setFormData] = useState({ title: "", receiver_name: "", phone_number: "", main_address: "", postal_code: "", entry_reference: "", details: "", latitude: 0, longitude: 0, is_default: false, }); // State for city search const [citySearchTerm, setCitySearchTerm] = useState(""); const [filteredCities, setFilteredCities] = useState(iranianCities); const [selectedProvince, setSelectedProvince] = useState(null); // Use a ref for the unique map ID to prevent it changing on re-renders const mapIdRef = useRef( `map-${Math.random().toString(36).substring(2, 9)}-${Date.now()}-${isEditing ? "edit" : "add"}-${addressId || "new"}` ); const navigate = useNavigate(); const [searchParams] = useSearchParams(); // Set up the toast notification system const { toast } = useToast(); // Get return parameters from URL const returnTo = searchParams.get("returnTo"); const cartId = searchParams.get("cartId"); // Set up the mutation for creating addresses const createAddressMutation = useServiceCreateAddress(); // For address updates, we'll use the hook with current form data const updateAddressMutation = useServiceUpdateAddress(addressId || "", { title: formData.title, receiverName: formData.receiver_name, phoneNumber: formData.phone_number, mainAddress: formData.main_address, postalCode: formData.postal_code, entryReference: formData.entry_reference, details: formData.details, latitude: formData.latitude, longitude: formData.longitude, isDefault: formData.is_default, }); // Fix Leaflet's default icon path issues when the component mounts useEffect(() => { // Fix Leaflet icon issues delete (L.Icon.Default.prototype as { _getIconUrl?: string })._getIconUrl; L.Icon.Default.mergeOptions({ iconUrl: "https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png", iconRetinaUrl: "https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png", shadowUrl: "https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png", }); // Cleanup function to remove map instance when component unmounts return () => { // This code ensures previous maps are cleaned up // eslint-disable-next-line react-hooks/exhaustive-deps const mapContainer = document.getElementById(mapIdRef.current); if (mapContainer) { // Clean up previous map on this element // In Leaflet version 1+, using map._leaflet no longer works // and instead removeAttribute should be used mapContainer.innerHTML = ""; mapContainer.removeAttribute("data-leaflet-internal-id"); mapContainer.removeAttribute("_leaflet_id"); console.log("Map container cleaned up successfully"); } }; }, []); // Initialize with the existing address data when in editing mode useEffect(() => { if (isEditing && initialAddress) { // Set form data with initial values from address setFormData({ title: initialAddress.title || "", receiver_name: initialAddress.receiverName || "", phone_number: initialAddress.phoneNumber || "", main_address: initialAddress.mainAddress || "", postal_code: initialAddress.postalCode || "", entry_reference: initialAddress.entryReference || "", details: initialAddress.details || "", latitude: initialAddress.latitude || 0, longitude: initialAddress.longitude || 0, is_default: initialAddress.isDefault || false, }); if (initialAddress.latitude && initialAddress.longitude) { console.log( "Setting position from initial address:", initialAddress.latitude, initialAddress.longitude ); // Set the marker position to the address location setSelectedPosition([ initialAddress.latitude, initialAddress.longitude, ]); setSelectedAddress(initialAddress.mainAddress || ""); } } }, [isEditing, initialAddress]); // Handle city search useEffect(() => { let cities = selectedProvince ? getCitiesByProvince(selectedProvince) : iranianCities; if (citySearchTerm) { cities = cities.filter( (city) => city.name.includes(citySearchTerm) || city.province.includes(citySearchTerm) ); } setFilteredCities(cities); }, [citySearchTerm, selectedProvince]); // Function to get address from coordinates const getAddressFromCoordinates = async (latlng: [number, number]) => { try { const response = await fetch( `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng[0]}&lon=${latlng[1]}&zoom=18&addressdetails=1&accept-language=fa&countrycodes=ir`, { headers: { "Accept-Language": "fa", }, } ); const data = await response.json(); return data.display_name || "آدرس نامشخص"; } catch (error) { console.error("Error fetching address:", error); return ""; } }; // Extract postal code from address data const getPostalCodeFromAddress = async (latlng: [number, number]) => { try { const response = await fetch( `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng[0]}&lon=${latlng[1]}&zoom=18&addressdetails=1&accept-language=fa&countrycodes=ir`, { headers: { "Accept-Language": "fa", }, } ); const data = await response.json(); if (data && data.address && data.address.postcode) { return data.address.postcode; } return ""; } catch (error) { console.error("Error fetching postal code:", error); return ""; } }; const [isSubmitting, setIsSubmitting] = useState(false); const [errors, setErrors] = useState>({}); const handleMapClick = async (latlng: [number, number]) => { const address = await getAddressFromCoordinates(latlng); const postalCode = await getPostalCodeFromAddress(latlng); setSelectedPosition(latlng); setSelectedAddress(address); // Don't update postal_code in edit mode, preserve the original value setFormData({ ...formData, main_address: address, // Don't set postal_code here in either mode, preserve the existing value in edit mode latitude: latlng[0], longitude: latlng[1], }); // If using as a location picker in another component, call the callback if (onLocationSelected) { onLocationSelected({ position: latlng, address, postalCode: isEditing ? formData.postal_code : postalCode, }); } }; const handleCitySelect = (cityName: string) => { setSelectedCity(cityName); const city = iranianCities.find((c) => c.name === cityName); if (city) { setCurrentCenter(city.position); } setSelectedProvince(null); setCitySearchTerm(""); setIsCityDrawerOpen(false); }; const handleOpenDrawer = () => { setIsDrawerOpen(true); }; const handleCloseDrawer = () => { setIsDrawerOpen(false); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); setErrors({}); // Validate required fields const newErrors: Record = {}; // عنوان آدرس if (!formData.title || !formData.title.trim()) { newErrors.title = "عنوان آدرس الزامی است"; } // نام گیرنده if (!formData.receiver_name || !formData.receiver_name.trim()) { newErrors.receiver_name = "نام گیرنده الزامی است"; } else if (formData.receiver_name.trim().length < 3) { newErrors.receiver_name = "نام گیرنده باید حداقل 3 کاراکتر باشد"; } // شماره تماس if (!formData.phone_number || !formData.phone_number.trim()) { newErrors.phone_number = "شماره تماس الزامی است"; } else { const phoneRegex = /^(\+98|0)?9\d{9}$/; const cleanPhone = convertPersianToEnglish( formData.phone_number.replace(/[\s-]/g, "") ); if (!phoneRegex.test(cleanPhone)) { newErrors.phone_number = "شماره تماس معتبر نیست (مثال: 09123456789)"; } } // آدرس کامل if (!formData.main_address || !formData.main_address.trim()) { newErrors.main_address = "آدرس الزامی است"; } else if (formData.main_address.trim().length < 10) { newErrors.main_address = "آدرس باید حداقل 10 کاراکتر باشد"; } // کد پستی if (!formData.postal_code || !formData.postal_code.trim()) { newErrors.postal_code = "کد پستی الزامی است"; } else { const postalCodeRegex = /^\d{10}$/; const cleanPostalCode = convertPersianToEnglish( formData.postal_code.replace(/[\s-]/g, "") ); if (!postalCodeRegex.test(cleanPostalCode)) { newErrors.postal_code = "کد پستی باید 10 رقم باشد"; } } // پلاک if (!formData.entry_reference || !formData.entry_reference.trim()) { newErrors.entry_reference = "پلاک الزامی است"; } if (Object.keys(newErrors).length > 0) { setErrors(newErrors); setIsSubmitting(false); return; } try { if (isEditing && addressId) { // Update existing address - use mutation instead of refetch console.log("Updating address with ID:", addressId); console.log("Form data for update:", formData); // In edit mode, use mutation for update await updateAddressMutation.mutateAsync(); toast({ title: "آدرس با موفقیت ویرایش شد", description: "آدرس شما با موفقیت ویرایش شد.", }); } else { // Create new address await createAddressMutation.mutateAsync(formData); toast({ title: "آدرس جدید با موفقیت اضافه شد", description: "آدرس جدید شما با موفقیت اضافه شد.", }); } // Redirect based on where the user came from if (returnTo === "cart" && cartId) { // Return to cart with step=1 to show shipping info navigate(`/cart/${cartId}?step=1`); } else { // Default behavior - go to address list navigate("/profile/location"); } } catch (error) { console.error("Error submitting address:", error); toast({ variant: "destructive", title: "خطا در ثبت آدرس", description: "متاسفانه خطایی در ثبت آدرس رخ داد. لطفا مجددا تلاش کنید.", }); } finally { setIsSubmitting(false); } }; // Clear validation errors when form data changes useEffect(() => { setErrors({}); }, [formData]); // ایجاد useEffect جدید برای ثبت ID منحصر به فرد نقشه در کنسول برای دیباگ useEffect(() => { console.log("Map ID created:", mapIdRef.current); }, []); // تنظیم حالت mounted در اولین رندر useEffect(() => { setMounted(true); return () => { // پاکسازی هنگام خروج از صفحه setMounted(false); console.log("Map component unmounting, cleaning up state"); }; }, []); // Initial center position setup - ensure it's synced with location in edit mode useEffect(() => { if ( isEditing && initialAddress && initialAddress.latitude && initialAddress.longitude ) { // Make sure the map center is always set to the location being edited initially // This is separate from the form data initialization to ensure map always stays centered // on the current location regardless of other data changes setCurrentCenter([initialAddress.latitude, initialAddress.longitude]); console.log("Map center synchronized with edited location coordinates"); } }, [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; handleMapClick([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 (
) => setFormData({ ...formData, title: e.target.value, }) } inputDir="rtl" /> {errors.title && (

{errors.title}

)}
) => setFormData({ ...formData, receiver_name: e.target.value, }) } inputDir="rtl" /> {errors.receiver_name && (

{errors.receiver_name}

)}
) => setFormData({ ...formData, phone_number: e.target.value, }) } inputDir={formData.phone_number ? "ltr" : "rtl"} /> {errors.phone_number && (

{errors.phone_number}

)}
) => setFormData({ ...formData, main_address: e.target.value, }) } inputDir="rtl" /> {errors.main_address && (

{errors.main_address}

)}
) => setFormData({ ...formData, postal_code: e.target.value, }) } inputDir="ltr" /> {errors.postal_code && (

{errors.postal_code}

)}
) => setFormData({ ...formData, entry_reference: e.target.value, }) } inputDir="rtl" /> {errors.entry_reference && (

{errors.entry_reference}

)}
) => setFormData({ ...formData, details: e.target.value, }) } inputDir="rtl" />
تنظیم به عنوان آدرس پیش‌فرض
); } return (
{/* City selector in a nice blue box */}
setIsCityDrawerOpen(true)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { setIsCityDrawerOpen(true); } }} >
{selectedCity}
{/* City Selector Drawer */} { setIsCityDrawerOpen(open); if (!open) { setSelectedProvince(null); setCitySearchTerm(""); } }}>
{selectedProvince ? ( ) : ( setIsCityDrawerOpen(false)} /> )} {selectedProvince ? `شهرهای ${selectedProvince}` : "انتخاب استان و شهر"}
) => setCitySearchTerm(e.target.value) } />
{/* Province/City List */}
{!selectedProvince && !citySearchTerm ? ( // Show provinces iranianProvinces.map((province) => ( )) ) : filteredCities.length > 0 ? ( // Show cities filteredCities.map((city) => ( )) ) : (

نتیجه‌ای یافت نشد

)}
{/* Map Container */}
{mounted && ( {/* */} {selectedPosition && (

آدرس:

{selectedAddress}

مختصات: {selectedPosition[0].toFixed(6)},{" "} {selectedPosition[1].toFixed(6)}

)}
)}
{/* Select Location Button */} {!onLocationSelected && selectedPosition && !isDrawerOpen && ( )} {/* Address Form Drawer */} {!onLocationSelected && (
setIsDrawerOpen(false)} /> {isEditing ? "ویرایش آدرس" : "ثبت آدرس"}
) => setFormData({ ...formData, title: e.target.value, }) } inputDir="rtl" /> {errors.title && (

{errors.title}

)}
) => setFormData({ ...formData, receiver_name: e.target.value, }) } inputDir="rtl" /> {errors.receiver_name && (

{errors.receiver_name}

)}
) => setFormData({ ...formData, phone_number: e.target.value, }) } inputDir={formData.phone_number ? "ltr" : "rtl"} /> {errors.phone_number && (

{errors.phone_number}

)}
) => setFormData({ ...formData, main_address: e.target.value, }) } inputDir="rtl" /> {errors.main_address && (

{errors.main_address}

)}
) => setFormData({ ...formData, postal_code: e.target.value, }) } inputDir="ltr" /> {errors.postal_code && (

{errors.postal_code}

)}
) => setFormData({ ...formData, entry_reference: e.target.value, }) } inputDir="rtl" /> {errors.entry_reference && (

{errors.entry_reference}

)}
) => setFormData({ ...formData, details: e.target.value, }) } inputDir="rtl" />
تنظیم به عنوان آدرس پیش‌فرض
)} {/* Overlay background when drawer is open */} {!onLocationSelected && isDrawerOpen && (
{ if (e.key === "Escape") { handleCloseDrawer(); } }} >
)} {/* Selection button for location picker mode */} {onLocationSelected && selectedPosition && ( )}
); }