'use client'; import React, { useEffect, useState } from 'react'; import { MapContainer, TileLayer, Marker, Popup, useMap, useMapEvents, Polygon } from 'react-leaflet'; import L from 'leaflet'; import { Skeleton } from '@/components/ui/skeleton'; import 'leaflet/dist/leaflet.css'; import './CustomMap.css'; import { ActiveSearchbox } from '../input/ActiveSearchbox'; // Define types for our props interface ServiceArea { type: "Polygon"; coordinates: number[][][]; } interface CustomMapProps { initialPosition: [number, number]; zoom?: number; markers?: Array<{ position: [number, number]; title: string; icon?: L.Icon | L.DivIcon; }>; serviceArea?: ServiceArea | null; onPositionSelect?: (position: [number, number]) => void; onZoomChange?: (zoom: number) => void; onClick?: (event: L.LeafletMouseEvent) => void; searchEnabled?: boolean markerActive?: boolean onMapReady?: (map: L.Map) => void; } // Component to handle map events and position updates function MapEvents({ onZoomChange, onClick, center, setMapRef, }: Pick & { center: [number, number]; setMapRef: (map: L.Map) => void; }) { const map = useMapEvents({ zoom: () => { onZoomChange?.(map.getZoom()); }, click: (e) => { onClick?.(e); }, }); // Update map center when center prop changes React.useEffect(() => { map.setView(center, map.getZoom(), { animate: true, duration: 1 }); }, [map, center]); // Set map reference React.useEffect(() => { setMapRef(map); }, [map, setMapRef]); return null; } // Component to handle marker updates on zoom function MarkerUpdater({ markers }: { markers?: CustomMapProps['markers'] }) { const map = useMap(); useEffect(() => { // You can implement custom marker scaling or clustering logic here const zoom = map.getZoom(); // Example: Scale markers based on zoom level markers?.forEach(marker => { const element = document.querySelector(`[title="${marker.title}"]`); if (element) { const scale = Math.min(1 + (zoom / 20), 1.5); (element as HTMLElement).style.transform = `scale(${scale})`; } }); }, [map, markers]); return null; } // Component to auto-open popup for shop marker function MarkerWithAutoOpen({ position, icon, title }: { position: [number, number]; icon?: L.Icon | L.DivIcon; title: string }) { const markerRef = React.useRef(null); useEffect(() => { if (markerRef.current && title === 'موقعیت فروشگاه') { markerRef.current.openPopup(); } }, [title]); return (
{title}
); } const CustomMap: React.FC = ({ initialPosition, zoom = 13, markers = [], serviceArea, onPositionSelect, onZoomChange, onClick, markerActive = true, searchEnabled = true, onMapReady, }) => { const [selectedPosition, setSelectedPosition] = useState<[number, number] | null>(null); const [mapInstance, setMapInstance] = useState(null); const [isSearching, setIsSearching] = useState(false); const [searchResults, setSearchResults] = useState>([]); const [isMapLoading, setIsMapLoading] = useState(true); // Handle map load complete useEffect(() => { const checkTiles = () => { if (mapInstance) { const container = document.querySelector('.leaflet-tile-container'); if (container && container.children.length > 0) { setIsMapLoading(false); } else { setTimeout(checkTiles, 100); } } }; checkTiles(); }, [mapInstance]); // Notify parent when map is ready useEffect(() => { if (mapInstance && onMapReady) { onMapReady(mapInstance); } }, [mapInstance, onMapReady]); // Initialize default icon on client side only useEffect(() => { // Dynamic import of Leaflet on the client side // Fix for default marker icon in Next.js delete ((L.Icon.Default.prototype) as { _getIconUrl?: unknown })._getIconUrl; L.Icon.Default.mergeOptions({ iconRetinaUrl: '/marker-icon-2x.png', iconUrl: '/marker-icon.png', shadowUrl: '/marker-shadow.png', }); }, []); const handleMapClick = (e: L.LeafletMouseEvent) => { const newPosition: [number, number] = [e.latlng.lat, e.latlng.lng]; if (markerActive) { setSelectedPosition(newPosition); } onPositionSelect?.(newPosition); onClick?.(e); }; const [searchTimeout, setSearchTimeout] = useState(); const handleSearch = async (value: string) => { if (!value || value.trim().length === 0) { setSearchResults([]); return; } // Clear previous timeout if (searchTimeout) { clearTimeout(searchTimeout); } setIsSearching(true); // Set new timeout const timeout = setTimeout(async () => { try { // Iran's bounding box coordinates (roughly) const viewbox = '44.0,25.0,63.0,40.0'; // [min lon, min lat, max lon, max lat] const response = await fetch( `https://nominatim.openstreetmap.org/search?` + `format=json&` + `q=${encodeURIComponent(value)}&` + `countrycodes=ir&` + // Prioritize Iran `viewbox=${viewbox}&` + `bounded=0&` + // Allow results outside viewbox but prioritize inside `limit=10&` + // Limit results to 10 `namedetails=1&` + // Get native names `accept-language=fa` // Request Persian results , { headers: { 'Accept-Language': 'fa,en;q=0.9' // Prefer Persian, fallback to English } }); interface SearchResult { place_id: string; display_name: string; lat: string; lon: string; namedetails?: { name?: string; 'name:fa'?: string; alt_name?: string; 'alt_name:fa'?: string; }; } const data: SearchResult[] = await response.json(); setSearchResults(data.map((item) => ({ id: item.place_id, // Use Persian name if available, fallback to display_name title: /* item.namedetails?.['name:fa'] || item.namedetails?.name || item.namedetails?.['alt_name:fa'] || item.namedetails?.alt_name || */ item.display_name, position: [parseFloat(item.lat), parseFloat(item.lon)] as [number, number] }))); } catch (error) { console.error('Search error:', error); setSearchResults([]); } finally { setIsSearching(false); } }, 500); // Wait 500ms after last keystroke setSearchTimeout(timeout); }; const handleSearchResultSelect = (result: { id: string; title: string; position: [number, number] }) => { console.log(result); setSelectedPosition(result.position); if (mapInstance) { mapInstance.setView(result.position, mapInstance.getZoom()); } }; return (
{searchEnabled && { // Find the full result object with position const result = searchResults.find(r => r.id === v.id && r.title === v.title); if (result) { handleSearchResultSelect(result); } }} /> }
{isMapLoading && (
)} {/* Service Area Polygon */} {serviceArea && serviceArea.coordinates && serviceArea.coordinates.length > 0 && ( [coord[1], coord[0]] as [number, number])} pathOptions={{ color: '#3b82f6', fillColor: '#3b82f6', fillOpacity: 0.2, weight: 2, interactive: false, }} /> )} {/* Custom markers */} {markers.map((marker, index) => ( ))} {/* Selected position marker */} {selectedPosition && (
موقعیت انتخاب شده
)} {/* Event handlers */} {/* Marker updater for zoom effects */}
); }; export default CustomMap;