@placeslayer/sdk-react 1.0.0
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.cjs +337 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +64 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +312 -0
- package/dist/index.js.map +1 -0
- package/dist/map.cjs +322 -0
- package/dist/map.cjs.map +1 -0
- package/dist/map.d.cts +37 -0
- package/dist/map.d.ts +37 -0
- package/dist/map.js +294 -0
- package/dist/map.js.map +1 -0
- package/package.json +65 -0
package/dist/map.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/map/index.ts","../src/map/PlacesMap.tsx","../src/hooks/useAsync.ts","../src/map/hooks/useMapInsights.ts","../src/map/hooks/useSemanticSearch.ts"],"sourcesContent":["export { PlacesMap, type PlacesMapProps } from \"./PlacesMap\";\nexport { useMapInsights } from \"./hooks/useMapInsights\";\nexport { useSemanticSearch, type SemanticSearchState } from \"./hooks/useSemanticSearch\";\n","import {\n useCallback,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n} from \"react\";\nimport {\n MapContainer,\n TileLayer,\n Marker,\n Popup,\n useMap,\n} from \"react-leaflet\";\nimport L from \"leaflet\";\nimport { PlacesLayerClient, type CityInsight } from \"@placeslayer/sdk\";\nimport { useMapInsights } from \"./hooks/useMapInsights\";\nimport { useSemanticSearch } from \"./hooks/useSemanticSearch\";\n\nexport interface PlacesMapProps {\n apiKey: string;\n country: string;\n center?: [number, number];\n zoom?: number;\n onCitySelect?: (insight: CityInsight) => void;\n searchEnabled?: boolean;\n baseUrl?: string;\n className?: string;\n style?: CSSProperties;\n}\n\nconst DEFAULT_ICON = new L.Icon({\n iconUrl: \"https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png\",\n iconRetinaUrl: \"https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png\",\n shadowUrl: \"https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png\",\n iconSize: [25, 41],\n iconAnchor: [12, 41],\n popupAnchor: [1, -34],\n shadowSize: [41, 41],\n});\n\nconst HIGHLIGHT_ICON = new L.Icon({\n iconUrl: \"https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png\",\n iconRetinaUrl: \"https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png\",\n shadowUrl: \"https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png\",\n iconSize: [30, 49],\n iconAnchor: [15, 49],\n popupAnchor: [1, -40],\n shadowSize: [49, 49],\n className: \"pl-marker-highlight\",\n});\n\nfunction FitBounds({ insights }: { insights: CityInsight[] }) {\n const map = useMap();\n const fitted = useRef(false);\n\n if (!fitted.current && insights.length > 0) {\n const bounds = L.latLngBounds(\n insights\n .filter((i) => i.latitude && i.longitude)\n .map((i) => [i.latitude!, i.longitude!] as [number, number])\n );\n if (bounds.isValid()) {\n map.fitBounds(bounds, { padding: [40, 40] });\n fitted.current = true;\n }\n }\n\n return null;\n}\n\nfunction FitSearchResults({ results }: { results: CityInsight[] }) {\n const map = useMap();\n\n if (results.length > 0) {\n const bounds = L.latLngBounds(\n results\n .filter((i) => i.latitude && i.longitude)\n .map((i) => [i.latitude!, i.longitude!] as [number, number])\n );\n if (bounds.isValid()) {\n map.fitBounds(bounds, { padding: [40, 40] });\n }\n }\n\n return null;\n}\n\nexport function PlacesMap({\n apiKey,\n country,\n center = [0, 0],\n zoom = 5,\n onCitySelect,\n searchEnabled = false,\n baseUrl,\n className,\n style,\n}: PlacesMapProps) {\n const clientRef = useRef<PlacesLayerClient | null>(null);\n if (\n !clientRef.current ||\n (clientRef.current as unknown as { apiKey: string }).apiKey !== apiKey\n ) {\n clientRef.current = new PlacesLayerClient({ apiKey, baseUrl });\n }\n const client = clientRef.current;\n\n const [searchQuery, setSearchQuery] = useState(\"\");\n const { data: allInsights } = useMapInsights(client, country);\n const { results: searchResults } = useSemanticSearch(\n client,\n country,\n searchEnabled ? searchQuery : \"\"\n );\n\n const highlightedCodes = useMemo(\n () => new Set(searchResults.map((r) => r.city_code)),\n [searchResults]\n );\n\n const hasSearch = searchEnabled && searchQuery.trim() && searchResults.length > 0;\n const insights = allInsights ?? [];\n\n const handleSelect = useCallback(\n (insight: CityInsight) => {\n onCitySelect?.(insight);\n },\n [onCitySelect]\n );\n\n return (\n <div className={className} style={{ position: \"relative\", ...style }}>\n {searchEnabled && (\n <div style={styles.searchContainer}>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"Search cities by description...\"\n style={styles.searchInput}\n />\n </div>\n )}\n <MapContainer\n center={center}\n zoom={zoom}\n style={{ width: \"100%\", height: \"100%\", minHeight: 400 }}\n scrollWheelZoom\n >\n <TileLayer\n attribution='© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a>'\n url=\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\"\n />\n <FitBounds insights={insights} />\n {hasSearch && <FitSearchResults results={searchResults} />}\n {insights\n .filter((i) => i.latitude && i.longitude)\n .map((insight) => {\n const isHighlighted = hasSearch && highlightedCodes.has(insight.city_code);\n const isDimmed = hasSearch && !isHighlighted;\n\n return (\n <Marker\n key={insight.city_code}\n position={[insight.latitude!, insight.longitude!]}\n icon={isHighlighted ? HIGHLIGHT_ICON : DEFAULT_ICON}\n opacity={isDimmed ? 0.3 : 1}\n eventHandlers={{ click: () => handleSelect(insight) }}\n >\n <Popup>\n <div style={styles.popup}>\n <strong>{insight.city_name}</strong>\n {insight.vibe && (\n <div style={styles.vibe}>{insight.vibe}</div>\n )}\n {insight.ideal_for?.length > 0 && (\n <div style={styles.tags}>\n {insight.ideal_for.map((tag) => (\n <span key={tag} style={styles.tag}>\n {tag}\n </span>\n ))}\n </div>\n )}\n {insight.cost_of_living && (\n <div style={styles.meta}>\n Cost: {insight.cost_of_living.replace(\"_\", \" \")}\n </div>\n )}\n {insight.climate_zone && (\n <div style={styles.meta}>Climate: {insight.climate_zone}</div>\n )}\n {insight.summary && (\n <p style={styles.summary}>{insight.summary}</p>\n )}\n </div>\n </Popup>\n </Marker>\n );\n })}\n </MapContainer>\n </div>\n );\n}\n\nconst styles: Record<string, CSSProperties> = {\n searchContainer: {\n position: \"absolute\",\n top: 10,\n left: 50,\n right: 50,\n zIndex: 1000,\n },\n searchInput: {\n width: \"100%\",\n padding: \"10px 14px\",\n fontSize: \"14px\",\n border: \"2px solid #d1d5db\",\n borderRadius: \"8px\",\n background: \"#fff\",\n boxShadow: \"0 2px 8px rgba(0,0,0,.15)\",\n outline: \"none\",\n boxSizing: \"border-box\",\n },\n popup: {\n maxWidth: 250,\n fontSize: \"13px\",\n lineHeight: 1.4,\n },\n vibe: {\n color: \"#6b7280\",\n fontStyle: \"italic\",\n marginTop: 4,\n },\n tags: {\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: 4,\n marginTop: 6,\n },\n tag: {\n background: \"#e5e7eb\",\n borderRadius: 4,\n padding: \"2px 6px\",\n fontSize: \"11px\",\n },\n meta: {\n color: \"#6b7280\",\n fontSize: \"12px\",\n marginTop: 4,\n },\n summary: {\n marginTop: 8,\n fontSize: \"12px\",\n color: \"#374151\",\n },\n};\n","import { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport interface AsyncState<T> {\n data: T | null;\n loading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\nexport function useAsync<T>(\n fn: () => Promise<T>,\n deps: unknown[]\n): AsyncState<T> {\n const [data, setData] = useState<T | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n const version = useRef(0);\n\n const execute = useCallback(() => {\n const current = ++version.current;\n setLoading(true);\n setError(null);\n\n fn()\n .then((result) => {\n if (current === version.current) {\n setData(result);\n setLoading(false);\n }\n })\n .catch((err) => {\n if (current === version.current) {\n setError(err instanceof Error ? err : new Error(String(err)));\n setLoading(false);\n }\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n useEffect(() => {\n execute();\n }, [execute]);\n\n return { data, loading, error, refetch: execute };\n}\n","import type { CityInsight, PlacesLayerClient } from \"@placeslayer/sdk\";\nimport { type AsyncState, useAsync } from \"../../hooks/useAsync\";\n\nexport function useMapInsights(\n client: PlacesLayerClient,\n countryCode: string\n): AsyncState<CityInsight[]> {\n return useAsync(\n () => client.mapInsights(countryCode),\n [client, countryCode]\n );\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { PlacesLayerClient, SemanticSearchResult } from \"@placeslayer/sdk\";\n\nconst DEBOUNCE_MS = 500;\n\nexport interface SemanticSearchState {\n results: SemanticSearchResult[];\n loading: boolean;\n error: Error | null;\n}\n\nexport function useSemanticSearch(\n client: PlacesLayerClient,\n countryCode: string,\n query: string,\n opts?: { limit?: number; debounceMs?: number }\n): SemanticSearchState {\n const [results, setResults] = useState<SemanticSearchResult[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const version = useRef(0);\n const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n const delay = opts?.debounceMs ?? DEBOUNCE_MS;\n\n const search = useCallback(() => {\n const trimmed = query.trim();\n if (!trimmed) {\n setResults([]);\n setLoading(false);\n return;\n }\n\n const current = ++version.current;\n setLoading(true);\n\n client\n .semanticSearch(countryCode, trimmed, { limit: opts?.limit })\n .then((data) => {\n if (current === version.current) {\n setResults(data);\n setLoading(false);\n }\n })\n .catch((err) => {\n if (current === version.current) {\n setError(err instanceof Error ? err : new Error(String(err)));\n setLoading(false);\n }\n });\n }, [client, countryCode, query, opts?.limit]);\n\n useEffect(() => {\n clearTimeout(timerRef.current);\n timerRef.current = setTimeout(search, delay);\n return () => clearTimeout(timerRef.current);\n }, [search, delay]);\n\n return { results, loading, error };\n}\n"],"mappings":"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","names":["import_react","import_react","L"]}
|
package/dist/map.d.cts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CSSProperties } from 'react';
|
|
3
|
+
import { CityInsight, PlacesLayerClient, SemanticSearchResult } from '@placeslayer/sdk';
|
|
4
|
+
|
|
5
|
+
interface PlacesMapProps {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
country: string;
|
|
8
|
+
center?: [number, number];
|
|
9
|
+
zoom?: number;
|
|
10
|
+
onCitySelect?: (insight: CityInsight) => void;
|
|
11
|
+
searchEnabled?: boolean;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
}
|
|
16
|
+
declare function PlacesMap({ apiKey, country, center, zoom, onCitySelect, searchEnabled, baseUrl, className, style, }: PlacesMapProps): react_jsx_runtime.JSX.Element;
|
|
17
|
+
|
|
18
|
+
interface AsyncState<T> {
|
|
19
|
+
data: T | null;
|
|
20
|
+
loading: boolean;
|
|
21
|
+
error: Error | null;
|
|
22
|
+
refetch: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare function useMapInsights(client: PlacesLayerClient, countryCode: string): AsyncState<CityInsight[]>;
|
|
26
|
+
|
|
27
|
+
interface SemanticSearchState {
|
|
28
|
+
results: SemanticSearchResult[];
|
|
29
|
+
loading: boolean;
|
|
30
|
+
error: Error | null;
|
|
31
|
+
}
|
|
32
|
+
declare function useSemanticSearch(client: PlacesLayerClient, countryCode: string, query: string, opts?: {
|
|
33
|
+
limit?: number;
|
|
34
|
+
debounceMs?: number;
|
|
35
|
+
}): SemanticSearchState;
|
|
36
|
+
|
|
37
|
+
export { PlacesMap, type PlacesMapProps, type SemanticSearchState, useMapInsights, useSemanticSearch };
|
package/dist/map.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CSSProperties } from 'react';
|
|
3
|
+
import { CityInsight, PlacesLayerClient, SemanticSearchResult } from '@placeslayer/sdk';
|
|
4
|
+
|
|
5
|
+
interface PlacesMapProps {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
country: string;
|
|
8
|
+
center?: [number, number];
|
|
9
|
+
zoom?: number;
|
|
10
|
+
onCitySelect?: (insight: CityInsight) => void;
|
|
11
|
+
searchEnabled?: boolean;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
}
|
|
16
|
+
declare function PlacesMap({ apiKey, country, center, zoom, onCitySelect, searchEnabled, baseUrl, className, style, }: PlacesMapProps): react_jsx_runtime.JSX.Element;
|
|
17
|
+
|
|
18
|
+
interface AsyncState<T> {
|
|
19
|
+
data: T | null;
|
|
20
|
+
loading: boolean;
|
|
21
|
+
error: Error | null;
|
|
22
|
+
refetch: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare function useMapInsights(client: PlacesLayerClient, countryCode: string): AsyncState<CityInsight[]>;
|
|
26
|
+
|
|
27
|
+
interface SemanticSearchState {
|
|
28
|
+
results: SemanticSearchResult[];
|
|
29
|
+
loading: boolean;
|
|
30
|
+
error: Error | null;
|
|
31
|
+
}
|
|
32
|
+
declare function useSemanticSearch(client: PlacesLayerClient, countryCode: string, query: string, opts?: {
|
|
33
|
+
limit?: number;
|
|
34
|
+
debounceMs?: number;
|
|
35
|
+
}): SemanticSearchState;
|
|
36
|
+
|
|
37
|
+
export { PlacesMap, type PlacesMapProps, type SemanticSearchState, useMapInsights, useSemanticSearch };
|
package/dist/map.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// src/map/PlacesMap.tsx
|
|
2
|
+
import {
|
|
3
|
+
useCallback as useCallback3,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef as useRef3,
|
|
6
|
+
useState as useState3
|
|
7
|
+
} from "react";
|
|
8
|
+
import {
|
|
9
|
+
MapContainer,
|
|
10
|
+
TileLayer,
|
|
11
|
+
Marker,
|
|
12
|
+
Popup,
|
|
13
|
+
useMap
|
|
14
|
+
} from "react-leaflet";
|
|
15
|
+
import L from "leaflet";
|
|
16
|
+
import { PlacesLayerClient } from "@placeslayer/sdk";
|
|
17
|
+
|
|
18
|
+
// src/hooks/useAsync.ts
|
|
19
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
20
|
+
function useAsync(fn, deps) {
|
|
21
|
+
const [data, setData] = useState(null);
|
|
22
|
+
const [loading, setLoading] = useState(true);
|
|
23
|
+
const [error, setError] = useState(null);
|
|
24
|
+
const version = useRef(0);
|
|
25
|
+
const execute = useCallback(() => {
|
|
26
|
+
const current = ++version.current;
|
|
27
|
+
setLoading(true);
|
|
28
|
+
setError(null);
|
|
29
|
+
fn().then((result) => {
|
|
30
|
+
if (current === version.current) {
|
|
31
|
+
setData(result);
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
}).catch((err) => {
|
|
35
|
+
if (current === version.current) {
|
|
36
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}, deps);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
execute();
|
|
43
|
+
}, [execute]);
|
|
44
|
+
return { data, loading, error, refetch: execute };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/map/hooks/useMapInsights.ts
|
|
48
|
+
function useMapInsights(client, countryCode) {
|
|
49
|
+
return useAsync(
|
|
50
|
+
() => client.mapInsights(countryCode),
|
|
51
|
+
[client, countryCode]
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/map/hooks/useSemanticSearch.ts
|
|
56
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
57
|
+
var DEBOUNCE_MS = 500;
|
|
58
|
+
function useSemanticSearch(client, countryCode, query, opts) {
|
|
59
|
+
const [results, setResults] = useState2([]);
|
|
60
|
+
const [loading, setLoading] = useState2(false);
|
|
61
|
+
const [error, setError] = useState2(null);
|
|
62
|
+
const version = useRef2(0);
|
|
63
|
+
const timerRef = useRef2(void 0);
|
|
64
|
+
const delay = opts?.debounceMs ?? DEBOUNCE_MS;
|
|
65
|
+
const search = useCallback2(() => {
|
|
66
|
+
const trimmed = query.trim();
|
|
67
|
+
if (!trimmed) {
|
|
68
|
+
setResults([]);
|
|
69
|
+
setLoading(false);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const current = ++version.current;
|
|
73
|
+
setLoading(true);
|
|
74
|
+
client.semanticSearch(countryCode, trimmed, { limit: opts?.limit }).then((data) => {
|
|
75
|
+
if (current === version.current) {
|
|
76
|
+
setResults(data);
|
|
77
|
+
setLoading(false);
|
|
78
|
+
}
|
|
79
|
+
}).catch((err) => {
|
|
80
|
+
if (current === version.current) {
|
|
81
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
82
|
+
setLoading(false);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}, [client, countryCode, query, opts?.limit]);
|
|
86
|
+
useEffect2(() => {
|
|
87
|
+
clearTimeout(timerRef.current);
|
|
88
|
+
timerRef.current = setTimeout(search, delay);
|
|
89
|
+
return () => clearTimeout(timerRef.current);
|
|
90
|
+
}, [search, delay]);
|
|
91
|
+
return { results, loading, error };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/map/PlacesMap.tsx
|
|
95
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
96
|
+
var DEFAULT_ICON = new L.Icon({
|
|
97
|
+
iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
|
|
98
|
+
iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png",
|
|
99
|
+
shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
|
|
100
|
+
iconSize: [25, 41],
|
|
101
|
+
iconAnchor: [12, 41],
|
|
102
|
+
popupAnchor: [1, -34],
|
|
103
|
+
shadowSize: [41, 41]
|
|
104
|
+
});
|
|
105
|
+
var HIGHLIGHT_ICON = new L.Icon({
|
|
106
|
+
iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
|
|
107
|
+
iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png",
|
|
108
|
+
shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
|
|
109
|
+
iconSize: [30, 49],
|
|
110
|
+
iconAnchor: [15, 49],
|
|
111
|
+
popupAnchor: [1, -40],
|
|
112
|
+
shadowSize: [49, 49],
|
|
113
|
+
className: "pl-marker-highlight"
|
|
114
|
+
});
|
|
115
|
+
function FitBounds({ insights }) {
|
|
116
|
+
const map = useMap();
|
|
117
|
+
const fitted = useRef3(false);
|
|
118
|
+
if (!fitted.current && insights.length > 0) {
|
|
119
|
+
const bounds = L.latLngBounds(
|
|
120
|
+
insights.filter((i) => i.latitude && i.longitude).map((i) => [i.latitude, i.longitude])
|
|
121
|
+
);
|
|
122
|
+
if (bounds.isValid()) {
|
|
123
|
+
map.fitBounds(bounds, { padding: [40, 40] });
|
|
124
|
+
fitted.current = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
function FitSearchResults({ results }) {
|
|
130
|
+
const map = useMap();
|
|
131
|
+
if (results.length > 0) {
|
|
132
|
+
const bounds = L.latLngBounds(
|
|
133
|
+
results.filter((i) => i.latitude && i.longitude).map((i) => [i.latitude, i.longitude])
|
|
134
|
+
);
|
|
135
|
+
if (bounds.isValid()) {
|
|
136
|
+
map.fitBounds(bounds, { padding: [40, 40] });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
function PlacesMap({
|
|
142
|
+
apiKey,
|
|
143
|
+
country,
|
|
144
|
+
center = [0, 0],
|
|
145
|
+
zoom = 5,
|
|
146
|
+
onCitySelect,
|
|
147
|
+
searchEnabled = false,
|
|
148
|
+
baseUrl,
|
|
149
|
+
className,
|
|
150
|
+
style
|
|
151
|
+
}) {
|
|
152
|
+
const clientRef = useRef3(null);
|
|
153
|
+
if (!clientRef.current || clientRef.current.apiKey !== apiKey) {
|
|
154
|
+
clientRef.current = new PlacesLayerClient({ apiKey, baseUrl });
|
|
155
|
+
}
|
|
156
|
+
const client = clientRef.current;
|
|
157
|
+
const [searchQuery, setSearchQuery] = useState3("");
|
|
158
|
+
const { data: allInsights } = useMapInsights(client, country);
|
|
159
|
+
const { results: searchResults } = useSemanticSearch(
|
|
160
|
+
client,
|
|
161
|
+
country,
|
|
162
|
+
searchEnabled ? searchQuery : ""
|
|
163
|
+
);
|
|
164
|
+
const highlightedCodes = useMemo(
|
|
165
|
+
() => new Set(searchResults.map((r) => r.city_code)),
|
|
166
|
+
[searchResults]
|
|
167
|
+
);
|
|
168
|
+
const hasSearch = searchEnabled && searchQuery.trim() && searchResults.length > 0;
|
|
169
|
+
const insights = allInsights ?? [];
|
|
170
|
+
const handleSelect = useCallback3(
|
|
171
|
+
(insight) => {
|
|
172
|
+
onCitySelect?.(insight);
|
|
173
|
+
},
|
|
174
|
+
[onCitySelect]
|
|
175
|
+
);
|
|
176
|
+
return /* @__PURE__ */ jsxs("div", { className, style: { position: "relative", ...style }, children: [
|
|
177
|
+
searchEnabled && /* @__PURE__ */ jsx("div", { style: styles.searchContainer, children: /* @__PURE__ */ jsx(
|
|
178
|
+
"input",
|
|
179
|
+
{
|
|
180
|
+
type: "text",
|
|
181
|
+
value: searchQuery,
|
|
182
|
+
onChange: (e) => setSearchQuery(e.target.value),
|
|
183
|
+
placeholder: "Search cities by description...",
|
|
184
|
+
style: styles.searchInput
|
|
185
|
+
}
|
|
186
|
+
) }),
|
|
187
|
+
/* @__PURE__ */ jsxs(
|
|
188
|
+
MapContainer,
|
|
189
|
+
{
|
|
190
|
+
center,
|
|
191
|
+
zoom,
|
|
192
|
+
style: { width: "100%", height: "100%", minHeight: 400 },
|
|
193
|
+
scrollWheelZoom: true,
|
|
194
|
+
children: [
|
|
195
|
+
/* @__PURE__ */ jsx(
|
|
196
|
+
TileLayer,
|
|
197
|
+
{
|
|
198
|
+
attribution: '\xA9 <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
|
199
|
+
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
200
|
+
}
|
|
201
|
+
),
|
|
202
|
+
/* @__PURE__ */ jsx(FitBounds, { insights }),
|
|
203
|
+
hasSearch && /* @__PURE__ */ jsx(FitSearchResults, { results: searchResults }),
|
|
204
|
+
insights.filter((i) => i.latitude && i.longitude).map((insight) => {
|
|
205
|
+
const isHighlighted = hasSearch && highlightedCodes.has(insight.city_code);
|
|
206
|
+
const isDimmed = hasSearch && !isHighlighted;
|
|
207
|
+
return /* @__PURE__ */ jsx(
|
|
208
|
+
Marker,
|
|
209
|
+
{
|
|
210
|
+
position: [insight.latitude, insight.longitude],
|
|
211
|
+
icon: isHighlighted ? HIGHLIGHT_ICON : DEFAULT_ICON,
|
|
212
|
+
opacity: isDimmed ? 0.3 : 1,
|
|
213
|
+
eventHandlers: { click: () => handleSelect(insight) },
|
|
214
|
+
children: /* @__PURE__ */ jsx(Popup, { children: /* @__PURE__ */ jsxs("div", { style: styles.popup, children: [
|
|
215
|
+
/* @__PURE__ */ jsx("strong", { children: insight.city_name }),
|
|
216
|
+
insight.vibe && /* @__PURE__ */ jsx("div", { style: styles.vibe, children: insight.vibe }),
|
|
217
|
+
insight.ideal_for?.length > 0 && /* @__PURE__ */ jsx("div", { style: styles.tags, children: insight.ideal_for.map((tag) => /* @__PURE__ */ jsx("span", { style: styles.tag, children: tag }, tag)) }),
|
|
218
|
+
insight.cost_of_living && /* @__PURE__ */ jsxs("div", { style: styles.meta, children: [
|
|
219
|
+
"Cost: ",
|
|
220
|
+
insight.cost_of_living.replace("_", " ")
|
|
221
|
+
] }),
|
|
222
|
+
insight.climate_zone && /* @__PURE__ */ jsxs("div", { style: styles.meta, children: [
|
|
223
|
+
"Climate: ",
|
|
224
|
+
insight.climate_zone
|
|
225
|
+
] }),
|
|
226
|
+
insight.summary && /* @__PURE__ */ jsx("p", { style: styles.summary, children: insight.summary })
|
|
227
|
+
] }) })
|
|
228
|
+
},
|
|
229
|
+
insight.city_code
|
|
230
|
+
);
|
|
231
|
+
})
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
] });
|
|
236
|
+
}
|
|
237
|
+
var styles = {
|
|
238
|
+
searchContainer: {
|
|
239
|
+
position: "absolute",
|
|
240
|
+
top: 10,
|
|
241
|
+
left: 50,
|
|
242
|
+
right: 50,
|
|
243
|
+
zIndex: 1e3
|
|
244
|
+
},
|
|
245
|
+
searchInput: {
|
|
246
|
+
width: "100%",
|
|
247
|
+
padding: "10px 14px",
|
|
248
|
+
fontSize: "14px",
|
|
249
|
+
border: "2px solid #d1d5db",
|
|
250
|
+
borderRadius: "8px",
|
|
251
|
+
background: "#fff",
|
|
252
|
+
boxShadow: "0 2px 8px rgba(0,0,0,.15)",
|
|
253
|
+
outline: "none",
|
|
254
|
+
boxSizing: "border-box"
|
|
255
|
+
},
|
|
256
|
+
popup: {
|
|
257
|
+
maxWidth: 250,
|
|
258
|
+
fontSize: "13px",
|
|
259
|
+
lineHeight: 1.4
|
|
260
|
+
},
|
|
261
|
+
vibe: {
|
|
262
|
+
color: "#6b7280",
|
|
263
|
+
fontStyle: "italic",
|
|
264
|
+
marginTop: 4
|
|
265
|
+
},
|
|
266
|
+
tags: {
|
|
267
|
+
display: "flex",
|
|
268
|
+
flexWrap: "wrap",
|
|
269
|
+
gap: 4,
|
|
270
|
+
marginTop: 6
|
|
271
|
+
},
|
|
272
|
+
tag: {
|
|
273
|
+
background: "#e5e7eb",
|
|
274
|
+
borderRadius: 4,
|
|
275
|
+
padding: "2px 6px",
|
|
276
|
+
fontSize: "11px"
|
|
277
|
+
},
|
|
278
|
+
meta: {
|
|
279
|
+
color: "#6b7280",
|
|
280
|
+
fontSize: "12px",
|
|
281
|
+
marginTop: 4
|
|
282
|
+
},
|
|
283
|
+
summary: {
|
|
284
|
+
marginTop: 8,
|
|
285
|
+
fontSize: "12px",
|
|
286
|
+
color: "#374151"
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
export {
|
|
290
|
+
PlacesMap,
|
|
291
|
+
useMapInsights,
|
|
292
|
+
useSemanticSearch
|
|
293
|
+
};
|
|
294
|
+
//# sourceMappingURL=map.js.map
|
package/dist/map.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/map/PlacesMap.tsx","../src/hooks/useAsync.ts","../src/map/hooks/useMapInsights.ts","../src/map/hooks/useSemanticSearch.ts"],"sourcesContent":["import {\n useCallback,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n} from \"react\";\nimport {\n MapContainer,\n TileLayer,\n Marker,\n Popup,\n useMap,\n} from \"react-leaflet\";\nimport L from \"leaflet\";\nimport { PlacesLayerClient, type CityInsight } from \"@placeslayer/sdk\";\nimport { useMapInsights } from \"./hooks/useMapInsights\";\nimport { useSemanticSearch } from \"./hooks/useSemanticSearch\";\n\nexport interface PlacesMapProps {\n apiKey: string;\n country: string;\n center?: [number, number];\n zoom?: number;\n onCitySelect?: (insight: CityInsight) => void;\n searchEnabled?: boolean;\n baseUrl?: string;\n className?: string;\n style?: CSSProperties;\n}\n\nconst DEFAULT_ICON = new L.Icon({\n iconUrl: \"https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png\",\n iconRetinaUrl: \"https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png\",\n shadowUrl: \"https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png\",\n iconSize: [25, 41],\n iconAnchor: [12, 41],\n popupAnchor: [1, -34],\n shadowSize: [41, 41],\n});\n\nconst HIGHLIGHT_ICON = new L.Icon({\n iconUrl: \"https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png\",\n iconRetinaUrl: \"https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png\",\n shadowUrl: \"https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png\",\n iconSize: [30, 49],\n iconAnchor: [15, 49],\n popupAnchor: [1, -40],\n shadowSize: [49, 49],\n className: \"pl-marker-highlight\",\n});\n\nfunction FitBounds({ insights }: { insights: CityInsight[] }) {\n const map = useMap();\n const fitted = useRef(false);\n\n if (!fitted.current && insights.length > 0) {\n const bounds = L.latLngBounds(\n insights\n .filter((i) => i.latitude && i.longitude)\n .map((i) => [i.latitude!, i.longitude!] as [number, number])\n );\n if (bounds.isValid()) {\n map.fitBounds(bounds, { padding: [40, 40] });\n fitted.current = true;\n }\n }\n\n return null;\n}\n\nfunction FitSearchResults({ results }: { results: CityInsight[] }) {\n const map = useMap();\n\n if (results.length > 0) {\n const bounds = L.latLngBounds(\n results\n .filter((i) => i.latitude && i.longitude)\n .map((i) => [i.latitude!, i.longitude!] as [number, number])\n );\n if (bounds.isValid()) {\n map.fitBounds(bounds, { padding: [40, 40] });\n }\n }\n\n return null;\n}\n\nexport function PlacesMap({\n apiKey,\n country,\n center = [0, 0],\n zoom = 5,\n onCitySelect,\n searchEnabled = false,\n baseUrl,\n className,\n style,\n}: PlacesMapProps) {\n const clientRef = useRef<PlacesLayerClient | null>(null);\n if (\n !clientRef.current ||\n (clientRef.current as unknown as { apiKey: string }).apiKey !== apiKey\n ) {\n clientRef.current = new PlacesLayerClient({ apiKey, baseUrl });\n }\n const client = clientRef.current;\n\n const [searchQuery, setSearchQuery] = useState(\"\");\n const { data: allInsights } = useMapInsights(client, country);\n const { results: searchResults } = useSemanticSearch(\n client,\n country,\n searchEnabled ? searchQuery : \"\"\n );\n\n const highlightedCodes = useMemo(\n () => new Set(searchResults.map((r) => r.city_code)),\n [searchResults]\n );\n\n const hasSearch = searchEnabled && searchQuery.trim() && searchResults.length > 0;\n const insights = allInsights ?? [];\n\n const handleSelect = useCallback(\n (insight: CityInsight) => {\n onCitySelect?.(insight);\n },\n [onCitySelect]\n );\n\n return (\n <div className={className} style={{ position: \"relative\", ...style }}>\n {searchEnabled && (\n <div style={styles.searchContainer}>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"Search cities by description...\"\n style={styles.searchInput}\n />\n </div>\n )}\n <MapContainer\n center={center}\n zoom={zoom}\n style={{ width: \"100%\", height: \"100%\", minHeight: 400 }}\n scrollWheelZoom\n >\n <TileLayer\n attribution='© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a>'\n url=\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\"\n />\n <FitBounds insights={insights} />\n {hasSearch && <FitSearchResults results={searchResults} />}\n {insights\n .filter((i) => i.latitude && i.longitude)\n .map((insight) => {\n const isHighlighted = hasSearch && highlightedCodes.has(insight.city_code);\n const isDimmed = hasSearch && !isHighlighted;\n\n return (\n <Marker\n key={insight.city_code}\n position={[insight.latitude!, insight.longitude!]}\n icon={isHighlighted ? HIGHLIGHT_ICON : DEFAULT_ICON}\n opacity={isDimmed ? 0.3 : 1}\n eventHandlers={{ click: () => handleSelect(insight) }}\n >\n <Popup>\n <div style={styles.popup}>\n <strong>{insight.city_name}</strong>\n {insight.vibe && (\n <div style={styles.vibe}>{insight.vibe}</div>\n )}\n {insight.ideal_for?.length > 0 && (\n <div style={styles.tags}>\n {insight.ideal_for.map((tag) => (\n <span key={tag} style={styles.tag}>\n {tag}\n </span>\n ))}\n </div>\n )}\n {insight.cost_of_living && (\n <div style={styles.meta}>\n Cost: {insight.cost_of_living.replace(\"_\", \" \")}\n </div>\n )}\n {insight.climate_zone && (\n <div style={styles.meta}>Climate: {insight.climate_zone}</div>\n )}\n {insight.summary && (\n <p style={styles.summary}>{insight.summary}</p>\n )}\n </div>\n </Popup>\n </Marker>\n );\n })}\n </MapContainer>\n </div>\n );\n}\n\nconst styles: Record<string, CSSProperties> = {\n searchContainer: {\n position: \"absolute\",\n top: 10,\n left: 50,\n right: 50,\n zIndex: 1000,\n },\n searchInput: {\n width: \"100%\",\n padding: \"10px 14px\",\n fontSize: \"14px\",\n border: \"2px solid #d1d5db\",\n borderRadius: \"8px\",\n background: \"#fff\",\n boxShadow: \"0 2px 8px rgba(0,0,0,.15)\",\n outline: \"none\",\n boxSizing: \"border-box\",\n },\n popup: {\n maxWidth: 250,\n fontSize: \"13px\",\n lineHeight: 1.4,\n },\n vibe: {\n color: \"#6b7280\",\n fontStyle: \"italic\",\n marginTop: 4,\n },\n tags: {\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: 4,\n marginTop: 6,\n },\n tag: {\n background: \"#e5e7eb\",\n borderRadius: 4,\n padding: \"2px 6px\",\n fontSize: \"11px\",\n },\n meta: {\n color: \"#6b7280\",\n fontSize: \"12px\",\n marginTop: 4,\n },\n summary: {\n marginTop: 8,\n fontSize: \"12px\",\n color: \"#374151\",\n },\n};\n","import { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport interface AsyncState<T> {\n data: T | null;\n loading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\nexport function useAsync<T>(\n fn: () => Promise<T>,\n deps: unknown[]\n): AsyncState<T> {\n const [data, setData] = useState<T | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n const version = useRef(0);\n\n const execute = useCallback(() => {\n const current = ++version.current;\n setLoading(true);\n setError(null);\n\n fn()\n .then((result) => {\n if (current === version.current) {\n setData(result);\n setLoading(false);\n }\n })\n .catch((err) => {\n if (current === version.current) {\n setError(err instanceof Error ? err : new Error(String(err)));\n setLoading(false);\n }\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n useEffect(() => {\n execute();\n }, [execute]);\n\n return { data, loading, error, refetch: execute };\n}\n","import type { CityInsight, PlacesLayerClient } from \"@placeslayer/sdk\";\nimport { type AsyncState, useAsync } from \"../../hooks/useAsync\";\n\nexport function useMapInsights(\n client: PlacesLayerClient,\n countryCode: string\n): AsyncState<CityInsight[]> {\n return useAsync(\n () => client.mapInsights(countryCode),\n [client, countryCode]\n );\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { PlacesLayerClient, SemanticSearchResult } from \"@placeslayer/sdk\";\n\nconst DEBOUNCE_MS = 500;\n\nexport interface SemanticSearchState {\n results: SemanticSearchResult[];\n loading: boolean;\n error: Error | null;\n}\n\nexport function useSemanticSearch(\n client: PlacesLayerClient,\n countryCode: string,\n query: string,\n opts?: { limit?: number; debounceMs?: number }\n): SemanticSearchState {\n const [results, setResults] = useState<SemanticSearchResult[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const version = useRef(0);\n const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n const delay = opts?.debounceMs ?? DEBOUNCE_MS;\n\n const search = useCallback(() => {\n const trimmed = query.trim();\n if (!trimmed) {\n setResults([]);\n setLoading(false);\n return;\n }\n\n const current = ++version.current;\n setLoading(true);\n\n client\n .semanticSearch(countryCode, trimmed, { limit: opts?.limit })\n .then((data) => {\n if (current === version.current) {\n setResults(data);\n setLoading(false);\n }\n })\n .catch((err) => {\n if (current === version.current) {\n setError(err instanceof Error ? err : new Error(String(err)));\n setLoading(false);\n }\n });\n }, [client, countryCode, query, opts?.limit]);\n\n useEffect(() => {\n clearTimeout(timerRef.current);\n timerRef.current = setTimeout(search, delay);\n return () => clearTimeout(timerRef.current);\n }, [search, delay]);\n\n return { results, loading, error };\n}\n"],"mappings":"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","names":["useCallback","useRef","useState","useCallback","useEffect","useRef","useState","useRef","useState","useCallback"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@placeslayer/sdk-react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React components and hooks for PlacesLayer",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./map": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/map.d.ts",
|
|
23
|
+
"default": "./dist/map.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/map.d.cts",
|
|
27
|
+
"default": "./dist/map.cjs"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"prepublishOnly": "npm run build"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@placeslayer/sdk": "^0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"react": ">=18",
|
|
44
|
+
"react-dom": ">=18",
|
|
45
|
+
"leaflet": ">=1.9",
|
|
46
|
+
"react-leaflet": ">=4"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"leaflet": { "optional": true },
|
|
50
|
+
"react-leaflet": { "optional": true }
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"placeslayer",
|
|
54
|
+
"react",
|
|
55
|
+
"hooks",
|
|
56
|
+
"leaflet",
|
|
57
|
+
"map",
|
|
58
|
+
"geography"
|
|
59
|
+
],
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"repository": {
|
|
62
|
+
"type": "git",
|
|
63
|
+
"url": "https://github.com/hexar/placeslayer-sdk"
|
|
64
|
+
}
|
|
65
|
+
}
|