@mapfirst.ai/react 0.0.14 → 0.0.16
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/README.md +231 -298
- package/dist/index.d.mts +96 -295
- package/dist/index.d.ts +96 -295
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/EXAMPLES.md +0 -740
package/EXAMPLES.md
DELETED
|
@@ -1,740 +0,0 @@
|
|
|
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
|
-
## Search Hooks Examples
|
|
390
|
-
|
|
391
|
-
### Using `usePropertiesSearch` for Location-Based Search
|
|
392
|
-
|
|
393
|
-
```tsx
|
|
394
|
-
import React, { useState } from "react";
|
|
395
|
-
import {
|
|
396
|
-
useMapFirstCore,
|
|
397
|
-
useMapLibreAttachment,
|
|
398
|
-
usePropertiesSearch,
|
|
399
|
-
} from "@mapfirst/react";
|
|
400
|
-
import maplibregl from "maplibre-gl";
|
|
401
|
-
|
|
402
|
-
function LocationSearchExample() {
|
|
403
|
-
const [city, setCity] = useState("Paris");
|
|
404
|
-
const [country, setCountry] = useState("France");
|
|
405
|
-
|
|
406
|
-
const { mapFirst, state } = useMapFirstCore({
|
|
407
|
-
initialLocationData: { city, country, currency: "USD" },
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
const { search, isLoading, error } = usePropertiesSearch(mapFirst);
|
|
411
|
-
|
|
412
|
-
const handleSearch = async () => {
|
|
413
|
-
try {
|
|
414
|
-
await search({
|
|
415
|
-
body: {
|
|
416
|
-
city,
|
|
417
|
-
country,
|
|
418
|
-
filters: {
|
|
419
|
-
checkIn: new Date("2024-06-01"),
|
|
420
|
-
checkOut: new Date("2024-06-07"),
|
|
421
|
-
numAdults: 2,
|
|
422
|
-
numRooms: 1,
|
|
423
|
-
currency: "USD",
|
|
424
|
-
},
|
|
425
|
-
},
|
|
426
|
-
});
|
|
427
|
-
} catch (err) {
|
|
428
|
-
console.error("Search failed:", err);
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
return (
|
|
433
|
-
<div>
|
|
434
|
-
<div>
|
|
435
|
-
<input
|
|
436
|
-
value={city}
|
|
437
|
-
onChange={(e) => setCity(e.target.value)}
|
|
438
|
-
placeholder="City"
|
|
439
|
-
/>
|
|
440
|
-
<input
|
|
441
|
-
value={country}
|
|
442
|
-
onChange={(e) => setCountry(e.target.value)}
|
|
443
|
-
placeholder="Country"
|
|
444
|
-
/>
|
|
445
|
-
<button onClick={handleSearch} disabled={isLoading}>
|
|
446
|
-
{isLoading ? "Searching..." : "Search"}
|
|
447
|
-
</button>
|
|
448
|
-
</div>
|
|
449
|
-
{error && <div style={{ color: "red" }}>Error: {error.message}</div>}
|
|
450
|
-
<div>Found {state?.properties.length || 0} properties</div>
|
|
451
|
-
</div>
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
### Using `useSmartFilterSearch` for Natural Language Queries
|
|
457
|
-
|
|
458
|
-
```tsx
|
|
459
|
-
import React, { useState } from "react";
|
|
460
|
-
import {
|
|
461
|
-
useMapFirstCore,
|
|
462
|
-
useMapLibreAttachment,
|
|
463
|
-
useSmartFilterSearch,
|
|
464
|
-
} from "@mapfirst/react";
|
|
465
|
-
import maplibregl from "maplibre-gl";
|
|
466
|
-
|
|
467
|
-
function SmartSearchExample() {
|
|
468
|
-
const [query, setQuery] = useState("");
|
|
469
|
-
|
|
470
|
-
const { mapFirst, state } = useMapFirstCore({
|
|
471
|
-
initialLocationData: {
|
|
472
|
-
city: "New York",
|
|
473
|
-
country: "United States",
|
|
474
|
-
currency: "USD",
|
|
475
|
-
},
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
const { search, isLoading, error } = useSmartFilterSearch(mapFirst);
|
|
479
|
-
|
|
480
|
-
const handleSearch = async (e: React.FormEvent) => {
|
|
481
|
-
e.preventDefault();
|
|
482
|
-
if (!query.trim()) return;
|
|
483
|
-
|
|
484
|
-
try {
|
|
485
|
-
await search({ query });
|
|
486
|
-
} catch (err) {
|
|
487
|
-
console.error("Search failed:", err);
|
|
488
|
-
}
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
const exampleQueries = [
|
|
492
|
-
"Hotels near Times Square with free wifi",
|
|
493
|
-
"4-star hotels with pool and gym",
|
|
494
|
-
"Budget hotels under $150",
|
|
495
|
-
"Luxury hotels with spa",
|
|
496
|
-
];
|
|
497
|
-
|
|
498
|
-
return (
|
|
499
|
-
<div>
|
|
500
|
-
<form onSubmit={handleSearch}>
|
|
501
|
-
<input
|
|
502
|
-
type="text"
|
|
503
|
-
value={query}
|
|
504
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
505
|
-
placeholder="Try: hotels near beach with pool"
|
|
506
|
-
style={{ width: "400px", padding: "8px" }}
|
|
507
|
-
/>
|
|
508
|
-
<button type="submit" disabled={isLoading}>
|
|
509
|
-
{isLoading ? "Searching..." : "Search"}
|
|
510
|
-
</button>
|
|
511
|
-
</form>
|
|
512
|
-
|
|
513
|
-
<div style={{ marginTop: "10px" }}>
|
|
514
|
-
<strong>Try these examples:</strong>
|
|
515
|
-
<ul>
|
|
516
|
-
{exampleQueries.map((q) => (
|
|
517
|
-
<li key={q}>
|
|
518
|
-
<button
|
|
519
|
-
onClick={() => {
|
|
520
|
-
setQuery(q);
|
|
521
|
-
search({ query: q });
|
|
522
|
-
}}
|
|
523
|
-
disabled={isLoading}
|
|
524
|
-
style={{ textAlign: "left", padding: "4px 8px" }}
|
|
525
|
-
>
|
|
526
|
-
{q}
|
|
527
|
-
</button>
|
|
528
|
-
</li>
|
|
529
|
-
))}
|
|
530
|
-
</ul>
|
|
531
|
-
</div>
|
|
532
|
-
|
|
533
|
-
{error && (
|
|
534
|
-
<div style={{ color: "red", marginTop: "10px" }}>
|
|
535
|
-
Error: {error.message}
|
|
536
|
-
</div>
|
|
537
|
-
)}
|
|
538
|
-
|
|
539
|
-
<div style={{ marginTop: "10px" }}>
|
|
540
|
-
{isLoading ? (
|
|
541
|
-
<p>Searching...</p>
|
|
542
|
-
) : (
|
|
543
|
-
<p>Found {state?.properties.length || 0} properties</p>
|
|
544
|
-
)}
|
|
545
|
-
</div>
|
|
546
|
-
</div>
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
### Combined Search with Filters
|
|
552
|
-
|
|
553
|
-
```tsx
|
|
554
|
-
import React, { useState } from "react";
|
|
555
|
-
import {
|
|
556
|
-
useMapFirstCore,
|
|
557
|
-
usePropertiesSearch,
|
|
558
|
-
useSmartFilterSearch,
|
|
559
|
-
} from "@mapfirst/react";
|
|
560
|
-
|
|
561
|
-
type SearchMode = "location" | "smart";
|
|
562
|
-
|
|
563
|
-
function CombinedSearchExample() {
|
|
564
|
-
const [mode, setMode] = useState<SearchMode>("location");
|
|
565
|
-
const [city, setCity] = useState("Los Angeles");
|
|
566
|
-
const [country, setCountry] = useState("United States");
|
|
567
|
-
const [query, setQuery] = useState("");
|
|
568
|
-
|
|
569
|
-
const { mapFirst, state } = useMapFirstCore({
|
|
570
|
-
initialLocationData: { city, country, currency: "USD" },
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
const propertiesSearch = usePropertiesSearch(mapFirst);
|
|
574
|
-
const smartSearch = useSmartFilterSearch(mapFirst);
|
|
575
|
-
|
|
576
|
-
const currentSearch = mode === "location" ? propertiesSearch : smartSearch;
|
|
577
|
-
|
|
578
|
-
const handleLocationSearch = async () => {
|
|
579
|
-
try {
|
|
580
|
-
await propertiesSearch.search({
|
|
581
|
-
body: {
|
|
582
|
-
city,
|
|
583
|
-
country,
|
|
584
|
-
filters: {
|
|
585
|
-
checkIn: new Date("2024-06-01"),
|
|
586
|
-
checkOut: new Date("2024-06-07"),
|
|
587
|
-
numAdults: 2,
|
|
588
|
-
numRooms: 1,
|
|
589
|
-
currency: "USD",
|
|
590
|
-
},
|
|
591
|
-
},
|
|
592
|
-
});
|
|
593
|
-
} catch (err) {
|
|
594
|
-
console.error("Location search failed:", err);
|
|
595
|
-
}
|
|
596
|
-
};
|
|
597
|
-
|
|
598
|
-
const handleSmartSearch = async () => {
|
|
599
|
-
if (!query.trim()) return;
|
|
600
|
-
try {
|
|
601
|
-
await smartSearch.search({ query });
|
|
602
|
-
} catch (err) {
|
|
603
|
-
console.error("Smart search failed:", err);
|
|
604
|
-
}
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
return (
|
|
608
|
-
<div>
|
|
609
|
-
<div>
|
|
610
|
-
<label>
|
|
611
|
-
<input
|
|
612
|
-
type="radio"
|
|
613
|
-
checked={mode === "location"}
|
|
614
|
-
onChange={() => setMode("location")}
|
|
615
|
-
/>
|
|
616
|
-
Location Search
|
|
617
|
-
</label>
|
|
618
|
-
<label style={{ marginLeft: "20px" }}>
|
|
619
|
-
<input
|
|
620
|
-
type="radio"
|
|
621
|
-
checked={mode === "smart"}
|
|
622
|
-
onChange={() => setMode("smart")}
|
|
623
|
-
/>
|
|
624
|
-
Smart Search
|
|
625
|
-
</label>
|
|
626
|
-
</div>
|
|
627
|
-
|
|
628
|
-
{mode === "location" ? (
|
|
629
|
-
<div style={{ marginTop: "10px" }}>
|
|
630
|
-
<input
|
|
631
|
-
value={city}
|
|
632
|
-
onChange={(e) => setCity(e.target.value)}
|
|
633
|
-
placeholder="City"
|
|
634
|
-
/>
|
|
635
|
-
<input
|
|
636
|
-
value={country}
|
|
637
|
-
onChange={(e) => setCountry(e.target.value)}
|
|
638
|
-
placeholder="Country"
|
|
639
|
-
style={{ marginLeft: "10px" }}
|
|
640
|
-
/>
|
|
641
|
-
<button
|
|
642
|
-
onClick={handleLocationSearch}
|
|
643
|
-
disabled={currentSearch.isLoading}
|
|
644
|
-
>
|
|
645
|
-
Search
|
|
646
|
-
</button>
|
|
647
|
-
</div>
|
|
648
|
-
) : (
|
|
649
|
-
<div style={{ marginTop: "10px" }}>
|
|
650
|
-
<input
|
|
651
|
-
value={query}
|
|
652
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
653
|
-
placeholder="Describe what you're looking for..."
|
|
654
|
-
style={{ width: "400px" }}
|
|
655
|
-
/>
|
|
656
|
-
<button
|
|
657
|
-
onClick={handleSmartSearch}
|
|
658
|
-
disabled={currentSearch.isLoading}
|
|
659
|
-
>
|
|
660
|
-
Search
|
|
661
|
-
</button>
|
|
662
|
-
</div>
|
|
663
|
-
)}
|
|
664
|
-
|
|
665
|
-
{currentSearch.error && (
|
|
666
|
-
<div style={{ color: "red", marginTop: "10px" }}>
|
|
667
|
-
Error: {currentSearch.error.message}
|
|
668
|
-
</div>
|
|
669
|
-
)}
|
|
670
|
-
|
|
671
|
-
<div style={{ marginTop: "10px" }}>
|
|
672
|
-
{currentSearch.isLoading ? (
|
|
673
|
-
<p>Searching...</p>
|
|
674
|
-
) : (
|
|
675
|
-
<p>Found {state?.properties.length || 0} properties</p>
|
|
676
|
-
)}
|
|
677
|
-
</div>
|
|
678
|
-
|
|
679
|
-
<div style={{ marginTop: "20px" }}>
|
|
680
|
-
<h3>Results:</h3>
|
|
681
|
-
<ul>
|
|
682
|
-
{state?.properties.slice(0, 5).map((property) => (
|
|
683
|
-
<li key={property.tripadvisor_id}>
|
|
684
|
-
<strong>{property.name}</strong>
|
|
685
|
-
{property.pricing && (
|
|
686
|
-
<span> - ${property.pricing.display_price}</span>
|
|
687
|
-
)}
|
|
688
|
-
</li>
|
|
689
|
-
))}
|
|
690
|
-
</ul>
|
|
691
|
-
</div>
|
|
692
|
-
</div>
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
|
-
```
|
|
696
|
-
|
|
697
|
-
## TypeScript Usage
|
|
698
|
-
|
|
699
|
-
Full type safety with TypeScript:
|
|
700
|
-
|
|
701
|
-
```tsx
|
|
702
|
-
import React from "react";
|
|
703
|
-
import { useMapFirstCore } from "@mapfirst/react";
|
|
704
|
-
import type { Property, MapState, FilterState } from "@mapfirst/core";
|
|
705
|
-
|
|
706
|
-
function TypeSafeComponent() {
|
|
707
|
-
const { mapFirst, state } = useMapFirstCore({
|
|
708
|
-
initialLocationData: {
|
|
709
|
-
city: "Paris",
|
|
710
|
-
country: "France",
|
|
711
|
-
currency: "EUR",
|
|
712
|
-
},
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
// All properly typed
|
|
716
|
-
const properties: Property[] = state?.properties || [];
|
|
717
|
-
const mapState: MapState | null = state;
|
|
718
|
-
const filters: FilterState | undefined = state?.filters;
|
|
719
|
-
|
|
720
|
-
// Type-safe property access
|
|
721
|
-
const handlePropertyClick = (property: Property) => {
|
|
722
|
-
if (mapFirst && property.location) {
|
|
723
|
-
mapFirst.flyMapTo(property.location.lon, property.location.lat, 14);
|
|
724
|
-
}
|
|
725
|
-
};
|
|
726
|
-
|
|
727
|
-
return (
|
|
728
|
-
<div>
|
|
729
|
-
{properties.map((property) => (
|
|
730
|
-
<button
|
|
731
|
-
key={property.tripadvisor_id}
|
|
732
|
-
onClick={() => handlePropertyClick(property)}
|
|
733
|
-
>
|
|
734
|
-
{property.name}
|
|
735
|
-
</button>
|
|
736
|
-
))}
|
|
737
|
-
</div>
|
|
738
|
-
);
|
|
739
|
-
}
|
|
740
|
-
```
|