mehdi-akbari-map 0.1.1 → 0.1.5

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.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { useState as useState2, useEffect as useEffect2 } from "react";
3
3
 
4
4
  // src/components/Map.tsx
5
- import { useEffect, useRef, useState } from "react";
5
+ import { useEffect, useRef, useState, useCallback } from "react";
6
6
 
7
7
  // src/utils/createCustomMarkerElement.ts
8
8
  function createCustomMarkerElement({
@@ -56,7 +56,16 @@ function createCustomMarkerElement({
56
56
  return container;
57
57
  }
58
58
 
59
+ // src/utils/createClusterElement.ts
60
+ function createClusterElement(count) {
61
+ const el = document.createElement("div");
62
+ el.className = "neshan-cluster-marker";
63
+ el.innerHTML = `<span class="neshan-cluster-count">${count}</span>`;
64
+ return el;
65
+ }
66
+
59
67
  // src/components/Map.tsx
68
+ import Supercluster from "supercluster";
60
69
  import "@neshan-maps-platform/mapbox-gl/dist/NeshanMapboxGl.css";
61
70
  import { jsx } from "react/jsx-runtime";
62
71
  var Map = ({
@@ -66,23 +75,20 @@ var Map = ({
66
75
  selectedMarkerId = null,
67
76
  onMarkerClick,
68
77
  onMapLoad,
69
- className = "",
70
78
  style = { width: "100%", height: "100%" }
71
79
  }) => {
72
80
  const mapContainerRef = useRef(null);
73
81
  const [mapInstance, setMapInstance] = useState(null);
74
82
  const [mapLib, setMapLib] = useState(null);
75
83
  const markersRef = useRef([]);
84
+ const clusterIndex = useRef(new Supercluster({
85
+ radius: 60,
86
+ maxZoom: 16
87
+ }));
76
88
  useEffect(() => {
77
- let isMounted = true;
78
89
  import("@neshan-maps-platform/mapbox-gl").then((mod) => {
79
- if (isMounted) {
80
- setMapLib(mod.default || mod);
81
- }
90
+ setMapLib(mod.default || mod);
82
91
  });
83
- return () => {
84
- isMounted = false;
85
- };
86
92
  }, []);
87
93
  useEffect(() => {
88
94
  if (!mapLib || !mapContainerRef.current || mapInstance) return;
@@ -95,56 +101,75 @@ var Map = ({
95
101
  onMapLoad?.(map);
96
102
  });
97
103
  return () => {
98
- if (map) {
99
- map.remove();
100
- setMapInstance(null);
101
- }
104
+ if (map) map.remove();
102
105
  };
103
106
  }, [mapLib]);
104
107
  useEffect(() => {
108
+ if (!mapInstance || !selectedMarkerId) return;
109
+ const selectedStore = markers.find((m) => m.id === selectedMarkerId);
110
+ if (selectedStore) {
111
+ mapInstance.flyTo({
112
+ center: [selectedStore.lng, selectedStore.lat],
113
+ zoom: 16,
114
+ // زوم نزدیک هنگام انتخاب
115
+ essential: true,
116
+ duration: 1500
117
+ });
118
+ }
119
+ }, [selectedMarkerId, mapInstance, markers]);
120
+ const updateMarkers = useCallback(() => {
105
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
+ );
106
128
  markersRef.current.forEach((m) => m.remove());
107
129
  markersRef.current = [];
108
- markers.forEach((markerData, index) => {
109
- const isSelected = markerData.id === selectedMarkerId;
110
- const el = createCustomMarkerElement({
111
- marker: markerData,
112
- isSelected,
113
- logoSrc: markerLogoUrl
114
- });
115
- const marker = new mapLib.Marker({
116
- element: el,
117
- anchor: "bottom",
118
- offset: [0, 0]
119
- }).setLngLat([markerData.lng, markerData.lat]).addTo(mapInstance);
120
- el.addEventListener("click", (e) => {
121
- e.stopPropagation();
122
- onMarkerClick?.(markerData, index, mapInstance);
123
- });
124
- markersRef.current.push(marker);
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 = () => {
137
+ const expansionZoom = Math.min(
138
+ clusterIndex.current.getClusterExpansionZoom(cluster.id),
139
+ 18
140
+ );
141
+ mapInstance.easeTo({ center: [lng, lat], zoom: expansionZoom });
142
+ };
143
+ } else {
144
+ const storeData = cluster.properties;
145
+ el = createCustomMarkerElement({
146
+ marker: storeData,
147
+ isSelected: storeData.id === selectedMarkerId,
148
+ logoSrc: markerLogoUrl
149
+ });
150
+ el.onclick = () => onMarkerClick?.(storeData, 0, mapInstance);
151
+ }
152
+ const newMarker = new mapLib.Marker({ element: el, anchor: isCluster ? "center" : "bottom" }).setLngLat([lng, lat]).addTo(mapInstance);
153
+ markersRef.current.push(newMarker);
125
154
  });
155
+ }, [mapInstance, mapLib, selectedMarkerId, markerLogoUrl, onMarkerClick]);
156
+ useEffect(() => {
157
+ const points = markers.map((s) => ({
158
+ type: "Feature",
159
+ properties: { ...s },
160
+ geometry: { type: "Point", coordinates: [s.lng, s.lat] }
161
+ }));
162
+ clusterIndex.current.load(points);
163
+ updateMarkers();
164
+ }, [markers, updateMarkers]);
165
+ useEffect(() => {
166
+ if (!mapInstance) return;
167
+ mapInstance.on("moveend", updateMarkers);
126
168
  return () => {
127
- markersRef.current.forEach((m) => m.remove());
169
+ mapInstance.off("moveend", updateMarkers);
128
170
  };
129
- }, [markers, mapInstance, mapLib, selectedMarkerId, markerLogoUrl, onMarkerClick]);
130
- return /* @__PURE__ */ jsx(
131
- "div",
132
- {
133
- style: {
134
- position: "relative",
135
- overflow: "hidden",
136
- ...style
137
- },
138
- className: `neshan-map-wrapper ${className}`,
139
- children: /* @__PURE__ */ jsx(
140
- "div",
141
- {
142
- ref: mapContainerRef,
143
- style: { width: "100%", height: "100%" }
144
- }
145
- )
146
- }
147
- );
171
+ }, [mapInstance, updateMarkers]);
172
+ return /* @__PURE__ */ jsx("div", { style: { position: "relative", ...style }, children: /* @__PURE__ */ jsx("div", { ref: mapContainerRef, style: { width: "100%", height: "100%" } }) });
148
173
  };
149
174
  var Map_default = Map;
150
175
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/InteractiveMap.tsx","../src/components/Map.tsx","../src/utils/createCustomMarkerElement.ts","../src/components/StoreCard.tsx"],"sourcesContent":["\"use client\";\r\n\r\nimport React, { useState, useEffect } 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 // ✅ ذخیره نمونه نقشه برای پاس دادن به کلیک مارکر\r\n const [mapInstance, setMapInstance] = useState<any>(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 const handleStoreSelect = (store: Store, index: number) => {\r\n // ✅ حالا دیگر null نمی‌فرستیم، نمونه واقعی نقشه را می‌فرستیم\r\n props.onMarkerClick?.(store, index, mapInstance);\r\n };\r\n\r\n // اطمینان از اینکه markers همیشه یک آرایه است\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 برای جلوگیری از خطای undefined */}\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 {/* ✅ دریافت نمونه نقشه وقتی لود شد */}\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\">\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 } from 'react';\r\nimport type { MapboxMap, MarkerData, MapProps } from '../types';\r\nimport { createCustomMarkerElement } from '../utils/createCustomMarkerElement';\r\n\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 className = '',\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 \r\n useEffect(() => {\r\n let isMounted = true;\r\n \r\n import('@neshan-maps-platform/mapbox-gl').then((mod) => {\r\n if (isMounted) {\r\n setMapLib(mod.default || mod);\r\n }\r\n });\r\n\r\n return () => {\r\n isMounted = false;\r\n };\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 setMapInstance(map);\r\n onMapLoad?.(map);\r\n });\r\n\r\n \r\n return () => {\r\n if (map) {\r\n map.remove();\r\n setMapInstance(null);\r\n }\r\n };\r\n \r\n \r\n }, [mapLib]); \r\n\r\n \r\n useEffect(() => {\r\n if (!mapInstance || !mapLib) return;\r\n\r\n \r\n markersRef.current.forEach((m) => m.remove());\r\n markersRef.current = [];\r\n\r\n markers.forEach((markerData: MarkerData, index: number) => {\r\n const isSelected = markerData.id === selectedMarkerId;\r\n \r\n \r\n const el = createCustomMarkerElement({\r\n marker: markerData,\r\n isSelected: isSelected,\r\n logoSrc: markerLogoUrl, \r\n });\r\n\r\n \r\n const marker = new mapLib.Marker({ \r\n element: el,\r\n anchor: 'bottom', \r\n offset: [0, 0] \r\n })\r\n .setLngLat([markerData.lng, markerData.lat])\r\n .addTo(mapInstance);\r\n\r\n \r\n el.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n onMarkerClick?.(markerData, index, mapInstance);\r\n });\r\n\r\n markersRef.current.push(marker);\r\n });\r\n\r\n \r\n return () => {\r\n markersRef.current.forEach((m) => m.remove());\r\n };\r\n }, [markers, mapInstance, mapLib, selectedMarkerId, markerLogoUrl, onMarkerClick]);\r\n\r\n return (\r\n <div \r\n style={{ \r\n position: 'relative', \r\n overflow: 'hidden',\r\n ...style \r\n }} \r\n className={`neshan-map-wrapper ${className}`}\r\n >\r\n <div \r\n ref={mapContainerRef} \r\n style={{ width: '100%', height: '100%' }} \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: '40px',\r\n height: '40px',\r\n borderRadius: '50%',\r\n backgroundColor: 'white',\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';\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}","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 >\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\">\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\">\r\n <span>واتساپ</span>\r\n </a>\r\n )}\r\n <a href={googleMapUrl} target=\"_blank\" className=\"btn btn-route\">\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":["useState","useEffect","jsx","jsx","jsxs","useState","useEffect"]}
1
+ {"version":3,"sources":["../src/components/InteractiveMap.tsx","../src/components/Map.tsx","../src/utils/createCustomMarkerElement.ts","../src/utils/createClusterElement.ts","../src/components/StoreCard.tsx"],"sourcesContent":["\"use client\";\r\n\r\nimport React, { useState, useEffect } 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 // ✅ ذخیره نمونه نقشه برای پاس دادن به کلیک مارکر\r\n const [mapInstance, setMapInstance] = useState<any>(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 const handleStoreSelect = (store: Store, index: number) => {\r\n // ✅ حالا دیگر null نمی‌فرستیم، نمونه واقعی نقشه را می‌فرستیم\r\n props.onMarkerClick?.(store, index, mapInstance);\r\n };\r\n\r\n // اطمینان از اینکه markers همیشه یک آرایه است\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 برای جلوگیری از خطای undefined */}\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 {/* ✅ دریافت نمونه نقشه وقتی لود شد */}\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\">\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 \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 import('@neshan-maps-platform/mapbox-gl').then((mod) => {\r\n setMapLib(mod.default || mod);\r\n });\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]);\r\n\r\n // ۳. منطق زوم خودکار (FlyTo) وقتی مارکری انتخاب می‌شود\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 // ۴. تابع رندر کردن مارکرها و کلاسترها\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 // دریافت داده‌های کلاستر شده برای محدوده فعلی نقشه\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 // پاک کردن مارکرهای قبلی\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 // اگر کلاستر بود\r\n el = createClusterElement(count);\r\n el.onclick = () => {\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 // اگر مارکر تکی بود\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 }, [mapInstance, mapLib, selectedMarkerId, markerLogoUrl, onMarkerClick]);\r\n\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 // ۶. گوش دادن به تغییرات نقشه برای بروزرسانی کلاسترها\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 ref={mapContainerRef} style={{ width: '100%', height: '100%' }} />\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: '40px',\r\n height: '40px',\r\n borderRadius: '50%',\r\n backgroundColor: 'white',\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 el = document.createElement('div');\r\n el.className = 'neshan-cluster-marker';\r\n el.innerHTML = `<span class=\"neshan-cluster-count\">${count}</span>`;\r\n return el;\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 >\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\">\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\">\r\n <span>واتساپ</span>\r\n </a>\r\n )}\r\n <a href={googleMapUrl} target=\"_blank\" className=\"btn btn-route\">\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":["useState","useEffect","jsx","jsx","jsxs","useState","useEffect"]}
package/dist/react.cjs CHANGED
@@ -93,7 +93,16 @@ function createCustomMarkerElement({
93
93
  return container;
94
94
  }
95
95
 
96
+ // src/utils/createClusterElement.ts
97
+ function createClusterElement(count) {
98
+ const el = document.createElement("div");
99
+ el.className = "neshan-cluster-marker";
100
+ el.innerHTML = `<span class="neshan-cluster-count">${count}</span>`;
101
+ return el;
102
+ }
103
+
96
104
  // src/components/Map.tsx
105
+ var import_supercluster = __toESM(require("supercluster"), 1);
97
106
  var import_NeshanMapboxGl = require("@neshan-maps-platform/mapbox-gl/dist/NeshanMapboxGl.css");
98
107
  var import_jsx_runtime = require("react/jsx-runtime");
99
108
  var Map = ({
@@ -103,23 +112,20 @@ var Map = ({
103
112
  selectedMarkerId = null,
104
113
  onMarkerClick,
105
114
  onMapLoad,
106
- className = "",
107
115
  style = { width: "100%", height: "100%" }
108
116
  }) => {
109
117
  const mapContainerRef = (0, import_react.useRef)(null);
110
118
  const [mapInstance, setMapInstance] = (0, import_react.useState)(null);
111
119
  const [mapLib, setMapLib] = (0, import_react.useState)(null);
112
120
  const markersRef = (0, import_react.useRef)([]);
121
+ const clusterIndex = (0, import_react.useRef)(new import_supercluster.default({
122
+ radius: 60,
123
+ maxZoom: 16
124
+ }));
113
125
  (0, import_react.useEffect)(() => {
114
- let isMounted = true;
115
126
  import("@neshan-maps-platform/mapbox-gl").then((mod) => {
116
- if (isMounted) {
117
- setMapLib(mod.default || mod);
118
- }
127
+ setMapLib(mod.default || mod);
119
128
  });
120
- return () => {
121
- isMounted = false;
122
- };
123
129
  }, []);
124
130
  (0, import_react.useEffect)(() => {
125
131
  if (!mapLib || !mapContainerRef.current || mapInstance) return;
@@ -132,56 +138,75 @@ var Map = ({
132
138
  onMapLoad?.(map);
133
139
  });
134
140
  return () => {
135
- if (map) {
136
- map.remove();
137
- setMapInstance(null);
138
- }
141
+ if (map) map.remove();
139
142
  };
140
143
  }, [mapLib]);
141
144
  (0, import_react.useEffect)(() => {
145
+ if (!mapInstance || !selectedMarkerId) return;
146
+ const selectedStore = markers.find((m) => m.id === selectedMarkerId);
147
+ if (selectedStore) {
148
+ mapInstance.flyTo({
149
+ center: [selectedStore.lng, selectedStore.lat],
150
+ zoom: 16,
151
+ // زوم نزدیک هنگام انتخاب
152
+ essential: true,
153
+ duration: 1500
154
+ });
155
+ }
156
+ }, [selectedMarkerId, mapInstance, markers]);
157
+ const updateMarkers = (0, import_react.useCallback)(() => {
142
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
+ );
143
165
  markersRef.current.forEach((m) => m.remove());
144
166
  markersRef.current = [];
145
- markers.forEach((markerData, index) => {
146
- const isSelected = markerData.id === selectedMarkerId;
147
- const el = createCustomMarkerElement({
148
- marker: markerData,
149
- isSelected,
150
- logoSrc: markerLogoUrl
151
- });
152
- const marker = new mapLib.Marker({
153
- element: el,
154
- anchor: "bottom",
155
- offset: [0, 0]
156
- }).setLngLat([markerData.lng, markerData.lat]).addTo(mapInstance);
157
- el.addEventListener("click", (e) => {
158
- e.stopPropagation();
159
- onMarkerClick?.(markerData, index, mapInstance);
160
- });
161
- markersRef.current.push(marker);
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 = () => {
174
+ const expansionZoom = Math.min(
175
+ clusterIndex.current.getClusterExpansionZoom(cluster.id),
176
+ 18
177
+ );
178
+ mapInstance.easeTo({ center: [lng, lat], zoom: expansionZoom });
179
+ };
180
+ } else {
181
+ const storeData = cluster.properties;
182
+ el = createCustomMarkerElement({
183
+ marker: storeData,
184
+ isSelected: storeData.id === selectedMarkerId,
185
+ logoSrc: markerLogoUrl
186
+ });
187
+ el.onclick = () => onMarkerClick?.(storeData, 0, mapInstance);
188
+ }
189
+ const newMarker = new mapLib.Marker({ element: el, anchor: isCluster ? "center" : "bottom" }).setLngLat([lng, lat]).addTo(mapInstance);
190
+ markersRef.current.push(newMarker);
162
191
  });
192
+ }, [mapInstance, mapLib, selectedMarkerId, markerLogoUrl, onMarkerClick]);
193
+ (0, import_react.useEffect)(() => {
194
+ const points = markers.map((s) => ({
195
+ type: "Feature",
196
+ properties: { ...s },
197
+ geometry: { type: "Point", coordinates: [s.lng, s.lat] }
198
+ }));
199
+ clusterIndex.current.load(points);
200
+ updateMarkers();
201
+ }, [markers, updateMarkers]);
202
+ (0, import_react.useEffect)(() => {
203
+ if (!mapInstance) return;
204
+ mapInstance.on("moveend", updateMarkers);
163
205
  return () => {
164
- markersRef.current.forEach((m) => m.remove());
206
+ mapInstance.off("moveend", updateMarkers);
165
207
  };
166
- }, [markers, mapInstance, mapLib, selectedMarkerId, markerLogoUrl, onMarkerClick]);
167
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
168
- "div",
169
- {
170
- style: {
171
- position: "relative",
172
- overflow: "hidden",
173
- ...style
174
- },
175
- className: `neshan-map-wrapper ${className}`,
176
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
177
- "div",
178
- {
179
- ref: mapContainerRef,
180
- style: { width: "100%", height: "100%" }
181
- }
182
- )
183
- }
184
- );
208
+ }, [mapInstance, updateMarkers]);
209
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "relative", ...style }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: mapContainerRef, style: { width: "100%", height: "100%" } }) });
185
210
  };
186
211
  var Map_default = Map;
187
212
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react.ts","../src/components/InteractiveMap.tsx","../src/components/Map.tsx","../src/utils/createCustomMarkerElement.ts","../src/components/StoreCard.tsx"],"sourcesContent":["\"use client\";\r\n\r\n\r\nexport { InteractiveMap as NeshanMap } from './components/InteractiveMap';\r\n\r\n\r\nexport type {\r\n MapProps,\r\n MapOptions,\r\n MarkerData,\r\n Store,\r\n} from './types';","\"use client\";\r\n\r\nimport React, { useState, useEffect } 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 // ✅ ذخیره نمونه نقشه برای پاس دادن به کلیک مارکر\r\n const [mapInstance, setMapInstance] = useState<any>(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 const handleStoreSelect = (store: Store, index: number) => {\r\n // ✅ حالا دیگر null نمی‌فرستیم، نمونه واقعی نقشه را می‌فرستیم\r\n props.onMarkerClick?.(store, index, mapInstance);\r\n };\r\n\r\n // اطمینان از اینکه markers همیشه یک آرایه است\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 برای جلوگیری از خطای undefined */}\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 {/* ✅ دریافت نمونه نقشه وقتی لود شد */}\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\">\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 } from 'react';\r\nimport type { MapboxMap, MarkerData, MapProps } from '../types';\r\nimport { createCustomMarkerElement } from '../utils/createCustomMarkerElement';\r\n\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 className = '',\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 \r\n useEffect(() => {\r\n let isMounted = true;\r\n \r\n import('@neshan-maps-platform/mapbox-gl').then((mod) => {\r\n if (isMounted) {\r\n setMapLib(mod.default || mod);\r\n }\r\n });\r\n\r\n return () => {\r\n isMounted = false;\r\n };\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 setMapInstance(map);\r\n onMapLoad?.(map);\r\n });\r\n\r\n \r\n return () => {\r\n if (map) {\r\n map.remove();\r\n setMapInstance(null);\r\n }\r\n };\r\n \r\n \r\n }, [mapLib]); \r\n\r\n \r\n useEffect(() => {\r\n if (!mapInstance || !mapLib) return;\r\n\r\n \r\n markersRef.current.forEach((m) => m.remove());\r\n markersRef.current = [];\r\n\r\n markers.forEach((markerData: MarkerData, index: number) => {\r\n const isSelected = markerData.id === selectedMarkerId;\r\n \r\n \r\n const el = createCustomMarkerElement({\r\n marker: markerData,\r\n isSelected: isSelected,\r\n logoSrc: markerLogoUrl, \r\n });\r\n\r\n \r\n const marker = new mapLib.Marker({ \r\n element: el,\r\n anchor: 'bottom', \r\n offset: [0, 0] \r\n })\r\n .setLngLat([markerData.lng, markerData.lat])\r\n .addTo(mapInstance);\r\n\r\n \r\n el.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n onMarkerClick?.(markerData, index, mapInstance);\r\n });\r\n\r\n markersRef.current.push(marker);\r\n });\r\n\r\n \r\n return () => {\r\n markersRef.current.forEach((m) => m.remove());\r\n };\r\n }, [markers, mapInstance, mapLib, selectedMarkerId, markerLogoUrl, onMarkerClick]);\r\n\r\n return (\r\n <div \r\n style={{ \r\n position: 'relative', \r\n overflow: 'hidden',\r\n ...style \r\n }} \r\n className={`neshan-map-wrapper ${className}`}\r\n >\r\n <div \r\n ref={mapContainerRef} \r\n style={{ width: '100%', height: '100%' }} \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: '40px',\r\n height: '40px',\r\n borderRadius: '50%',\r\n backgroundColor: 'white',\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';\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}","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 >\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\">\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\">\r\n <span>واتساپ</span>\r\n </a>\r\n )}\r\n <a href={googleMapUrl} target=\"_blank\" className=\"btn btn-route\">\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","import_jsx_runtime","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/react.ts","../src/components/InteractiveMap.tsx","../src/components/Map.tsx","../src/utils/createCustomMarkerElement.ts","../src/utils/createClusterElement.ts","../src/components/StoreCard.tsx"],"sourcesContent":["\"use client\";\r\n\r\n\r\nexport { InteractiveMap as NeshanMap } from './components/InteractiveMap';\r\n\r\n\r\nexport type {\r\n MapProps,\r\n MapOptions,\r\n MarkerData,\r\n Store,\r\n} from './types';","\"use client\";\r\n\r\nimport React, { useState, useEffect } 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 // ✅ ذخیره نمونه نقشه برای پاس دادن به کلیک مارکر\r\n const [mapInstance, setMapInstance] = useState<any>(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 const handleStoreSelect = (store: Store, index: number) => {\r\n // ✅ حالا دیگر null نمی‌فرستیم، نمونه واقعی نقشه را می‌فرستیم\r\n props.onMarkerClick?.(store, index, mapInstance);\r\n };\r\n\r\n // اطمینان از اینکه markers همیشه یک آرایه است\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 برای جلوگیری از خطای undefined */}\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 {/* ✅ دریافت نمونه نقشه وقتی لود شد */}\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\">\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 \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 import('@neshan-maps-platform/mapbox-gl').then((mod) => {\r\n setMapLib(mod.default || mod);\r\n });\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]);\r\n\r\n // ۳. منطق زوم خودکار (FlyTo) وقتی مارکری انتخاب می‌شود\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 // ۴. تابع رندر کردن مارکرها و کلاسترها\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 // دریافت داده‌های کلاستر شده برای محدوده فعلی نقشه\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 // پاک کردن مارکرهای قبلی\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 // اگر کلاستر بود\r\n el = createClusterElement(count);\r\n el.onclick = () => {\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 // اگر مارکر تکی بود\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 }, [mapInstance, mapLib, selectedMarkerId, markerLogoUrl, onMarkerClick]);\r\n\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 // ۶. گوش دادن به تغییرات نقشه برای بروزرسانی کلاسترها\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 ref={mapContainerRef} style={{ width: '100%', height: '100%' }} />\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: '40px',\r\n height: '40px',\r\n borderRadius: '50%',\r\n backgroundColor: 'white',\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 el = document.createElement('div');\r\n el.className = 'neshan-cluster-marker';\r\n el.innerHTML = `<span class=\"neshan-cluster-count\">${count}</span>`;\r\n return el;\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 >\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\">\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\">\r\n <span>واتساپ</span>\r\n </a>\r\n )}\r\n <a href={googleMapUrl} target=\"_blank\" className=\"btn btn-route\">\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"]}