mehdi-akbari-map 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"InteractiveMap.d.ts","sourceRoot":"","sources":["../../src/components/InteractiveMap.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAG3D,OAAO,EAAS,QAAQ,EAAE,MAAM,UAAU,CAAC;AAE3C,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,QAAQ,CAiF7C,CAAC"}
1
+ {"version":3,"file":"InteractiveMap.d.ts","sourceRoot":"","sources":["../../src/components/InteractiveMap.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAGxE,OAAO,EAAS,QAAQ,EAAE,MAAM,UAAU,CAAC;AAE3C,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,QAAQ,CAqG7C,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { MapProps } from '../types';
3
+ import '@neshan-maps-platform/mapbox-gl/dist/NeshanMapboxGl.css';
3
4
  declare const Map: React.FC<MapProps>;
4
5
  export default Map;
5
6
  //# sourceMappingURL=Map.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Map.d.ts","sourceRoot":"","sources":["../../src/components/Map.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAC;AACxE,OAAO,KAAK,EAAoB,QAAQ,EAAE,MAAM,UAAU,CAAC;AAK3D,QAAA,MAAM,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC,QAAQ,CAkI3B,CAAC;AAEF,eAAe,GAAG,CAAC"}
1
+ {"version":3,"file":"Map.d.ts","sourceRoot":"","sources":["../../src/components/Map.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAC;AACxE,OAAO,KAAK,EAAoB,QAAQ,EAAE,MAAM,UAAU,CAAC;AAK3D,OAAO,yDAAyD,CAAC;AAEjE,QAAA,MAAM,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC,QAAQ,CA2J3B,CAAC;AAEF,eAAe,GAAG,CAAC"}
package/dist/index.cjs CHANGED
@@ -105,6 +105,7 @@ function createClusterElement(count) {
105
105
 
106
106
  // src/components/Map.tsx
107
107
  var import_supercluster = __toESM(require("supercluster"), 1);
108
+ var import_NeshanMapboxGl = require("@neshan-maps-platform/mapbox-gl/dist/NeshanMapboxGl.css");
108
109
  var import_jsx_runtime = require("react/jsx-runtime");
109
110
  var Map = ({
110
111
  options,
@@ -119,14 +120,19 @@ var Map = ({
119
120
  const [mapInstance, setMapInstance] = (0, import_react.useState)(null);
120
121
  const [mapLib, setMapLib] = (0, import_react.useState)(null);
121
122
  const markersRef = (0, import_react.useRef)([]);
123
+ const isRemoving = (0, import_react.useRef)(false);
122
124
  const clusterIndex = (0, import_react.useRef)(new import_supercluster.default({
123
125
  radius: 60,
124
126
  maxZoom: 16
125
127
  }));
126
128
  (0, import_react.useEffect)(() => {
129
+ let mounted = true;
127
130
  import("@neshan-maps-platform/mapbox-gl").then((mod) => {
128
- setMapLib(mod.default || mod);
131
+ if (mounted) setMapLib(mod.default || mod);
129
132
  });
133
+ return () => {
134
+ mounted = false;
135
+ };
130
136
  }, []);
131
137
  (0, import_react.useEffect)(() => {
132
138
  if (!mapLib || !mapContainerRef.current || mapInstance) return;
@@ -135,70 +141,79 @@ var Map = ({
135
141
  ...options
136
142
  });
137
143
  map.on("load", () => {
138
- setMapInstance(map);
139
- onMapLoad?.(map);
144
+ if (!isRemoving.current) {
145
+ setMapInstance(map);
146
+ onMapLoad?.(map);
147
+ }
140
148
  });
141
149
  return () => {
142
- if (map) map.remove();
150
+ if (map) {
151
+ isRemoving.current = true;
152
+ setTimeout(() => {
153
+ try {
154
+ map.remove();
155
+ } catch (e) {
156
+ console.warn("Mapbox remove error ignored:", e);
157
+ }
158
+ }, 0);
159
+ }
143
160
  };
144
- }, [mapLib, options, onMapLoad]);
161
+ }, [mapLib]);
145
162
  (0, import_react.useEffect)(() => {
146
- if (!mapInstance || !selectedMarkerId) return;
163
+ if (!mapInstance || !selectedMarkerId || isRemoving.current) return;
147
164
  const selectedStore = markers.find((m) => m.id === selectedMarkerId);
148
165
  if (selectedStore) {
149
166
  mapInstance.flyTo({
150
167
  center: [selectedStore.lng, selectedStore.lat],
151
168
  zoom: 16,
152
169
  essential: true,
153
- duration: 1500
170
+ duration: 1200
154
171
  });
155
172
  }
156
173
  }, [selectedMarkerId, mapInstance, markers]);
157
174
  const updateMarkers = (0, import_react.useCallback)(() => {
158
- if (!mapInstance || !mapLib) return;
159
- const bounds = mapInstance.getBounds();
160
- const zoom = Math.floor(mapInstance.getZoom());
161
- const clusters = clusterIndex.current.getClusters(
162
- [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()],
163
- zoom
164
- );
165
- markersRef.current.forEach((m) => m.remove());
166
- markersRef.current = [];
167
- clusters.forEach((cluster) => {
168
- const [lng, lat] = cluster.geometry.coordinates;
169
- const { cluster: isCluster, point_count: count } = cluster.properties;
170
- let el;
171
- if (isCluster) {
172
- el = createClusterElement(count);
173
- el.onclick = (e) => {
174
- e.stopPropagation();
175
- const expansionZoom = Math.min(
176
- clusterIndex.current.getClusterExpansionZoom(cluster.id),
177
- 18
178
- );
179
- mapInstance.easeTo({
180
- center: [lng, lat],
181
- zoom: expansionZoom,
182
- duration: 800
175
+ if (!mapInstance || !mapLib || isRemoving.current) return;
176
+ try {
177
+ const bounds = mapInstance.getBounds();
178
+ const zoom = Math.floor(mapInstance.getZoom());
179
+ const clusters = clusterIndex.current.getClusters(
180
+ [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()],
181
+ zoom
182
+ );
183
+ markersRef.current.forEach((m) => m.remove());
184
+ markersRef.current = [];
185
+ clusters.forEach((cluster) => {
186
+ const [lng, lat] = cluster.geometry.coordinates;
187
+ const { cluster: isCluster, point_count: count } = cluster.properties;
188
+ let el;
189
+ if (isCluster) {
190
+ el = createClusterElement(count);
191
+ el.onclick = (e) => {
192
+ e.stopPropagation();
193
+ const expansionZoom = Math.min(
194
+ clusterIndex.current.getClusterExpansionZoom(cluster.id),
195
+ 18
196
+ );
197
+ mapInstance.easeTo({ center: [lng, lat], zoom: expansionZoom });
198
+ };
199
+ } else {
200
+ const storeData = cluster.properties;
201
+ el = createCustomMarkerElement({
202
+ marker: storeData,
203
+ isSelected: storeData.id === selectedMarkerId,
204
+ logoSrc: markerLogoUrl
183
205
  });
184
- };
185
- } else {
186
- const storeData = cluster.properties;
187
- el = createCustomMarkerElement({
188
- marker: storeData,
189
- isSelected: storeData.id === selectedMarkerId,
190
- logoSrc: markerLogoUrl
191
- });
192
- el.onclick = () => onMarkerClick?.(storeData, 0, mapInstance);
193
- }
194
- const newMarker = new mapLib.Marker({
195
- element: el,
196
- anchor: isCluster ? "center" : "bottom"
197
- }).setLngLat([lng, lat]).addTo(mapInstance);
198
- markersRef.current.push(newMarker);
199
- });
206
+ el.onclick = () => onMarkerClick?.(storeData, 0, mapInstance);
207
+ }
208
+ const newMarker = new mapLib.Marker({ element: el, anchor: isCluster ? "center" : "bottom" }).setLngLat([lng, lat]).addTo(mapInstance);
209
+ markersRef.current.push(newMarker);
210
+ });
211
+ } catch (err) {
212
+ console.error("Update markers failed:", err);
213
+ }
200
214
  }, [mapInstance, mapLib, selectedMarkerId, markerLogoUrl, onMarkerClick]);
201
215
  (0, import_react.useEffect)(() => {
216
+ if (isRemoving.current) return;
202
217
  const points = markers.map((s) => ({
203
218
  type: "Feature",
204
219
  properties: { ...s },
@@ -208,13 +223,13 @@ var Map = ({
208
223
  updateMarkers();
209
224
  }, [markers, updateMarkers]);
210
225
  (0, import_react.useEffect)(() => {
211
- if (!mapInstance) return;
226
+ if (!mapInstance || isRemoving.current) return;
212
227
  mapInstance.on("moveend", updateMarkers);
213
228
  return () => {
214
229
  mapInstance.off("moveend", updateMarkers);
215
230
  };
216
231
  }, [mapInstance, updateMarkers]);
217
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "relative", ...style }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
232
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "relative", overflow: "hidden", ...style }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
218
233
  "div",
219
234
  {
220
235
  ref: mapContainerRef,
@@ -280,9 +295,15 @@ var InteractiveMap = (props) => {
280
295
  }
281
296
  }
282
297
  }, [props.selectedMarkerId, isMobile]);
283
- const handleStoreSelect = (store, index) => {
284
- props.onMarkerClick?.(store, index, mapInstance);
285
- };
298
+ const handleStoreSelect = (0, import_react2.useCallback)((store, index) => {
299
+ if (!mapInstance) {
300
+ console.warn("\u0646\u0642\u0634\u0647 \u0647\u0646\u0648\u0632 \u0622\u0645\u0627\u062F\u0647 \u0646\u0634\u062F\u0647 \u0627\u0633\u062A.");
301
+ return;
302
+ }
303
+ if (props.onMarkerClick) {
304
+ props.onMarkerClick(store, index, mapInstance);
305
+ }
306
+ }, [mapInstance, props]);
286
307
  const safeMarkers = props.markers || [];
287
308
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `map-layout-root ${isMobile ? "is-mobile" : "is-desktop"}`, children: [
288
309
  !isMobile && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("aside", { className: "sidebar", children: [
@@ -303,8 +324,12 @@ var InteractiveMap = (props) => {
303
324
  {
304
325
  ...props,
305
326
  onMapLoad: (map) => {
306
- setMapInstance(map);
307
- props.onMapLoad?.(map);
327
+ if (map) {
328
+ setMapInstance(map);
329
+ if (props.onMapLoad) {
330
+ props.onMapLoad(map);
331
+ }
332
+ }
308
333
  }
309
334
  }
310
335
  ),
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/components/InteractiveMap.tsx","../src/components/Map.tsx","../src/utils/createCustomMarkerElement.ts","../src/utils/createClusterElement.ts","../src/components/StoreCard.tsx"],"sourcesContent":["\r\nimport './styles.css';\r\n\r\n\r\nexport { InteractiveMap as NeshanMap } from './components/InteractiveMap';\r\n\r\n\r\nexport { default as RawMap } from './components/Map';\r\n\r\n\r\nexport type {\r\n MapProps,\r\n MapOptions,\r\n MarkerData,\r\n Store,\r\n MapboxMap,\r\n LngLatLike,\r\n} from './types';","\"use client\";\r\n\r\nimport React, { useState, useEffect, useRef } from 'react';\r\nimport Map from './Map';\r\nimport { StoreCard } from './StoreCard';\r\nimport { Store, MapProps } from '../types';\r\n\r\nexport const InteractiveMap: React.FC<MapProps> = (props) => {\r\n const [isMobile, setIsMobile] = useState(false);\r\n const [mapInstance, setMapInstance] = useState<any>(null);\r\n const sliderRef = useRef<HTMLDivElement>(null);\r\n\r\n useEffect(() => {\r\n const checkMobile = () => setIsMobile(window.innerWidth < 1024);\r\n checkMobile();\r\n window.addEventListener('resize', checkMobile);\r\n return () => window.removeEventListener('resize', checkMobile);\r\n }, []);\r\n\r\n // ✅ هماهنگی اسکرول اسلایدر موبایل با مارکر انتخاب شده\r\n useEffect(() => {\r\n if (isMobile && props.selectedMarkerId && sliderRef.current) {\r\n const selectedCard = sliderRef.current.querySelector(`[data-id=\"${props.selectedMarkerId}\"]`);\r\n if (selectedCard) {\r\n selectedCard.scrollIntoView({\r\n behavior: 'smooth',\r\n inline: 'center',\r\n block: 'nearest'\r\n });\r\n }\r\n }\r\n }, [props.selectedMarkerId, isMobile]);\r\n\r\n const handleStoreSelect = (store: Store, index: number) => {\r\n props.onMarkerClick?.(store, index, mapInstance);\r\n };\r\n\r\n const safeMarkers = props.markers || [];\r\n\r\n return (\r\n <div className={`map-layout-root ${isMobile ? 'is-mobile' : 'is-desktop'}`}>\r\n \r\n {!isMobile && (\r\n <aside className=\"sidebar\">\r\n <header className=\"sidebar-header\">لیست فروشگاه‌ها</header>\r\n <div className=\"sidebar-list\">\r\n {safeMarkers.map((store, idx) => (\r\n <StoreCard \r\n key={store.id} \r\n store={store} \r\n isSelected={props.selectedMarkerId === store.id}\r\n onSelect={() => handleStoreSelect(store, idx)}\r\n />\r\n ))}\r\n </div>\r\n </aside>\r\n )}\r\n\r\n <main className=\"map-container\">\r\n <Map \r\n {...props} \r\n onMapLoad={(map) => {\r\n setMapInstance(map);\r\n props.onMapLoad?.(map);\r\n }} \r\n />\r\n \r\n {props.productName && (\r\n <div className=\"product-badge\">\r\n <span>{props.productName}</span>\r\n </div>\r\n )}\r\n\r\n {isMobile && (\r\n <div className=\"mobile-slider\" ref={sliderRef}>\r\n {safeMarkers.map((store, idx) => (\r\n <StoreCard \r\n key={store.id} \r\n store={store} \r\n isSelected={props.selectedMarkerId === store.id}\r\n onSelect={() => handleStoreSelect(store, idx)}\r\n />\r\n ))}\r\n </div>\r\n )}\r\n </main>\r\n </div>\r\n );\r\n};","\"use client\";\r\n\r\nimport React, { useEffect, useRef, useState, useCallback } from 'react';\r\nimport type { MapboxMap, Store, MapProps } from '../types';\r\nimport { createCustomMarkerElement } from '../utils/createCustomMarkerElement';\r\nimport { createClusterElement } from '../utils/createClusterElement';\r\nimport Supercluster from 'supercluster';\r\n\r\nconst Map: React.FC<MapProps> = ({\r\n options,\r\n markers = [],\r\n markerLogoUrl = '',\r\n selectedMarkerId = null,\r\n onMarkerClick,\r\n onMapLoad,\r\n style = { width: '100%', height: '100%' },\r\n}) => {\r\n const mapContainerRef = useRef<HTMLDivElement>(null);\r\n const [mapInstance, setMapInstance] = useState<MapboxMap | null>(null);\r\n const [mapLib, setMapLib] = useState<any>(null);\r\n const markersRef = useRef<any[]>([]);\r\n \r\n const clusterIndex = useRef(new Supercluster({\r\n radius: 60,\r\n maxZoom: 16\r\n }));\r\n\r\n useEffect(() => {\r\n import('@neshan-maps-platform/mapbox-gl').then((mod) => {\r\n setMapLib(mod.default || mod);\r\n });\r\n }, []);\r\n\r\n useEffect(() => {\r\n if (!mapLib || !mapContainerRef.current || mapInstance) return;\r\n const map = new mapLib.Map({\r\n container: mapContainerRef.current,\r\n ...options,\r\n });\r\n map.on('load', () => {\r\n setMapInstance(map);\r\n onMapLoad?.(map);\r\n });\r\n return () => { if (map) map.remove(); };\r\n }, [mapLib, options, onMapLoad]);\r\n\r\n useEffect(() => {\r\n if (!mapInstance || !selectedMarkerId) return;\r\n const selectedStore = markers.find(m => m.id === selectedMarkerId);\r\n if (selectedStore) {\r\n mapInstance.flyTo({\r\n center: [selectedStore.lng, selectedStore.lat],\r\n zoom: 16, \r\n essential: true,\r\n duration: 1500\r\n });\r\n }\r\n }, [selectedMarkerId, mapInstance, markers]);\r\n\r\n const updateMarkers = useCallback(() => {\r\n if (!mapInstance || !mapLib) return;\r\n\r\n const bounds = mapInstance.getBounds();\r\n const zoom = Math.floor(mapInstance.getZoom());\r\n\r\n const clusters = clusterIndex.current.getClusters(\r\n [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()],\r\n zoom\r\n );\r\n\r\n markersRef.current.forEach(m => m.remove());\r\n markersRef.current = [];\r\n\r\n clusters.forEach(cluster => {\r\n const [lng, lat] = cluster.geometry.coordinates;\r\n const { cluster: isCluster, point_count: count } = cluster.properties;\r\n\r\n let el: HTMLElement;\r\n\r\n if (isCluster) {\r\n el = createClusterElement(count);\r\n el.onclick = (e) => {\r\n e.stopPropagation();\r\n const expansionZoom = Math.min(\r\n clusterIndex.current.getClusterExpansionZoom(cluster.id as number),\r\n 18\r\n );\r\n mapInstance.easeTo({ \r\n center: [lng, lat], \r\n zoom: expansionZoom,\r\n duration: 800 \r\n });\r\n };\r\n } else {\r\n const storeData = cluster.properties as Store;\r\n el = createCustomMarkerElement({\r\n marker: storeData,\r\n isSelected: storeData.id === selectedMarkerId,\r\n logoSrc: markerLogoUrl,\r\n });\r\n el.onclick = () => onMarkerClick?.(storeData, 0, mapInstance);\r\n }\r\n\r\n const newMarker = new mapLib.Marker({ \r\n element: el, \r\n anchor: isCluster ? 'center' : 'bottom' \r\n })\r\n .setLngLat([lng, lat])\r\n .addTo(mapInstance);\r\n \r\n markersRef.current.push(newMarker);\r\n });\r\n }, [mapInstance, mapLib, selectedMarkerId, markerLogoUrl, onMarkerClick]);\r\n\r\n useEffect(() => {\r\n const points: any = markers.map(s => ({\r\n type: 'Feature',\r\n properties: { ...s },\r\n geometry: { type: 'Point', coordinates: [s.lng, s.lat] }\r\n }));\r\n clusterIndex.current.load(points);\r\n updateMarkers();\r\n }, [markers, updateMarkers]);\r\n\r\n useEffect(() => {\r\n if (!mapInstance) return;\r\n mapInstance.on('moveend', updateMarkers);\r\n return () => { mapInstance.off('moveend', updateMarkers); };\r\n }, [mapInstance, updateMarkers]);\r\n\r\n return (\r\n <div style={{ position: 'relative', ...style }}>\r\n <div \r\n ref={mapContainerRef} \r\n style={{ width: '100%', height: '100%', backfaceVisibility: 'hidden' }} \r\n />\r\n </div>\r\n );\r\n};\r\n\r\nexport default Map;","import type { MarkerData } from '../types';\r\n\r\ninterface CreateMarkerOptions {\r\n marker: MarkerData;\r\n isSelected: boolean;\r\n logoSrc?: string;\r\n showPrice?: boolean;\r\n}\r\n\r\nexport function createCustomMarkerElement({\r\n marker,\r\n isSelected,\r\n logoSrc = '',\r\n showPrice = true,\r\n}: CreateMarkerOptions): HTMLElement {\r\n \r\n const container = document.createElement('div');\r\n container.className = 'neshan-marker-container';\r\n\r\n \r\n const body = document.createElement('div');\r\n body.className = `neshan-marker-body ${isSelected ? 'neshan-marker-selected' : ''}`;\r\n \r\n \r\n Object.assign(body.style, {\r\n display: 'flex',\r\n flexDirection: 'column',\r\n alignItems: 'center',\r\n transition: 'transform 0.3s ease',\r\n transform: isSelected ? 'scale(1.2)' : 'scale(1)',\r\n });\r\n\r\n \r\n const iconBox = document.createElement('div');\r\n Object.assign(iconBox.style, {\r\n width: '36px',\r\n height: '36px',\r\n borderRadius: '50%',\r\n\r\n border: isSelected ? '3px solid #3b82f6' : '2px solid #ef4444',\r\n boxShadow: '0 4px 8px rgba(0,0,0,0.2)',\r\n display: 'flex',\r\n alignItems: 'center',\r\n justifyContent: 'center',\r\n overflow: 'hidden'\r\n });\r\n\r\n if (logoSrc) {\r\n const img = document.createElement('img');\r\n img.src = logoSrc;\r\n img.style.width = '100%';\r\n img.style.height = '100%';\r\n img.style.objectFit = 'cover';\r\n iconBox.appendChild(img);\r\n } else {\r\n iconBox.style.backgroundColor = '#ef4444';\r\n }\r\n\r\n body.appendChild(iconBox);\r\n\r\n \r\n if (showPrice && (marker.price || marker.name)) {\r\n const label = document.createElement('div');\r\n label.className = 'neshan-marker-label'; // استفاده از کلاس CSS جدید\r\n label.textContent = marker.price ? `${marker.price} ت` : (marker.name ?? '');\r\n body.appendChild(label);\r\n}\r\n\r\n container.appendChild(body);\r\n return container;\r\n}","export function createClusterElement(count: number): HTMLElement {\r\n const container = document.createElement('div');\r\n container.className = 'neshan-cluster-marker';\r\n \r\n \r\n const countSpan = document.createElement('span');\r\n countSpan.className = 'neshan-cluster-count';\r\n countSpan.innerText = count.toString();\r\n \r\n container.appendChild(countSpan);\r\n return container;\r\n}","import React from 'react';\r\nimport { Store } from '../types';\r\n\r\ninterface StoreCardProps {\r\n store: Store;\r\n isSelected: boolean;\r\n onSelect: () => void;\r\n}\r\n\r\nexport const StoreCard: React.FC<StoreCardProps> = ({ store, isSelected, onSelect }) => {\r\n const googleMapUrl = `https://www.google.com/maps/dir/?api=1&destination=${store.lat},${store.lng}`;\r\n\r\n return (\r\n <div \r\n className={`store-card ${isSelected ? 'active' : ''}`}\r\n onClick={onSelect}\r\n data-id={store.id} // برای پیدا کردن کارت در اسکرول موبایل\r\n >\r\n <div className=\"store-card-header\">\r\n <div>\r\n <h4 className=\"store-name\">{store.name}</h4>\r\n <span className=\"store-city\">{store.city}</span>\r\n </div>\r\n <div className=\"store-price\">{store.price} تومان</div>\r\n </div>\r\n\r\n <p className=\"store-desc\">{store.desc}</p>\r\n\r\n {/* بخش آکاردئونی با CSS Animation */}\r\n <div className=\"store-details-wrapper\">\r\n <div className=\"store-details-content\">\r\n <div className=\"action-buttons\">\r\n {store.phone && (\r\n <a href={`tel:${store.phone}`} className=\"btn btn-call\" onClick={(e) => e.stopPropagation()}>\r\n <span>تماس</span>\r\n </a>\r\n )}\r\n {store.whatsapp && (\r\n <a href={`https://wa.me/${store.whatsapp}`} target=\"_blank\" className=\"btn btn-wa\" onClick={(e) => e.stopPropagation()}>\r\n <span>واتساپ</span>\r\n </a>\r\n )}\r\n <a href={googleMapUrl} target=\"_blank\" className=\"btn btn-route\" onClick={(e) => e.stopPropagation()}>\r\n <span>مسیریابی</span>\r\n </a>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n};"],"mappings":"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","names":["import_react","Supercluster","import_jsx_runtime","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/components/InteractiveMap.tsx","../src/components/Map.tsx","../src/utils/createCustomMarkerElement.ts","../src/utils/createClusterElement.ts","../src/components/StoreCard.tsx"],"sourcesContent":["\r\nimport './styles.css';\r\n\r\n\r\nexport { InteractiveMap as NeshanMap } from './components/InteractiveMap';\r\n\r\n\r\nexport { default as RawMap } from './components/Map';\r\n\r\n\r\nexport type {\r\n MapProps,\r\n MapOptions,\r\n MarkerData,\r\n Store,\r\n MapboxMap,\r\n LngLatLike,\r\n} from './types';","\"use client\";\r\n\r\nimport React, { useState, useEffect, useRef, useCallback } from 'react';\r\nimport Map from './Map';\r\nimport { StoreCard } from './StoreCard';\r\nimport { Store, MapProps } from '../types';\r\n\r\nexport const InteractiveMap: React.FC<MapProps> = (props) => {\r\n const [isMobile, setIsMobile] = useState(false);\r\n const [mapInstance, setMapInstance] = useState<any>(null);\r\n const sliderRef = useRef<HTMLDivElement>(null);\r\n\r\n \r\n useEffect(() => {\r\n const checkMobile = () => setIsMobile(window.innerWidth < 1024);\r\n checkMobile();\r\n window.addEventListener('resize', checkMobile);\r\n return () => window.removeEventListener('resize', checkMobile);\r\n }, []);\r\n\r\n \r\n useEffect(() => {\r\n if (isMobile && props.selectedMarkerId && sliderRef.current) {\r\n const selectedCard = sliderRef.current.querySelector(`[data-id=\"${props.selectedMarkerId}\"]`);\r\n if (selectedCard) {\r\n selectedCard.scrollIntoView({\r\n behavior: 'smooth',\r\n inline: 'center',\r\n block: 'nearest'\r\n });\r\n }\r\n }\r\n }, [props.selectedMarkerId, isMobile]);\r\n\r\n \r\n const handleStoreSelect = useCallback((store: Store, index: number) => {\r\n \r\n if (!mapInstance) {\r\n console.warn(\"نقشه هنوز آماده نشده است.\");\r\n return;\r\n }\r\n \r\n \r\n if (props.onMarkerClick) {\r\n props.onMarkerClick(store, index, mapInstance);\r\n }\r\n }, [mapInstance, props]);\r\n\r\n const safeMarkers = props.markers || [];\r\n\r\n return (\r\n <div className={`map-layout-root ${isMobile ? 'is-mobile' : 'is-desktop'}`}>\r\n \r\n {/* سایدبار دسکتاپ */}\r\n {!isMobile && (\r\n <aside className=\"sidebar\">\r\n <header className=\"sidebar-header\">لیست فروشگاه‌ها</header>\r\n <div className=\"sidebar-list\">\r\n {safeMarkers.map((store, idx) => (\r\n <StoreCard \r\n key={store.id} \r\n store={store} \r\n isSelected={props.selectedMarkerId === store.id}\r\n onSelect={() => handleStoreSelect(store, idx)}\r\n />\r\n ))}\r\n </div>\r\n </aside>\r\n )}\r\n\r\n {/* بخش نقشه */}\r\n <main className=\"map-container\">\r\n <Map \r\n {...props} \r\n onMapLoad={(map) => {\r\n \r\n if (map) {\r\n setMapInstance(map);\r\n if (props.onMapLoad) {\r\n props.onMapLoad(map);\r\n }\r\n }\r\n }} \r\n />\r\n \r\n {/* باج محصول */}\r\n {props.productName && (\r\n <div className=\"product-badge\">\r\n <span>{props.productName}</span>\r\n </div>\r\n )}\r\n\r\n {/* اسلایدر موبایل */}\r\n {isMobile && (\r\n <div className=\"mobile-slider\" ref={sliderRef}>\r\n {safeMarkers.map((store, idx) => (\r\n <StoreCard \r\n key={store.id} \r\n store={store} \r\n isSelected={props.selectedMarkerId === store.id}\r\n onSelect={() => handleStoreSelect(store, idx)}\r\n />\r\n ))}\r\n </div>\r\n )}\r\n </main>\r\n </div>\r\n );\r\n};","\"use client\";\r\n\r\nimport React, { useEffect, useRef, useState, useCallback } from 'react';\r\nimport type { MapboxMap, Store, MapProps } from '../types';\r\nimport { createCustomMarkerElement } from '../utils/createCustomMarkerElement';\r\nimport { createClusterElement } from '../utils/createClusterElement';\r\nimport Supercluster from 'supercluster';\r\n\r\nimport '@neshan-maps-platform/mapbox-gl/dist/NeshanMapboxGl.css';\r\n\r\nconst Map: React.FC<MapProps> = ({\r\n options,\r\n markers = [],\r\n markerLogoUrl = '',\r\n selectedMarkerId = null,\r\n onMarkerClick,\r\n onMapLoad,\r\n style = { width: '100%', height: '100%' },\r\n}) => {\r\n const mapContainerRef = useRef<HTMLDivElement>(null);\r\n const [mapInstance, setMapInstance] = useState<MapboxMap | null>(null);\r\n const [mapLib, setMapLib] = useState<any>(null);\r\n const markersRef = useRef<any[]>([]);\r\n const isRemoving = useRef(false); \r\n \r\n const clusterIndex = useRef(new Supercluster({\r\n radius: 60,\r\n maxZoom: 16\r\n }));\r\n\r\n \r\n useEffect(() => {\r\n let mounted = true;\r\n import('@neshan-maps-platform/mapbox-gl').then((mod) => {\r\n if (mounted) setMapLib(mod.default || mod);\r\n });\r\n return () => { mounted = false; };\r\n }, []);\r\n\r\n \r\n useEffect(() => {\r\n if (!mapLib || !mapContainerRef.current || mapInstance) return;\r\n\r\n const map = new mapLib.Map({\r\n container: mapContainerRef.current,\r\n ...options,\r\n });\r\n\r\n map.on('load', () => {\r\n if (!isRemoving.current) {\r\n setMapInstance(map);\r\n onMapLoad?.(map);\r\n }\r\n });\r\n\r\n return () => {\r\n if (map) {\r\n isRemoving.current = true;\r\n \r\n setTimeout(() => {\r\n try {\r\n map.remove();\r\n } catch (e) {\r\n console.warn(\"Mapbox remove error ignored:\", e);\r\n }\r\n }, 0);\r\n }\r\n };\r\n \r\n }, [mapLib]); \r\n\r\n \r\n useEffect(() => {\r\n if (!mapInstance || !selectedMarkerId || isRemoving.current) return;\r\n const selectedStore = markers.find(m => m.id === selectedMarkerId);\r\n if (selectedStore) {\r\n mapInstance.flyTo({\r\n center: [selectedStore.lng, selectedStore.lat],\r\n zoom: 16, \r\n essential: true,\r\n duration: 1200\r\n });\r\n }\r\n }, [selectedMarkerId, mapInstance, markers]);\r\n\r\n \r\n const updateMarkers = useCallback(() => {\r\n if (!mapInstance || !mapLib || isRemoving.current) return;\r\n\r\n try {\r\n const bounds = mapInstance.getBounds();\r\n const zoom = Math.floor(mapInstance.getZoom());\r\n\r\n const clusters = clusterIndex.current.getClusters(\r\n [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()],\r\n zoom\r\n );\r\n\r\n markersRef.current.forEach(m => m.remove());\r\n markersRef.current = [];\r\n\r\n clusters.forEach(cluster => {\r\n const [lng, lat] = cluster.geometry.coordinates;\r\n const { cluster: isCluster, point_count: count } = cluster.properties;\r\n\r\n let el: HTMLElement;\r\n\r\n if (isCluster) {\r\n el = createClusterElement(count);\r\n el.onclick = (e) => {\r\n e.stopPropagation();\r\n const expansionZoom = Math.min(\r\n clusterIndex.current.getClusterExpansionZoom(cluster.id as number),\r\n 18\r\n );\r\n mapInstance.easeTo({ center: [lng, lat], zoom: expansionZoom });\r\n };\r\n } else {\r\n const storeData = cluster.properties as Store;\r\n el = createCustomMarkerElement({\r\n marker: storeData,\r\n isSelected: storeData.id === selectedMarkerId,\r\n logoSrc: markerLogoUrl,\r\n });\r\n el.onclick = () => onMarkerClick?.(storeData, 0, mapInstance);\r\n }\r\n\r\n const newMarker = new mapLib.Marker({ element: el, anchor: isCluster ? 'center' : 'bottom' })\r\n .setLngLat([lng, lat])\r\n .addTo(mapInstance);\r\n \r\n markersRef.current.push(newMarker);\r\n });\r\n } catch (err) {\r\n console.error(\"Update markers failed:\", err);\r\n }\r\n }, [mapInstance, mapLib, selectedMarkerId, markerLogoUrl, onMarkerClick]);\r\n\r\n \r\n useEffect(() => {\r\n if (isRemoving.current) return;\r\n const points: any = markers.map(s => ({\r\n type: 'Feature',\r\n properties: { ...s },\r\n geometry: { type: 'Point', coordinates: [s.lng, s.lat] }\r\n }));\r\n clusterIndex.current.load(points);\r\n updateMarkers();\r\n }, [markers, updateMarkers]);\r\n\r\n \r\n useEffect(() => {\r\n if (!mapInstance || isRemoving.current) return;\r\n mapInstance.on('moveend', updateMarkers);\r\n return () => { mapInstance.off('moveend', updateMarkers); };\r\n }, [mapInstance, updateMarkers]);\r\n\r\n return (\r\n <div style={{ position: 'relative', overflow: 'hidden', ...style }}>\r\n <div \r\n ref={mapContainerRef} \r\n style={{ width: '100%', height: '100%', backfaceVisibility: 'hidden' }} \r\n />\r\n </div>\r\n );\r\n};\r\n\r\nexport default Map;","import type { MarkerData } from '../types';\r\n\r\ninterface CreateMarkerOptions {\r\n marker: MarkerData;\r\n isSelected: boolean;\r\n logoSrc?: string;\r\n showPrice?: boolean;\r\n}\r\n\r\nexport function createCustomMarkerElement({\r\n marker,\r\n isSelected,\r\n logoSrc = '',\r\n showPrice = true,\r\n}: CreateMarkerOptions): HTMLElement {\r\n \r\n const container = document.createElement('div');\r\n container.className = 'neshan-marker-container';\r\n\r\n \r\n const body = document.createElement('div');\r\n body.className = `neshan-marker-body ${isSelected ? 'neshan-marker-selected' : ''}`;\r\n \r\n \r\n Object.assign(body.style, {\r\n display: 'flex',\r\n flexDirection: 'column',\r\n alignItems: 'center',\r\n transition: 'transform 0.3s ease',\r\n transform: isSelected ? 'scale(1.2)' : 'scale(1)',\r\n });\r\n\r\n \r\n const iconBox = document.createElement('div');\r\n Object.assign(iconBox.style, {\r\n width: '36px',\r\n height: '36px',\r\n borderRadius: '50%',\r\n\r\n border: isSelected ? '3px solid #3b82f6' : '2px solid #ef4444',\r\n boxShadow: '0 4px 8px rgba(0,0,0,0.2)',\r\n display: 'flex',\r\n alignItems: 'center',\r\n justifyContent: 'center',\r\n overflow: 'hidden'\r\n });\r\n\r\n if (logoSrc) {\r\n const img = document.createElement('img');\r\n img.src = logoSrc;\r\n img.style.width = '100%';\r\n img.style.height = '100%';\r\n img.style.objectFit = 'cover';\r\n iconBox.appendChild(img);\r\n } else {\r\n iconBox.style.backgroundColor = '#ef4444';\r\n }\r\n\r\n body.appendChild(iconBox);\r\n\r\n \r\n if (showPrice && (marker.price || marker.name)) {\r\n const label = document.createElement('div');\r\n label.className = 'neshan-marker-label'; // استفاده از کلاس CSS جدید\r\n label.textContent = marker.price ? `${marker.price} ت` : (marker.name ?? '');\r\n body.appendChild(label);\r\n}\r\n\r\n container.appendChild(body);\r\n return container;\r\n}","export function createClusterElement(count: number): HTMLElement {\r\n const container = document.createElement('div');\r\n container.className = 'neshan-cluster-marker';\r\n \r\n \r\n const countSpan = document.createElement('span');\r\n countSpan.className = 'neshan-cluster-count';\r\n countSpan.innerText = count.toString();\r\n \r\n container.appendChild(countSpan);\r\n return container;\r\n}","import React from 'react';\r\nimport { Store } from '../types';\r\n\r\ninterface StoreCardProps {\r\n store: Store;\r\n isSelected: boolean;\r\n onSelect: () => void;\r\n}\r\n\r\nexport const StoreCard: React.FC<StoreCardProps> = ({ store, isSelected, onSelect }) => {\r\n const googleMapUrl = `https://www.google.com/maps/dir/?api=1&destination=${store.lat},${store.lng}`;\r\n\r\n return (\r\n <div \r\n className={`store-card ${isSelected ? 'active' : ''}`}\r\n onClick={onSelect}\r\n data-id={store.id} // برای پیدا کردن کارت در اسکرول موبایل\r\n >\r\n <div className=\"store-card-header\">\r\n <div>\r\n <h4 className=\"store-name\">{store.name}</h4>\r\n <span className=\"store-city\">{store.city}</span>\r\n </div>\r\n <div className=\"store-price\">{store.price} تومان</div>\r\n </div>\r\n\r\n <p className=\"store-desc\">{store.desc}</p>\r\n\r\n {/* بخش آکاردئونی با CSS Animation */}\r\n <div className=\"store-details-wrapper\">\r\n <div className=\"store-details-content\">\r\n <div className=\"action-buttons\">\r\n {store.phone && (\r\n <a href={`tel:${store.phone}`} className=\"btn btn-call\" onClick={(e) => e.stopPropagation()}>\r\n <span>تماس</span>\r\n </a>\r\n )}\r\n {store.whatsapp && (\r\n <a href={`https://wa.me/${store.whatsapp}`} target=\"_blank\" className=\"btn btn-wa\" onClick={(e) => e.stopPropagation()}>\r\n <span>واتساپ</span>\r\n </a>\r\n )}\r\n <a href={googleMapUrl} target=\"_blank\" className=\"btn btn-route\" onClick={(e) => e.stopPropagation()}>\r\n <span>مسیریابی</span>\r\n </a>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n};"],"mappings":"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","names":["import_react","Supercluster","import_jsx_runtime","import_jsx_runtime"]}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/InteractiveMap.tsx
2
- import { useState as useState2, useEffect as useEffect2, useRef as useRef2 } from "react";
2
+ import { useState as useState2, useEffect as useEffect2, useRef as useRef2, useCallback as useCallback2 } from "react";
3
3
 
4
4
  // src/components/Map.tsx
5
5
  import { useEffect, useRef, useState, useCallback } from "react";
@@ -68,6 +68,7 @@ function createClusterElement(count) {
68
68
 
69
69
  // src/components/Map.tsx
70
70
  import Supercluster from "supercluster";
71
+ import "@neshan-maps-platform/mapbox-gl/dist/NeshanMapboxGl.css";
71
72
  import { jsx } from "react/jsx-runtime";
72
73
  var Map = ({
73
74
  options,
@@ -82,14 +83,19 @@ var Map = ({
82
83
  const [mapInstance, setMapInstance] = useState(null);
83
84
  const [mapLib, setMapLib] = useState(null);
84
85
  const markersRef = useRef([]);
86
+ const isRemoving = useRef(false);
85
87
  const clusterIndex = useRef(new Supercluster({
86
88
  radius: 60,
87
89
  maxZoom: 16
88
90
  }));
89
91
  useEffect(() => {
92
+ let mounted = true;
90
93
  import("@neshan-maps-platform/mapbox-gl").then((mod) => {
91
- setMapLib(mod.default || mod);
94
+ if (mounted) setMapLib(mod.default || mod);
92
95
  });
96
+ return () => {
97
+ mounted = false;
98
+ };
93
99
  }, []);
94
100
  useEffect(() => {
95
101
  if (!mapLib || !mapContainerRef.current || mapInstance) return;
@@ -98,70 +104,79 @@ var Map = ({
98
104
  ...options
99
105
  });
100
106
  map.on("load", () => {
101
- setMapInstance(map);
102
- onMapLoad?.(map);
107
+ if (!isRemoving.current) {
108
+ setMapInstance(map);
109
+ onMapLoad?.(map);
110
+ }
103
111
  });
104
112
  return () => {
105
- if (map) map.remove();
113
+ if (map) {
114
+ isRemoving.current = true;
115
+ setTimeout(() => {
116
+ try {
117
+ map.remove();
118
+ } catch (e) {
119
+ console.warn("Mapbox remove error ignored:", e);
120
+ }
121
+ }, 0);
122
+ }
106
123
  };
107
- }, [mapLib, options, onMapLoad]);
124
+ }, [mapLib]);
108
125
  useEffect(() => {
109
- if (!mapInstance || !selectedMarkerId) return;
126
+ if (!mapInstance || !selectedMarkerId || isRemoving.current) return;
110
127
  const selectedStore = markers.find((m) => m.id === selectedMarkerId);
111
128
  if (selectedStore) {
112
129
  mapInstance.flyTo({
113
130
  center: [selectedStore.lng, selectedStore.lat],
114
131
  zoom: 16,
115
132
  essential: true,
116
- duration: 1500
133
+ duration: 1200
117
134
  });
118
135
  }
119
136
  }, [selectedMarkerId, mapInstance, markers]);
120
137
  const updateMarkers = useCallback(() => {
121
- if (!mapInstance || !mapLib) return;
122
- const bounds = mapInstance.getBounds();
123
- const zoom = Math.floor(mapInstance.getZoom());
124
- const clusters = clusterIndex.current.getClusters(
125
- [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()],
126
- zoom
127
- );
128
- markersRef.current.forEach((m) => m.remove());
129
- markersRef.current = [];
130
- clusters.forEach((cluster) => {
131
- const [lng, lat] = cluster.geometry.coordinates;
132
- const { cluster: isCluster, point_count: count } = cluster.properties;
133
- let el;
134
- if (isCluster) {
135
- el = createClusterElement(count);
136
- el.onclick = (e) => {
137
- e.stopPropagation();
138
- const expansionZoom = Math.min(
139
- clusterIndex.current.getClusterExpansionZoom(cluster.id),
140
- 18
141
- );
142
- mapInstance.easeTo({
143
- center: [lng, lat],
144
- zoom: expansionZoom,
145
- duration: 800
138
+ if (!mapInstance || !mapLib || isRemoving.current) return;
139
+ try {
140
+ const bounds = mapInstance.getBounds();
141
+ const zoom = Math.floor(mapInstance.getZoom());
142
+ const clusters = clusterIndex.current.getClusters(
143
+ [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()],
144
+ zoom
145
+ );
146
+ markersRef.current.forEach((m) => m.remove());
147
+ markersRef.current = [];
148
+ clusters.forEach((cluster) => {
149
+ const [lng, lat] = cluster.geometry.coordinates;
150
+ const { cluster: isCluster, point_count: count } = cluster.properties;
151
+ let el;
152
+ if (isCluster) {
153
+ el = createClusterElement(count);
154
+ el.onclick = (e) => {
155
+ e.stopPropagation();
156
+ const expansionZoom = Math.min(
157
+ clusterIndex.current.getClusterExpansionZoom(cluster.id),
158
+ 18
159
+ );
160
+ mapInstance.easeTo({ center: [lng, lat], zoom: expansionZoom });
161
+ };
162
+ } else {
163
+ const storeData = cluster.properties;
164
+ el = createCustomMarkerElement({
165
+ marker: storeData,
166
+ isSelected: storeData.id === selectedMarkerId,
167
+ logoSrc: markerLogoUrl
146
168
  });
147
- };
148
- } else {
149
- const storeData = cluster.properties;
150
- el = createCustomMarkerElement({
151
- marker: storeData,
152
- isSelected: storeData.id === selectedMarkerId,
153
- logoSrc: markerLogoUrl
154
- });
155
- el.onclick = () => onMarkerClick?.(storeData, 0, mapInstance);
156
- }
157
- const newMarker = new mapLib.Marker({
158
- element: el,
159
- anchor: isCluster ? "center" : "bottom"
160
- }).setLngLat([lng, lat]).addTo(mapInstance);
161
- markersRef.current.push(newMarker);
162
- });
169
+ el.onclick = () => onMarkerClick?.(storeData, 0, mapInstance);
170
+ }
171
+ const newMarker = new mapLib.Marker({ element: el, anchor: isCluster ? "center" : "bottom" }).setLngLat([lng, lat]).addTo(mapInstance);
172
+ markersRef.current.push(newMarker);
173
+ });
174
+ } catch (err) {
175
+ console.error("Update markers failed:", err);
176
+ }
163
177
  }, [mapInstance, mapLib, selectedMarkerId, markerLogoUrl, onMarkerClick]);
164
178
  useEffect(() => {
179
+ if (isRemoving.current) return;
165
180
  const points = markers.map((s) => ({
166
181
  type: "Feature",
167
182
  properties: { ...s },
@@ -171,13 +186,13 @@ var Map = ({
171
186
  updateMarkers();
172
187
  }, [markers, updateMarkers]);
173
188
  useEffect(() => {
174
- if (!mapInstance) return;
189
+ if (!mapInstance || isRemoving.current) return;
175
190
  mapInstance.on("moveend", updateMarkers);
176
191
  return () => {
177
192
  mapInstance.off("moveend", updateMarkers);
178
193
  };
179
194
  }, [mapInstance, updateMarkers]);
180
- return /* @__PURE__ */ jsx("div", { style: { position: "relative", ...style }, children: /* @__PURE__ */ jsx(
195
+ return /* @__PURE__ */ jsx("div", { style: { position: "relative", overflow: "hidden", ...style }, children: /* @__PURE__ */ jsx(
181
196
  "div",
182
197
  {
183
198
  ref: mapContainerRef,
@@ -243,9 +258,15 @@ var InteractiveMap = (props) => {
243
258
  }
244
259
  }
245
260
  }, [props.selectedMarkerId, isMobile]);
246
- const handleStoreSelect = (store, index) => {
247
- props.onMarkerClick?.(store, index, mapInstance);
248
- };
261
+ const handleStoreSelect = useCallback2((store, index) => {
262
+ if (!mapInstance) {
263
+ console.warn("\u0646\u0642\u0634\u0647 \u0647\u0646\u0648\u0632 \u0622\u0645\u0627\u062F\u0647 \u0646\u0634\u062F\u0647 \u0627\u0633\u062A.");
264
+ return;
265
+ }
266
+ if (props.onMarkerClick) {
267
+ props.onMarkerClick(store, index, mapInstance);
268
+ }
269
+ }, [mapInstance, props]);
249
270
  const safeMarkers = props.markers || [];
250
271
  return /* @__PURE__ */ jsxs2("div", { className: `map-layout-root ${isMobile ? "is-mobile" : "is-desktop"}`, children: [
251
272
  !isMobile && /* @__PURE__ */ jsxs2("aside", { className: "sidebar", children: [
@@ -266,8 +287,12 @@ var InteractiveMap = (props) => {
266
287
  {
267
288
  ...props,
268
289
  onMapLoad: (map) => {
269
- setMapInstance(map);
270
- props.onMapLoad?.(map);
290
+ if (map) {
291
+ setMapInstance(map);
292
+ if (props.onMapLoad) {
293
+ props.onMapLoad(map);
294
+ }
295
+ }
271
296
  }
272
297
  }
273
298
  ),