@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/index.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// src/CitiesDropdown.tsx
|
|
2
|
+
import {
|
|
3
|
+
useCallback as useCallback2,
|
|
4
|
+
useEffect as useEffect2,
|
|
5
|
+
useRef as useRef2,
|
|
6
|
+
useState as useState2
|
|
7
|
+
} from "react";
|
|
8
|
+
import {
|
|
9
|
+
PlacesLayerClient
|
|
10
|
+
} from "@placeslayer/sdk";
|
|
11
|
+
|
|
12
|
+
// src/hooks/useAutocomplete.ts
|
|
13
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
14
|
+
var DEBOUNCE_MS = 300;
|
|
15
|
+
function useAutocomplete(client, countryCode, query, opts) {
|
|
16
|
+
const [results, setResults] = useState([]);
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
const [error, setError] = useState(null);
|
|
19
|
+
const version = useRef(0);
|
|
20
|
+
const timerRef = useRef(void 0);
|
|
21
|
+
const delay = opts?.debounceMs ?? DEBOUNCE_MS;
|
|
22
|
+
const search = useCallback(() => {
|
|
23
|
+
const trimmed = query.trim();
|
|
24
|
+
if (!trimmed) {
|
|
25
|
+
setResults([]);
|
|
26
|
+
setLoading(false);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const current = ++version.current;
|
|
30
|
+
setLoading(true);
|
|
31
|
+
client.autocomplete(countryCode, trimmed, { limit: opts?.limit }).then((data) => {
|
|
32
|
+
if (current === version.current) {
|
|
33
|
+
setResults(data);
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
}).catch((err) => {
|
|
37
|
+
if (current === version.current) {
|
|
38
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
39
|
+
setLoading(false);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}, [client, countryCode, query, opts?.limit]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
clearTimeout(timerRef.current);
|
|
45
|
+
timerRef.current = setTimeout(search, delay);
|
|
46
|
+
return () => clearTimeout(timerRef.current);
|
|
47
|
+
}, [search, delay]);
|
|
48
|
+
return { results, loading, error };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/CitiesDropdown.tsx
|
|
52
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
53
|
+
function CitiesDropdown({
|
|
54
|
+
apiKey,
|
|
55
|
+
country,
|
|
56
|
+
province,
|
|
57
|
+
onSelect,
|
|
58
|
+
limit = 10,
|
|
59
|
+
lang = "default",
|
|
60
|
+
placeholder = "Search cities...",
|
|
61
|
+
className,
|
|
62
|
+
baseUrl,
|
|
63
|
+
theme
|
|
64
|
+
}) {
|
|
65
|
+
const clientRef = useRef2(null);
|
|
66
|
+
if (!clientRef.current || clientRef.current.apiKey !== apiKey) {
|
|
67
|
+
clientRef.current = new PlacesLayerClient({ apiKey, baseUrl });
|
|
68
|
+
}
|
|
69
|
+
const client = clientRef.current;
|
|
70
|
+
const [query, setQuery] = useState2("");
|
|
71
|
+
const [open, setOpen] = useState2(false);
|
|
72
|
+
const [activeIndex, setActiveIndex] = useState2(-1);
|
|
73
|
+
const containerRef = useRef2(null);
|
|
74
|
+
const prevCountry = useRef2(country);
|
|
75
|
+
const prevProvince = useRef2(province);
|
|
76
|
+
useEffect2(() => {
|
|
77
|
+
if (prevCountry.current !== country || prevProvince.current !== province) {
|
|
78
|
+
setQuery("");
|
|
79
|
+
setOpen(false);
|
|
80
|
+
prevCountry.current = country;
|
|
81
|
+
prevProvince.current = province;
|
|
82
|
+
}
|
|
83
|
+
}, [country, province]);
|
|
84
|
+
const { results, loading } = useAutocomplete(client, country, query, {
|
|
85
|
+
limit
|
|
86
|
+
});
|
|
87
|
+
const filtered = province ? results.filter(
|
|
88
|
+
(r) => r.type === "city" && r.province_code === province
|
|
89
|
+
) : results;
|
|
90
|
+
useEffect2(() => {
|
|
91
|
+
const handler = (e) => {
|
|
92
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
93
|
+
setOpen(false);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
document.addEventListener("mousedown", handler);
|
|
97
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
98
|
+
}, []);
|
|
99
|
+
useEffect2(() => {
|
|
100
|
+
setActiveIndex(-1);
|
|
101
|
+
}, [filtered.length]);
|
|
102
|
+
const handleSelect = useCallback2(
|
|
103
|
+
async (result) => {
|
|
104
|
+
setQuery(result.name);
|
|
105
|
+
setOpen(false);
|
|
106
|
+
const detail = await client.city(result.country_code, result.code);
|
|
107
|
+
onSelect(detail);
|
|
108
|
+
},
|
|
109
|
+
[client, onSelect]
|
|
110
|
+
);
|
|
111
|
+
const handleKeyDown = (e) => {
|
|
112
|
+
if (!open || filtered.length === 0) return;
|
|
113
|
+
switch (e.key) {
|
|
114
|
+
case "ArrowDown":
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
setActiveIndex(
|
|
117
|
+
(prev) => prev < filtered.length - 1 ? prev + 1 : 0
|
|
118
|
+
);
|
|
119
|
+
break;
|
|
120
|
+
case "ArrowUp":
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
setActiveIndex(
|
|
123
|
+
(prev) => prev > 0 ? prev - 1 : filtered.length - 1
|
|
124
|
+
);
|
|
125
|
+
break;
|
|
126
|
+
case "Enter":
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
if (activeIndex >= 0 && activeIndex < filtered.length) {
|
|
129
|
+
handleSelect(filtered[activeIndex]);
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
case "Escape":
|
|
133
|
+
setOpen(false);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const t = theme ?? {};
|
|
138
|
+
const borderColor = t.borderColor ?? "#d1d5db";
|
|
139
|
+
const borderRadius = t.borderRadius ?? "6px";
|
|
140
|
+
const background = t.background ?? "#fff";
|
|
141
|
+
const textColor = t.textColor ?? "inherit";
|
|
142
|
+
const secondaryTextColor = t.secondaryTextColor ?? "#6b7280";
|
|
143
|
+
const activeBackground = t.activeBackground ?? "#f3f4f6";
|
|
144
|
+
const fontSize = t.fontSize ?? "14px";
|
|
145
|
+
const s = {
|
|
146
|
+
container: { position: "relative", width: "100%" },
|
|
147
|
+
input: {
|
|
148
|
+
width: "100%",
|
|
149
|
+
padding: "8px 12px",
|
|
150
|
+
fontSize,
|
|
151
|
+
color: textColor,
|
|
152
|
+
background,
|
|
153
|
+
border: `1px solid ${borderColor}`,
|
|
154
|
+
borderRadius,
|
|
155
|
+
outline: "none",
|
|
156
|
+
boxSizing: "border-box"
|
|
157
|
+
},
|
|
158
|
+
list: {
|
|
159
|
+
position: "absolute",
|
|
160
|
+
top: "100%",
|
|
161
|
+
left: 0,
|
|
162
|
+
right: 0,
|
|
163
|
+
margin: "4px 0 0",
|
|
164
|
+
padding: 0,
|
|
165
|
+
listStyle: "none",
|
|
166
|
+
background,
|
|
167
|
+
border: `1px solid ${borderColor}`,
|
|
168
|
+
borderRadius,
|
|
169
|
+
boxShadow: "0 4px 6px -1px rgba(0,0,0,.1)",
|
|
170
|
+
maxHeight: "240px",
|
|
171
|
+
overflowY: "auto",
|
|
172
|
+
zIndex: 50
|
|
173
|
+
},
|
|
174
|
+
item: {
|
|
175
|
+
display: "flex",
|
|
176
|
+
justifyContent: "space-between",
|
|
177
|
+
alignItems: "center",
|
|
178
|
+
padding: "8px 12px",
|
|
179
|
+
cursor: "pointer",
|
|
180
|
+
fontSize,
|
|
181
|
+
color: textColor
|
|
182
|
+
},
|
|
183
|
+
itemActive: { background: activeBackground },
|
|
184
|
+
name: { fontWeight: 500 },
|
|
185
|
+
province: { fontSize: "12px", color: secondaryTextColor },
|
|
186
|
+
status: {
|
|
187
|
+
position: "absolute",
|
|
188
|
+
top: "100%",
|
|
189
|
+
left: 0,
|
|
190
|
+
right: 0,
|
|
191
|
+
margin: "4px 0 0",
|
|
192
|
+
padding: "8px 12px",
|
|
193
|
+
background,
|
|
194
|
+
border: `1px solid ${borderColor}`,
|
|
195
|
+
borderRadius,
|
|
196
|
+
fontSize,
|
|
197
|
+
color: secondaryTextColor
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, style: s.container, className, children: [
|
|
201
|
+
/* @__PURE__ */ jsx(
|
|
202
|
+
"input",
|
|
203
|
+
{
|
|
204
|
+
type: "text",
|
|
205
|
+
value: query,
|
|
206
|
+
onChange: (e) => {
|
|
207
|
+
setQuery(e.target.value);
|
|
208
|
+
setOpen(true);
|
|
209
|
+
},
|
|
210
|
+
onFocus: () => query.trim() && setOpen(true),
|
|
211
|
+
onKeyDown: handleKeyDown,
|
|
212
|
+
placeholder,
|
|
213
|
+
style: s.input,
|
|
214
|
+
role: "combobox",
|
|
215
|
+
"aria-expanded": open,
|
|
216
|
+
"aria-autocomplete": "list",
|
|
217
|
+
"aria-activedescendant": activeIndex >= 0 ? `pl-option-${activeIndex}` : void 0
|
|
218
|
+
}
|
|
219
|
+
),
|
|
220
|
+
open && filtered.length > 0 && /* @__PURE__ */ jsx("ul", { role: "listbox", style: s.list, children: filtered.map((result, i) => /* @__PURE__ */ jsxs(
|
|
221
|
+
"li",
|
|
222
|
+
{
|
|
223
|
+
id: `pl-option-${i}`,
|
|
224
|
+
role: "option",
|
|
225
|
+
"aria-selected": i === activeIndex,
|
|
226
|
+
style: {
|
|
227
|
+
...s.item,
|
|
228
|
+
...i === activeIndex ? s.itemActive : {}
|
|
229
|
+
},
|
|
230
|
+
onMouseEnter: () => setActiveIndex(i),
|
|
231
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
232
|
+
onClick: () => handleSelect(result),
|
|
233
|
+
children: [
|
|
234
|
+
/* @__PURE__ */ jsx("span", { style: s.name, children: result.name }),
|
|
235
|
+
result.province_name && /* @__PURE__ */ jsx("span", { style: s.province, children: result.province_name })
|
|
236
|
+
]
|
|
237
|
+
},
|
|
238
|
+
result.code
|
|
239
|
+
)) }),
|
|
240
|
+
open && loading && filtered.length === 0 && /* @__PURE__ */ jsx("div", { style: s.status, children: "Loading..." })
|
|
241
|
+
] });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/hooks/usePlacesLayer.ts
|
|
245
|
+
import { useRef as useRef3 } from "react";
|
|
246
|
+
import { PlacesLayerClient as PlacesLayerClient2 } from "@placeslayer/sdk";
|
|
247
|
+
function usePlacesLayerClient(config) {
|
|
248
|
+
const ref = useRef3(null);
|
|
249
|
+
if (!ref.current || ref.current.apiKey !== config.apiKey) {
|
|
250
|
+
ref.current = new PlacesLayerClient2(config);
|
|
251
|
+
}
|
|
252
|
+
return ref.current;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/hooks/useAsync.ts
|
|
256
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef4, useState as useState3 } from "react";
|
|
257
|
+
function useAsync(fn, deps) {
|
|
258
|
+
const [data, setData] = useState3(null);
|
|
259
|
+
const [loading, setLoading] = useState3(true);
|
|
260
|
+
const [error, setError] = useState3(null);
|
|
261
|
+
const version = useRef4(0);
|
|
262
|
+
const execute = useCallback3(() => {
|
|
263
|
+
const current = ++version.current;
|
|
264
|
+
setLoading(true);
|
|
265
|
+
setError(null);
|
|
266
|
+
fn().then((result) => {
|
|
267
|
+
if (current === version.current) {
|
|
268
|
+
setData(result);
|
|
269
|
+
setLoading(false);
|
|
270
|
+
}
|
|
271
|
+
}).catch((err) => {
|
|
272
|
+
if (current === version.current) {
|
|
273
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
274
|
+
setLoading(false);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}, deps);
|
|
278
|
+
useEffect3(() => {
|
|
279
|
+
execute();
|
|
280
|
+
}, [execute]);
|
|
281
|
+
return { data, loading, error, refetch: execute };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/hooks/useCountries.ts
|
|
285
|
+
function useCountries(client) {
|
|
286
|
+
return useAsync(() => client.countries(), [client]);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/hooks/useProvinces.ts
|
|
290
|
+
function useProvinces(client, countryCode, opts) {
|
|
291
|
+
return useAsync(
|
|
292
|
+
() => client.provinces(countryCode, opts),
|
|
293
|
+
[client, countryCode, opts?.native]
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/hooks/useCities.ts
|
|
298
|
+
function useCities(client, countryCode, provinceCode, opts) {
|
|
299
|
+
return useAsync(
|
|
300
|
+
() => client.cities(countryCode, provinceCode, opts),
|
|
301
|
+
[client, countryCode, provinceCode, opts?.native]
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
export {
|
|
305
|
+
CitiesDropdown,
|
|
306
|
+
useAutocomplete,
|
|
307
|
+
useCities,
|
|
308
|
+
useCountries,
|
|
309
|
+
usePlacesLayerClient,
|
|
310
|
+
useProvinces
|
|
311
|
+
};
|
|
312
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/CitiesDropdown.tsx","../src/hooks/useAutocomplete.ts","../src/hooks/usePlacesLayer.ts","../src/hooks/useAsync.ts","../src/hooks/useCountries.ts","../src/hooks/useProvinces.ts","../src/hooks/useCities.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useRef,\n useState,\n type CSSProperties,\n type KeyboardEvent,\n} from \"react\";\nimport {\n PlacesLayerClient,\n type AutocompleteResult,\n type CityDetail,\n} from \"@placeslayer/sdk\";\nimport { useAutocomplete } from \"./hooks/useAutocomplete\";\n\nexport interface CitiesDropdownTheme {\n /** Input border color (default: \"#d1d5db\") */\n borderColor?: string;\n /** Input border radius (default: \"6px\") */\n borderRadius?: string;\n /** Dropdown background color (default: \"#fff\") */\n background?: string;\n /** Main text color (default: \"inherit\") */\n textColor?: string;\n /** Secondary text color for province labels (default: \"#6b7280\") */\n secondaryTextColor?: string;\n /** Active/hovered item background (default: \"#f3f4f6\") */\n activeBackground?: string;\n /** Input font size (default: \"14px\") */\n fontSize?: string;\n}\n\nexport interface CitiesDropdownProps {\n apiKey: string;\n country: string;\n province?: string;\n onSelect: (city: CityDetail) => void;\n limit?: number;\n lang?: \"default\" | \"native\";\n placeholder?: string;\n className?: string;\n baseUrl?: string;\n /** Customize dropdown colors and appearance */\n theme?: CitiesDropdownTheme;\n}\n\nexport function CitiesDropdown({\n apiKey,\n country,\n province,\n onSelect,\n limit = 10,\n lang = \"default\",\n placeholder = \"Search cities...\",\n className,\n baseUrl,\n theme,\n}: CitiesDropdownProps) {\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 [query, setQuery] = useState(\"\");\n const [open, setOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState(-1);\n const containerRef = useRef<HTMLDivElement>(null);\n\n // Reset input when country or province changes\n const prevCountry = useRef(country);\n const prevProvince = useRef(province);\n useEffect(() => {\n if (prevCountry.current !== country || prevProvince.current !== province) {\n setQuery(\"\");\n setOpen(false);\n prevCountry.current = country;\n prevProvince.current = province;\n }\n }, [country, province]);\n\n const { results, loading } = useAutocomplete(client, country, query, {\n limit,\n });\n\n const filtered = province\n ? results.filter(\n (r) => r.type === \"city\" && r.province_code === province\n )\n : results;\n\n useEffect(() => {\n const handler = (e: MouseEvent) => {\n if (\n containerRef.current &&\n !containerRef.current.contains(e.target as Node)\n ) {\n setOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", handler);\n return () => document.removeEventListener(\"mousedown\", handler);\n }, []);\n\n useEffect(() => {\n setActiveIndex(-1);\n }, [filtered.length]);\n\n const handleSelect = useCallback(\n async (result: AutocompleteResult) => {\n setQuery(result.name);\n setOpen(false);\n\n const detail = await client.city(result.country_code, result.code);\n onSelect(detail);\n },\n [client, onSelect]\n );\n\n const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n if (!open || filtered.length === 0) return;\n\n switch (e.key) {\n case \"ArrowDown\":\n e.preventDefault();\n setActiveIndex((prev) =>\n prev < filtered.length - 1 ? prev + 1 : 0\n );\n break;\n case \"ArrowUp\":\n e.preventDefault();\n setActiveIndex((prev) =>\n prev > 0 ? prev - 1 : filtered.length - 1\n );\n break;\n case \"Enter\":\n e.preventDefault();\n if (activeIndex >= 0 && activeIndex < filtered.length) {\n handleSelect(filtered[activeIndex]);\n }\n break;\n case \"Escape\":\n setOpen(false);\n break;\n }\n };\n\n const t = theme ?? {};\n const borderColor = t.borderColor ?? \"#d1d5db\";\n const borderRadius = t.borderRadius ?? \"6px\";\n const background = t.background ?? \"#fff\";\n const textColor = t.textColor ?? \"inherit\";\n const secondaryTextColor = t.secondaryTextColor ?? \"#6b7280\";\n const activeBackground = t.activeBackground ?? \"#f3f4f6\";\n const fontSize = t.fontSize ?? \"14px\";\n\n const s: Record<string, CSSProperties> = {\n container: { position: \"relative\", width: \"100%\" },\n input: {\n width: \"100%\",\n padding: \"8px 12px\",\n fontSize,\n color: textColor,\n background,\n border: `1px solid ${borderColor}`,\n borderRadius,\n outline: \"none\",\n boxSizing: \"border-box\",\n },\n list: {\n position: \"absolute\",\n top: \"100%\",\n left: 0,\n right: 0,\n margin: \"4px 0 0\",\n padding: 0,\n listStyle: \"none\",\n background,\n border: `1px solid ${borderColor}`,\n borderRadius,\n boxShadow: \"0 4px 6px -1px rgba(0,0,0,.1)\",\n maxHeight: \"240px\",\n overflowY: \"auto\",\n zIndex: 50,\n },\n item: {\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n padding: \"8px 12px\",\n cursor: \"pointer\",\n fontSize,\n color: textColor,\n },\n itemActive: { background: activeBackground },\n name: { fontWeight: 500 },\n province: { fontSize: \"12px\", color: secondaryTextColor },\n status: {\n position: \"absolute\",\n top: \"100%\",\n left: 0,\n right: 0,\n margin: \"4px 0 0\",\n padding: \"8px 12px\",\n background,\n border: `1px solid ${borderColor}`,\n borderRadius,\n fontSize,\n color: secondaryTextColor,\n },\n };\n\n return (\n <div ref={containerRef} style={s.container} className={className}>\n <input\n type=\"text\"\n value={query}\n onChange={(e) => {\n setQuery(e.target.value);\n setOpen(true);\n }}\n onFocus={() => query.trim() && setOpen(true)}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n style={s.input}\n role=\"combobox\"\n aria-expanded={open}\n aria-autocomplete=\"list\"\n aria-activedescendant={\n activeIndex >= 0 ? `pl-option-${activeIndex}` : undefined\n }\n />\n {open && filtered.length > 0 && (\n <ul role=\"listbox\" style={s.list}>\n {filtered.map((result, i) => (\n <li\n key={result.code}\n id={`pl-option-${i}`}\n role=\"option\"\n aria-selected={i === activeIndex}\n style={{\n ...s.item,\n ...(i === activeIndex ? s.itemActive : {}),\n }}\n onMouseEnter={() => setActiveIndex(i)}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => handleSelect(result)}\n >\n <span style={s.name}>{result.name}</span>\n {result.province_name && (\n <span style={s.province}>{result.province_name}</span>\n )}\n </li>\n ))}\n </ul>\n )}\n {open && loading && filtered.length === 0 && (\n <div style={s.status}>Loading...</div>\n )}\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { AutocompleteResult, PlacesLayerClient } from \"@placeslayer/sdk\";\n\nconst DEBOUNCE_MS = 300;\n\nexport interface AutocompleteState {\n results: AutocompleteResult[];\n loading: boolean;\n error: Error | null;\n}\n\nexport function useAutocomplete(\n client: PlacesLayerClient,\n countryCode: string,\n query: string,\n opts?: { limit?: number; debounceMs?: number }\n): AutocompleteState {\n const [results, setResults] = useState<AutocompleteResult[]>([]);\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 .autocomplete(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","import { useRef } from \"react\";\nimport { PlacesLayerClient, type PlacesLayerConfig } from \"@placeslayer/sdk\";\n\nexport function usePlacesLayerClient(config: PlacesLayerConfig): PlacesLayerClient {\n const ref = useRef<PlacesLayerClient | null>(null);\n\n if (!ref.current || (ref.current as unknown as { apiKey: string }).apiKey !== config.apiKey) {\n ref.current = new PlacesLayerClient(config);\n }\n\n return ref.current;\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 { Country, PlacesLayerClient } from \"@placeslayer/sdk\";\nimport { type AsyncState, useAsync } from \"./useAsync\";\n\nexport function useCountries(client: PlacesLayerClient): AsyncState<Country[]> {\n return useAsync(() => client.countries(), [client]);\n}\n","import type { PlacesLayerClient, Province } from \"@placeslayer/sdk\";\nimport { type AsyncState, useAsync } from \"./useAsync\";\n\nexport function useProvinces(\n client: PlacesLayerClient,\n countryCode: string,\n opts?: { native?: boolean }\n): AsyncState<Province[]> {\n return useAsync(\n () => client.provinces(countryCode, opts),\n [client, countryCode, opts?.native]\n );\n}\n","import type { City, PlacesLayerClient } from \"@placeslayer/sdk\";\nimport { type AsyncState, useAsync } from \"./useAsync\";\n\nexport function useCities(\n client: PlacesLayerClient,\n countryCode: string,\n provinceCode: string,\n opts?: { native?: boolean }\n): AsyncState<City[]> {\n return useAsync(\n () => client.cities(countryCode, provinceCode, opts),\n [client, countryCode, provinceCode, opts?.native]\n );\n}\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useState","useRef","useState","useEffect","useCallback","useRef","PlacesLayerClient","useCallback","useEffect","useRef","useState"]}
|
package/dist/map.cjs
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/map/index.ts
|
|
31
|
+
var map_exports = {};
|
|
32
|
+
__export(map_exports, {
|
|
33
|
+
PlacesMap: () => PlacesMap,
|
|
34
|
+
useMapInsights: () => useMapInsights,
|
|
35
|
+
useSemanticSearch: () => useSemanticSearch
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(map_exports);
|
|
38
|
+
|
|
39
|
+
// src/map/PlacesMap.tsx
|
|
40
|
+
var import_react3 = require("react");
|
|
41
|
+
var import_react_leaflet = require("react-leaflet");
|
|
42
|
+
var import_leaflet = __toESM(require("leaflet"), 1);
|
|
43
|
+
var import_sdk = require("@placeslayer/sdk");
|
|
44
|
+
|
|
45
|
+
// src/hooks/useAsync.ts
|
|
46
|
+
var import_react = require("react");
|
|
47
|
+
function useAsync(fn, deps) {
|
|
48
|
+
const [data, setData] = (0, import_react.useState)(null);
|
|
49
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
50
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
51
|
+
const version = (0, import_react.useRef)(0);
|
|
52
|
+
const execute = (0, import_react.useCallback)(() => {
|
|
53
|
+
const current = ++version.current;
|
|
54
|
+
setLoading(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
fn().then((result) => {
|
|
57
|
+
if (current === version.current) {
|
|
58
|
+
setData(result);
|
|
59
|
+
setLoading(false);
|
|
60
|
+
}
|
|
61
|
+
}).catch((err) => {
|
|
62
|
+
if (current === version.current) {
|
|
63
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
64
|
+
setLoading(false);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}, deps);
|
|
68
|
+
(0, import_react.useEffect)(() => {
|
|
69
|
+
execute();
|
|
70
|
+
}, [execute]);
|
|
71
|
+
return { data, loading, error, refetch: execute };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/map/hooks/useMapInsights.ts
|
|
75
|
+
function useMapInsights(client, countryCode) {
|
|
76
|
+
return useAsync(
|
|
77
|
+
() => client.mapInsights(countryCode),
|
|
78
|
+
[client, countryCode]
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/map/hooks/useSemanticSearch.ts
|
|
83
|
+
var import_react2 = require("react");
|
|
84
|
+
var DEBOUNCE_MS = 500;
|
|
85
|
+
function useSemanticSearch(client, countryCode, query, opts) {
|
|
86
|
+
const [results, setResults] = (0, import_react2.useState)([]);
|
|
87
|
+
const [loading, setLoading] = (0, import_react2.useState)(false);
|
|
88
|
+
const [error, setError] = (0, import_react2.useState)(null);
|
|
89
|
+
const version = (0, import_react2.useRef)(0);
|
|
90
|
+
const timerRef = (0, import_react2.useRef)(void 0);
|
|
91
|
+
const delay = opts?.debounceMs ?? DEBOUNCE_MS;
|
|
92
|
+
const search = (0, import_react2.useCallback)(() => {
|
|
93
|
+
const trimmed = query.trim();
|
|
94
|
+
if (!trimmed) {
|
|
95
|
+
setResults([]);
|
|
96
|
+
setLoading(false);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const current = ++version.current;
|
|
100
|
+
setLoading(true);
|
|
101
|
+
client.semanticSearch(countryCode, trimmed, { limit: opts?.limit }).then((data) => {
|
|
102
|
+
if (current === version.current) {
|
|
103
|
+
setResults(data);
|
|
104
|
+
setLoading(false);
|
|
105
|
+
}
|
|
106
|
+
}).catch((err) => {
|
|
107
|
+
if (current === version.current) {
|
|
108
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
109
|
+
setLoading(false);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}, [client, countryCode, query, opts?.limit]);
|
|
113
|
+
(0, import_react2.useEffect)(() => {
|
|
114
|
+
clearTimeout(timerRef.current);
|
|
115
|
+
timerRef.current = setTimeout(search, delay);
|
|
116
|
+
return () => clearTimeout(timerRef.current);
|
|
117
|
+
}, [search, delay]);
|
|
118
|
+
return { results, loading, error };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/map/PlacesMap.tsx
|
|
122
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
123
|
+
var DEFAULT_ICON = new import_leaflet.default.Icon({
|
|
124
|
+
iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
|
|
125
|
+
iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png",
|
|
126
|
+
shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
|
|
127
|
+
iconSize: [25, 41],
|
|
128
|
+
iconAnchor: [12, 41],
|
|
129
|
+
popupAnchor: [1, -34],
|
|
130
|
+
shadowSize: [41, 41]
|
|
131
|
+
});
|
|
132
|
+
var HIGHLIGHT_ICON = new import_leaflet.default.Icon({
|
|
133
|
+
iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
|
|
134
|
+
iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png",
|
|
135
|
+
shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
|
|
136
|
+
iconSize: [30, 49],
|
|
137
|
+
iconAnchor: [15, 49],
|
|
138
|
+
popupAnchor: [1, -40],
|
|
139
|
+
shadowSize: [49, 49],
|
|
140
|
+
className: "pl-marker-highlight"
|
|
141
|
+
});
|
|
142
|
+
function FitBounds({ insights }) {
|
|
143
|
+
const map = (0, import_react_leaflet.useMap)();
|
|
144
|
+
const fitted = (0, import_react3.useRef)(false);
|
|
145
|
+
if (!fitted.current && insights.length > 0) {
|
|
146
|
+
const bounds = import_leaflet.default.latLngBounds(
|
|
147
|
+
insights.filter((i) => i.latitude && i.longitude).map((i) => [i.latitude, i.longitude])
|
|
148
|
+
);
|
|
149
|
+
if (bounds.isValid()) {
|
|
150
|
+
map.fitBounds(bounds, { padding: [40, 40] });
|
|
151
|
+
fitted.current = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function FitSearchResults({ results }) {
|
|
157
|
+
const map = (0, import_react_leaflet.useMap)();
|
|
158
|
+
if (results.length > 0) {
|
|
159
|
+
const bounds = import_leaflet.default.latLngBounds(
|
|
160
|
+
results.filter((i) => i.latitude && i.longitude).map((i) => [i.latitude, i.longitude])
|
|
161
|
+
);
|
|
162
|
+
if (bounds.isValid()) {
|
|
163
|
+
map.fitBounds(bounds, { padding: [40, 40] });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
function PlacesMap({
|
|
169
|
+
apiKey,
|
|
170
|
+
country,
|
|
171
|
+
center = [0, 0],
|
|
172
|
+
zoom = 5,
|
|
173
|
+
onCitySelect,
|
|
174
|
+
searchEnabled = false,
|
|
175
|
+
baseUrl,
|
|
176
|
+
className,
|
|
177
|
+
style
|
|
178
|
+
}) {
|
|
179
|
+
const clientRef = (0, import_react3.useRef)(null);
|
|
180
|
+
if (!clientRef.current || clientRef.current.apiKey !== apiKey) {
|
|
181
|
+
clientRef.current = new import_sdk.PlacesLayerClient({ apiKey, baseUrl });
|
|
182
|
+
}
|
|
183
|
+
const client = clientRef.current;
|
|
184
|
+
const [searchQuery, setSearchQuery] = (0, import_react3.useState)("");
|
|
185
|
+
const { data: allInsights } = useMapInsights(client, country);
|
|
186
|
+
const { results: searchResults } = useSemanticSearch(
|
|
187
|
+
client,
|
|
188
|
+
country,
|
|
189
|
+
searchEnabled ? searchQuery : ""
|
|
190
|
+
);
|
|
191
|
+
const highlightedCodes = (0, import_react3.useMemo)(
|
|
192
|
+
() => new Set(searchResults.map((r) => r.city_code)),
|
|
193
|
+
[searchResults]
|
|
194
|
+
);
|
|
195
|
+
const hasSearch = searchEnabled && searchQuery.trim() && searchResults.length > 0;
|
|
196
|
+
const insights = allInsights ?? [];
|
|
197
|
+
const handleSelect = (0, import_react3.useCallback)(
|
|
198
|
+
(insight) => {
|
|
199
|
+
onCitySelect?.(insight);
|
|
200
|
+
},
|
|
201
|
+
[onCitySelect]
|
|
202
|
+
);
|
|
203
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, style: { position: "relative", ...style }, children: [
|
|
204
|
+
searchEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.searchContainer, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
205
|
+
"input",
|
|
206
|
+
{
|
|
207
|
+
type: "text",
|
|
208
|
+
value: searchQuery,
|
|
209
|
+
onChange: (e) => setSearchQuery(e.target.value),
|
|
210
|
+
placeholder: "Search cities by description...",
|
|
211
|
+
style: styles.searchInput
|
|
212
|
+
}
|
|
213
|
+
) }),
|
|
214
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
215
|
+
import_react_leaflet.MapContainer,
|
|
216
|
+
{
|
|
217
|
+
center,
|
|
218
|
+
zoom,
|
|
219
|
+
style: { width: "100%", height: "100%", minHeight: 400 },
|
|
220
|
+
scrollWheelZoom: true,
|
|
221
|
+
children: [
|
|
222
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
223
|
+
import_react_leaflet.TileLayer,
|
|
224
|
+
{
|
|
225
|
+
attribution: '\xA9 <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
|
226
|
+
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
227
|
+
}
|
|
228
|
+
),
|
|
229
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FitBounds, { insights }),
|
|
230
|
+
hasSearch && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FitSearchResults, { results: searchResults }),
|
|
231
|
+
insights.filter((i) => i.latitude && i.longitude).map((insight) => {
|
|
232
|
+
const isHighlighted = hasSearch && highlightedCodes.has(insight.city_code);
|
|
233
|
+
const isDimmed = hasSearch && !isHighlighted;
|
|
234
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
235
|
+
import_react_leaflet.Marker,
|
|
236
|
+
{
|
|
237
|
+
position: [insight.latitude, insight.longitude],
|
|
238
|
+
icon: isHighlighted ? HIGHLIGHT_ICON : DEFAULT_ICON,
|
|
239
|
+
opacity: isDimmed ? 0.3 : 1,
|
|
240
|
+
eventHandlers: { click: () => handleSelect(insight) },
|
|
241
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_leaflet.Popup, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.popup, children: [
|
|
242
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: insight.city_name }),
|
|
243
|
+
insight.vibe && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.vibe, children: insight.vibe }),
|
|
244
|
+
insight.ideal_for?.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.tags, children: insight.ideal_for.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.tag, children: tag }, tag)) }),
|
|
245
|
+
insight.cost_of_living && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.meta, children: [
|
|
246
|
+
"Cost: ",
|
|
247
|
+
insight.cost_of_living.replace("_", " ")
|
|
248
|
+
] }),
|
|
249
|
+
insight.climate_zone && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.meta, children: [
|
|
250
|
+
"Climate: ",
|
|
251
|
+
insight.climate_zone
|
|
252
|
+
] }),
|
|
253
|
+
insight.summary && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.summary, children: insight.summary })
|
|
254
|
+
] }) })
|
|
255
|
+
},
|
|
256
|
+
insight.city_code
|
|
257
|
+
);
|
|
258
|
+
})
|
|
259
|
+
]
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
] });
|
|
263
|
+
}
|
|
264
|
+
var styles = {
|
|
265
|
+
searchContainer: {
|
|
266
|
+
position: "absolute",
|
|
267
|
+
top: 10,
|
|
268
|
+
left: 50,
|
|
269
|
+
right: 50,
|
|
270
|
+
zIndex: 1e3
|
|
271
|
+
},
|
|
272
|
+
searchInput: {
|
|
273
|
+
width: "100%",
|
|
274
|
+
padding: "10px 14px",
|
|
275
|
+
fontSize: "14px",
|
|
276
|
+
border: "2px solid #d1d5db",
|
|
277
|
+
borderRadius: "8px",
|
|
278
|
+
background: "#fff",
|
|
279
|
+
boxShadow: "0 2px 8px rgba(0,0,0,.15)",
|
|
280
|
+
outline: "none",
|
|
281
|
+
boxSizing: "border-box"
|
|
282
|
+
},
|
|
283
|
+
popup: {
|
|
284
|
+
maxWidth: 250,
|
|
285
|
+
fontSize: "13px",
|
|
286
|
+
lineHeight: 1.4
|
|
287
|
+
},
|
|
288
|
+
vibe: {
|
|
289
|
+
color: "#6b7280",
|
|
290
|
+
fontStyle: "italic",
|
|
291
|
+
marginTop: 4
|
|
292
|
+
},
|
|
293
|
+
tags: {
|
|
294
|
+
display: "flex",
|
|
295
|
+
flexWrap: "wrap",
|
|
296
|
+
gap: 4,
|
|
297
|
+
marginTop: 6
|
|
298
|
+
},
|
|
299
|
+
tag: {
|
|
300
|
+
background: "#e5e7eb",
|
|
301
|
+
borderRadius: 4,
|
|
302
|
+
padding: "2px 6px",
|
|
303
|
+
fontSize: "11px"
|
|
304
|
+
},
|
|
305
|
+
meta: {
|
|
306
|
+
color: "#6b7280",
|
|
307
|
+
fontSize: "12px",
|
|
308
|
+
marginTop: 4
|
|
309
|
+
},
|
|
310
|
+
summary: {
|
|
311
|
+
marginTop: 8,
|
|
312
|
+
fontSize: "12px",
|
|
313
|
+
color: "#374151"
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
317
|
+
0 && (module.exports = {
|
|
318
|
+
PlacesMap,
|
|
319
|
+
useMapInsights,
|
|
320
|
+
useSemanticSearch
|
|
321
|
+
});
|
|
322
|
+
//# sourceMappingURL=map.cjs.map
|