@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 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
+ ```