@mapfirst.ai/react 0.0.4
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/EXAMPLES.md +432 -0
- package/README.md +519 -0
- package/dist/index.d.mts +197 -0
- package/dist/index.d.ts +197 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
package/EXAMPLES.md
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# React SDK Examples
|
|
2
|
+
|
|
3
|
+
## Complete Example with Reactive State
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
7
|
+
import maplibregl from "maplibre-gl";
|
|
8
|
+
import {
|
|
9
|
+
useMapFirstCore,
|
|
10
|
+
useMapLibreAttachment,
|
|
11
|
+
usePrimaryType,
|
|
12
|
+
useSelectedMarker,
|
|
13
|
+
} from "@mapfirst/react";
|
|
14
|
+
import "maplibre-gl/dist/maplibre-gl.css";
|
|
15
|
+
|
|
16
|
+
function HotelSearchApp() {
|
|
17
|
+
const mapContainerRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
const [map, setMap] = useState<maplibregl.Map | null>(null);
|
|
19
|
+
|
|
20
|
+
// Create SDK instance with reactive state
|
|
21
|
+
const { mapFirst, state } = useMapFirstCore({
|
|
22
|
+
initialLocationData: {
|
|
23
|
+
city: "Paris",
|
|
24
|
+
country: "France",
|
|
25
|
+
currency: "EUR",
|
|
26
|
+
},
|
|
27
|
+
callbacks: {
|
|
28
|
+
onError: (error, context) => {
|
|
29
|
+
console.error(`Error in ${context}:`, error);
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Use dedicated hooks for controlling state
|
|
35
|
+
const [primaryType, setPrimaryType] = usePrimaryType(mapFirst);
|
|
36
|
+
const [selectedMarker, setSelectedMarker] = useSelectedMarker(mapFirst);
|
|
37
|
+
|
|
38
|
+
// Access reactive state
|
|
39
|
+
const properties = state?.properties || [];
|
|
40
|
+
const isSearching = state?.isSearching || false;
|
|
41
|
+
const filters = state?.filters;
|
|
42
|
+
|
|
43
|
+
// Initialize map
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!mapContainerRef.current) return;
|
|
46
|
+
|
|
47
|
+
const mapInstance = new maplibregl.Map({
|
|
48
|
+
container: mapContainerRef.current,
|
|
49
|
+
style: "https://demotiles.maplibre.org/style.json",
|
|
50
|
+
center: [2.3522, 48.8566],
|
|
51
|
+
zoom: 12,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
mapInstance.on("load", () => {
|
|
55
|
+
setMap(mapInstance);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
mapInstance.remove();
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
// Attach map to SDK
|
|
64
|
+
useMapLibreAttachment({
|
|
65
|
+
mapFirst,
|
|
66
|
+
map,
|
|
67
|
+
maplibregl,
|
|
68
|
+
onMarkerClick: (marker) => {
|
|
69
|
+
console.log("Clicked:", marker.name);
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Handle search
|
|
74
|
+
const handleSearch = async () => {
|
|
75
|
+
if (!mapFirst) return;
|
|
76
|
+
|
|
77
|
+
await mapFirst.runPropertiesSearch({
|
|
78
|
+
body: {
|
|
79
|
+
city: "Paris",
|
|
80
|
+
country: "France",
|
|
81
|
+
filters: {
|
|
82
|
+
checkIn: "2024-06-01",
|
|
83
|
+
checkOut: "2024-06-07",
|
|
84
|
+
numAdults: 2,
|
|
85
|
+
currency: "EUR",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Handle date change
|
|
92
|
+
const handleDateChange = (checkIn: Date, checkOut: Date) => {
|
|
93
|
+
if (!mapFirst) return;
|
|
94
|
+
|
|
95
|
+
mapFirst.setFilters({
|
|
96
|
+
...filters,
|
|
97
|
+
checkIn,
|
|
98
|
+
checkOut,
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
|
|
104
|
+
{/* Search Controls */}
|
|
105
|
+
<div style={{ padding: "20px", background: "#f5f5f5" }}>
|
|
106
|
+
<h1>Hotel Search</h1>
|
|
107
|
+
<button onClick={handleSearch} disabled={isSearching}>
|
|
108
|
+
{isSearching ? "Searching..." : "Search"}
|
|
109
|
+
</button>
|
|
110
|
+
<p>
|
|
111
|
+
{isSearching
|
|
112
|
+
? "Searching for properties..."
|
|
113
|
+
: `Found ${properties.length} properties`}
|
|
114
|
+
</p>
|
|
115
|
+
{selectedId && <p>Selected Property ID: {selectedId}</p>}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Map */}
|
|
119
|
+
<div style={{ flex: 1, position: "relative" }}>
|
|
120
|
+
<div ref={mapContainerRef} style={{ width: "100%", height: "100%" }} />
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Property List */}
|
|
124
|
+
<div
|
|
125
|
+
style={{
|
|
126
|
+
maxHeight: "200px",
|
|
127
|
+
overflow: "auto",
|
|
128
|
+
background: "#fff",
|
|
129
|
+
borderTop: "1px solid #ddd",
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{properties.map((property) => (
|
|
133
|
+
<div
|
|
134
|
+
key={property.tripadvisor_id}
|
|
135
|
+
style={{
|
|
136
|
+
padding: "10px",
|
|
137
|
+
borderBottom: "1px solid #eee",
|
|
138
|
+
background:
|
|
139
|
+
selectedId === property.tripadvisor_id
|
|
140
|
+
? "#e3f2fd"
|
|
141
|
+
: "transparent",
|
|
142
|
+
cursor: "pointer",
|
|
143
|
+
}}
|
|
144
|
+
onClick={() => {
|
|
145
|
+
// Use the hook setter instead of calling the SDK method directly
|
|
146
|
+
setSelectedMarker(property.tripadvisor_id);
|
|
147
|
+
if (mapFirst && property.location) {
|
|
148
|
+
mapFirst.flyMapTo(
|
|
149
|
+
property.location.lon,
|
|
150
|
+
property.location.lat,
|
|
151
|
+
14
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
<strong>{property.name}</strong>
|
|
157
|
+
<br />
|
|
158
|
+
{property.pricing && (
|
|
159
|
+
<span>
|
|
160
|
+
{property.pricing.isPending
|
|
161
|
+
? "Loading price..."
|
|
162
|
+
: `$${property.pricing.lead_rate?.display_price}`}
|
|
163
|
+
</span>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export default HotelSearchApp;
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Using Dedicated Hooks for Performance
|
|
176
|
+
|
|
177
|
+
When you only need specific state values, use the dedicated hooks to avoid unnecessary re-renders:
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
import React from "react";
|
|
181
|
+
import {
|
|
182
|
+
useMapFirstCore,
|
|
183
|
+
useMapFirstProperties,
|
|
184
|
+
useMapFirstSelectedProperty,
|
|
185
|
+
} from "@mapfirst/react";
|
|
186
|
+
|
|
187
|
+
function PropertyStats() {
|
|
188
|
+
const { mapFirst } = useMapFirstCore({
|
|
189
|
+
initialLocationData: {
|
|
190
|
+
city: "New York",
|
|
191
|
+
country: "United States",
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Only re-renders when properties change
|
|
196
|
+
const properties = useMapFirstProperties(mapFirst);
|
|
197
|
+
|
|
198
|
+
// Only re-renders when selection changes
|
|
199
|
+
const selectedId = useMapFirstSelectedProperty(mapFirst);
|
|
200
|
+
|
|
201
|
+
const selectedProperty = properties.find(
|
|
202
|
+
(p) => p.tripadvisor_id === selectedId
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div>
|
|
207
|
+
<h2>Statistics</h2>
|
|
208
|
+
<p>Total Properties: {properties.length}</p>
|
|
209
|
+
<p>
|
|
210
|
+
Accommodations:{" "}
|
|
211
|
+
{properties.filter((p) => p.type === "Accommodation").length}
|
|
212
|
+
</p>
|
|
213
|
+
<p>
|
|
214
|
+
Restaurants: {properties.filter((p) => p.type === "Restaurant").length}
|
|
215
|
+
</p>
|
|
216
|
+
<p>
|
|
217
|
+
Attractions: {properties.filter((p) => p.type === "Attraction").length}
|
|
218
|
+
</p>
|
|
219
|
+
|
|
220
|
+
{selectedProperty && (
|
|
221
|
+
<div>
|
|
222
|
+
<h3>Selected</h3>
|
|
223
|
+
<p>{selectedProperty.name}</p>
|
|
224
|
+
<p>{selectedProperty.type}</p>
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Combining Multiple State Sources
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
import React, { useState } from "react";
|
|
236
|
+
import { useMapFirstCore } from "@mapfirst/react";
|
|
237
|
+
|
|
238
|
+
function AdvancedSearch() {
|
|
239
|
+
const { mapFirst, state } = useMapFirstCore({
|
|
240
|
+
initialLocationData: {
|
|
241
|
+
city: "London",
|
|
242
|
+
country: "United Kingdom",
|
|
243
|
+
currency: "GBP",
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Local UI state
|
|
248
|
+
const [showFilters, setShowFilters] = useState(false);
|
|
249
|
+
const [priceRange, setPriceRange] = useState({ min: 0, max: 1000 });
|
|
250
|
+
|
|
251
|
+
// SDK reactive state
|
|
252
|
+
const properties = state?.properties || [];
|
|
253
|
+
const isSearching = state?.isSearching || false;
|
|
254
|
+
const filters = state?.filters;
|
|
255
|
+
|
|
256
|
+
// Computed values
|
|
257
|
+
const filteredProperties = properties.filter((p) => {
|
|
258
|
+
if (!p.pricing?.lead_rate?.display_price) return true;
|
|
259
|
+
const price = p.pricing.lead_rate.display_price;
|
|
260
|
+
return price >= priceRange.min && price <= priceRange.max;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const handleApplyFilters = async () => {
|
|
264
|
+
if (!mapFirst) return;
|
|
265
|
+
|
|
266
|
+
await mapFirst.runPropertiesSearch({
|
|
267
|
+
body: {
|
|
268
|
+
city: "London",
|
|
269
|
+
country: "United Kingdom",
|
|
270
|
+
filters: {
|
|
271
|
+
...filters,
|
|
272
|
+
min_price: priceRange.min,
|
|
273
|
+
max_price: priceRange.max,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<div>
|
|
281
|
+
<button onClick={() => setShowFilters(!showFilters)}>
|
|
282
|
+
{showFilters ? "Hide" : "Show"} Filters
|
|
283
|
+
</button>
|
|
284
|
+
|
|
285
|
+
{showFilters && (
|
|
286
|
+
<div>
|
|
287
|
+
<label>
|
|
288
|
+
Min Price: ${priceRange.min}
|
|
289
|
+
<input
|
|
290
|
+
type="range"
|
|
291
|
+
min="0"
|
|
292
|
+
max="1000"
|
|
293
|
+
value={priceRange.min}
|
|
294
|
+
onChange={(e) =>
|
|
295
|
+
setPriceRange({
|
|
296
|
+
...priceRange,
|
|
297
|
+
min: parseInt(e.target.value),
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
/>
|
|
301
|
+
</label>
|
|
302
|
+
|
|
303
|
+
<label>
|
|
304
|
+
Max Price: ${priceRange.max}
|
|
305
|
+
<input
|
|
306
|
+
type="range"
|
|
307
|
+
min="0"
|
|
308
|
+
max="1000"
|
|
309
|
+
value={priceRange.max}
|
|
310
|
+
onChange={(e) =>
|
|
311
|
+
setPriceRange({
|
|
312
|
+
...priceRange,
|
|
313
|
+
max: parseInt(e.target.value),
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
/>
|
|
317
|
+
</label>
|
|
318
|
+
|
|
319
|
+
<button onClick={handleApplyFilters} disabled={isSearching}>
|
|
320
|
+
Apply Filters
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
<p>
|
|
326
|
+
Showing {filteredProperties.length} of {properties.length} properties
|
|
327
|
+
{isSearching && " (searching...)"}
|
|
328
|
+
</p>
|
|
329
|
+
|
|
330
|
+
{filteredProperties.map((property) => (
|
|
331
|
+
<div key={property.tripadvisor_id}>{property.name}</div>
|
|
332
|
+
))}
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Real-time Updates
|
|
339
|
+
|
|
340
|
+
The reactive state automatically updates when:
|
|
341
|
+
|
|
342
|
+
- Properties are loaded or updated
|
|
343
|
+
- A property is selected/deselected
|
|
344
|
+
- Filters are changed
|
|
345
|
+
- Search state changes (loading, searching, etc.)
|
|
346
|
+
- Map bounds change
|
|
347
|
+
- Primary type changes
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
function RealTimeUpdates() {
|
|
351
|
+
const { mapFirst, state } = useMapFirstCore({
|
|
352
|
+
initialLocationData: {
|
|
353
|
+
city: "Tokyo",
|
|
354
|
+
country: "Japan",
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// These values update automatically
|
|
359
|
+
const properties = state?.properties || [];
|
|
360
|
+
const isSearching = state?.isSearching || false;
|
|
361
|
+
const selectedId = state?.selectedPropertyId;
|
|
362
|
+
const bounds = state?.bounds;
|
|
363
|
+
const center = state?.center;
|
|
364
|
+
const zoom = state?.zoom;
|
|
365
|
+
|
|
366
|
+
// Display real-time updates
|
|
367
|
+
return (
|
|
368
|
+
<div>
|
|
369
|
+
<h2>Real-time State</h2>
|
|
370
|
+
<pre>
|
|
371
|
+
{JSON.stringify(
|
|
372
|
+
{
|
|
373
|
+
propertyCount: properties.length,
|
|
374
|
+
isSearching,
|
|
375
|
+
selectedId,
|
|
376
|
+
bounds,
|
|
377
|
+
center,
|
|
378
|
+
zoom,
|
|
379
|
+
},
|
|
380
|
+
null,
|
|
381
|
+
2
|
|
382
|
+
)}
|
|
383
|
+
</pre>
|
|
384
|
+
</div>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## TypeScript Usage
|
|
390
|
+
|
|
391
|
+
Full type safety with TypeScript:
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
import React from "react";
|
|
395
|
+
import { useMapFirstCore } from "@mapfirst/react";
|
|
396
|
+
import type { Property, MapState, FilterState } from "@mapfirst/core";
|
|
397
|
+
|
|
398
|
+
function TypeSafeComponent() {
|
|
399
|
+
const { mapFirst, state } = useMapFirstCore({
|
|
400
|
+
initialLocationData: {
|
|
401
|
+
city: "Paris",
|
|
402
|
+
country: "France",
|
|
403
|
+
currency: "EUR",
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// All properly typed
|
|
408
|
+
const properties: Property[] = state?.properties || [];
|
|
409
|
+
const mapState: MapState | null = state;
|
|
410
|
+
const filters: FilterState | undefined = state?.filters;
|
|
411
|
+
|
|
412
|
+
// Type-safe property access
|
|
413
|
+
const handlePropertyClick = (property: Property) => {
|
|
414
|
+
if (mapFirst && property.location) {
|
|
415
|
+
mapFirst.flyMapTo(property.location.lon, property.location.lat, 14);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
<div>
|
|
421
|
+
{properties.map((property) => (
|
|
422
|
+
<button
|
|
423
|
+
key={property.tripadvisor_id}
|
|
424
|
+
onClick={() => handlePropertyClick(property)}
|
|
425
|
+
>
|
|
426
|
+
{property.name}
|
|
427
|
+
</button>
|
|
428
|
+
))}
|
|
429
|
+
</div>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
```
|