@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.
@@ -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='&copy; <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
@@ -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='&copy; <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
+ }