@mapfirst.ai/react 0.0.6 → 0.0.8
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 +308 -0
- package/README.md +157 -1
- package/dist/index.d.mts +293 -4
- package/dist/index.d.ts +293 -4
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/EXAMPLES.md
CHANGED
|
@@ -386,6 +386,314 @@ function RealTimeUpdates() {
|
|
|
386
386
|
}
|
|
387
387
|
```
|
|
388
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
|
+
|
|
389
697
|
## TypeScript Usage
|
|
390
698
|
|
|
391
699
|
Full type safety with TypeScript:
|
package/README.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# @mapfirst/react
|
|
2
2
|
|
|
3
|
-
React hooks for the MapFirst SDK supporting MapLibre, Google Maps, and Mapbox.
|
|
3
|
+
React hooks and components for the MapFirst SDK supporting MapLibre, Google Maps, and Mapbox.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🗺️ **Multi-Platform Support**: Works with MapLibre GL JS, Google Maps, and Mapbox GL JS
|
|
8
|
+
- 🔍 **SmartFilter Component**: AI-powered search with interactive filter chips
|
|
9
|
+
- ⚛️ **React Hooks**: Reactive state management for properties, filters, and map state
|
|
10
|
+
- 🎨 **Customizable**: Native React styles (CSS-in-JS) - no framework dependencies
|
|
11
|
+
- 📱 **Responsive**: Adapts to different screen sizes and orientations
|
|
12
|
+
- ♿ **Accessible**: Full keyboard navigation and ARIA support
|
|
13
|
+
- 🌍 **i18n Ready**: Built-in translations with extensibility
|
|
4
14
|
|
|
5
15
|
## Installation
|
|
6
16
|
|
|
@@ -12,6 +22,39 @@ pnpm add @mapfirst/react @mapfirst/core
|
|
|
12
22
|
yarn add @mapfirst/react @mapfirst/core
|
|
13
23
|
```
|
|
14
24
|
|
|
25
|
+
## Quick Start - SmartFilter Component
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { useMapFirstCore, SmartFilter } from "@mapfirst/react";
|
|
29
|
+
import { useState } from "react";
|
|
30
|
+
|
|
31
|
+
function App() {
|
|
32
|
+
const { mapFirst, state } = useMapFirstCore({
|
|
33
|
+
initialLocationData: {
|
|
34
|
+
city: "New York",
|
|
35
|
+
country: "United States",
|
|
36
|
+
currency: "USD",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const [filters, setFilters] = useState([]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<SmartFilter
|
|
44
|
+
mapFirst={mapFirst}
|
|
45
|
+
filters={filters}
|
|
46
|
+
isSearching={state?.isSearching}
|
|
47
|
+
onSearch={async (query) => {
|
|
48
|
+
// Implement search logic
|
|
49
|
+
}}
|
|
50
|
+
onFilterChange={setFilters}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
See [SMARTFILTER.md](./SMARTFILTER.md) for complete SmartFilter documentation.
|
|
57
|
+
|
|
15
58
|
## Usage
|
|
16
59
|
|
|
17
60
|
The React SDK supports a two-phase initialization pattern:
|
|
@@ -499,6 +542,119 @@ const selectedId = useMapFirstSelectedProperty(mapFirst);
|
|
|
499
542
|
return <div>Selected: {selectedId || 'None'}</div>;
|
|
500
543
|
```
|
|
501
544
|
|
|
545
|
+
### `usePropertiesSearch`
|
|
546
|
+
|
|
547
|
+
Hook to run properties search with the MapFirst SDK. Returns a function to trigger the search and loading state.
|
|
548
|
+
|
|
549
|
+
**Parameters:**
|
|
550
|
+
|
|
551
|
+
- `mapFirst` - SDK instance from `useMapFirstCore`
|
|
552
|
+
|
|
553
|
+
**Returns:** `{ search: Function, isLoading: boolean, error: Error | null }`
|
|
554
|
+
|
|
555
|
+
**Example:**
|
|
556
|
+
|
|
557
|
+
```tsx
|
|
558
|
+
const { mapFirst } = useMapFirstCore({ ... });
|
|
559
|
+
const { search, isLoading, error } = usePropertiesSearch(mapFirst);
|
|
560
|
+
|
|
561
|
+
const handleSearch = async () => {
|
|
562
|
+
try {
|
|
563
|
+
await search({
|
|
564
|
+
body: {
|
|
565
|
+
city: "Paris",
|
|
566
|
+
country: "France",
|
|
567
|
+
filters: {
|
|
568
|
+
checkIn: new Date(),
|
|
569
|
+
checkOut: new Date(Date.now() + 86400000 * 3), // 3 days later
|
|
570
|
+
numAdults: 2,
|
|
571
|
+
numRooms: 1,
|
|
572
|
+
currency: "EUR"
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
} catch (err) {
|
|
577
|
+
console.error("Search failed:", err);
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
return (
|
|
582
|
+
<div>
|
|
583
|
+
<button onClick={handleSearch} disabled={isLoading}>
|
|
584
|
+
{isLoading ? "Searching..." : "Search Properties"}
|
|
585
|
+
</button>
|
|
586
|
+
{error && <p>Error: {error.message}</p>}
|
|
587
|
+
</div>
|
|
588
|
+
);
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### `useSmartFilterSearch`
|
|
592
|
+
|
|
593
|
+
Hook to run smart filter search with natural language queries or predefined filters.
|
|
594
|
+
|
|
595
|
+
**Parameters:**
|
|
596
|
+
|
|
597
|
+
- `mapFirst` - SDK instance from `useMapFirstCore`
|
|
598
|
+
|
|
599
|
+
**Returns:** `{ search: Function, isLoading: boolean, error: Error | null }`
|
|
600
|
+
|
|
601
|
+
**Example with natural language query:**
|
|
602
|
+
|
|
603
|
+
```tsx
|
|
604
|
+
const { mapFirst } = useMapFirstCore({ ... });
|
|
605
|
+
const { search, isLoading, error } = useSmartFilterSearch(mapFirst);
|
|
606
|
+
|
|
607
|
+
const handleSearch = async (query: string) => {
|
|
608
|
+
try {
|
|
609
|
+
await search({ query });
|
|
610
|
+
} catch (err) {
|
|
611
|
+
console.error("Search failed:", err);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
return (
|
|
616
|
+
<div>
|
|
617
|
+
<input
|
|
618
|
+
type="text"
|
|
619
|
+
placeholder="e.g., hotels near beach with pool"
|
|
620
|
+
onKeyDown={(e) => {
|
|
621
|
+
if (e.key === 'Enter') {
|
|
622
|
+
handleSearch(e.currentTarget.value);
|
|
623
|
+
}
|
|
624
|
+
}}
|
|
625
|
+
/>
|
|
626
|
+
{isLoading && <p>Searching...</p>}
|
|
627
|
+
{error && <p>Error: {error.message}</p>}
|
|
628
|
+
</div>
|
|
629
|
+
);
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
**Example with predefined filters:**
|
|
633
|
+
|
|
634
|
+
```tsx
|
|
635
|
+
const { mapFirst } = useMapFirstCore({ ... });
|
|
636
|
+
const { search, isLoading, error } = useSmartFilterSearch(mapFirst);
|
|
637
|
+
|
|
638
|
+
const handleFilterSearch = async () => {
|
|
639
|
+
try {
|
|
640
|
+
await search({
|
|
641
|
+
filters: [
|
|
642
|
+
{ id: "pool", label: "Pool", type: "amenity", value: "pool" },
|
|
643
|
+
{ id: "4star", label: "4 Star", type: "starRating", value: "4", numericValue: 4 }
|
|
644
|
+
]
|
|
645
|
+
});
|
|
646
|
+
} catch (err) {
|
|
647
|
+
console.error("Search failed:", err);
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
return (
|
|
652
|
+
<button onClick={handleFilterSearch} disabled={isLoading}>
|
|
653
|
+
Apply Filters
|
|
654
|
+
</button>
|
|
655
|
+
);
|
|
656
|
+
```
|
|
657
|
+
|
|
502
658
|
## Legacy API
|
|
503
659
|
|
|
504
660
|
The old `useMapFirst` hook is still available but deprecated:
|