@ttoss/google-maps 2.1.23 → 2.2.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/esm/index.js +360 -0
- package/dist/index.d.ts +96 -0
- package/package.json +6 -6
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __name = (target, value) => __defProp(target, "name", {
|
|
4
|
+
value,
|
|
5
|
+
configurable: true
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/GoogleMapsProvider.tsx
|
|
9
|
+
import { useScript } from "@ttoss/react-hooks";
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
var GoogleMapsContext = /* @__PURE__ */React.createContext({
|
|
12
|
+
status: "idle",
|
|
13
|
+
google: {
|
|
14
|
+
maps: null
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
var DefaultScript = /* @__PURE__ */__name(({
|
|
18
|
+
src,
|
|
19
|
+
onReady
|
|
20
|
+
}) => {
|
|
21
|
+
const {
|
|
22
|
+
status
|
|
23
|
+
} = useScript(src);
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
if (status === "ready") {
|
|
26
|
+
onReady();
|
|
27
|
+
}
|
|
28
|
+
}, [status, onReady]);
|
|
29
|
+
return null;
|
|
30
|
+
}, "DefaultScript");
|
|
31
|
+
var GoogleMapsProvider = /* @__PURE__ */__name(({
|
|
32
|
+
children,
|
|
33
|
+
apiKey,
|
|
34
|
+
loading = "async",
|
|
35
|
+
libraries,
|
|
36
|
+
language,
|
|
37
|
+
Script = DefaultScript,
|
|
38
|
+
onError
|
|
39
|
+
}) => {
|
|
40
|
+
const src = (() => {
|
|
41
|
+
let srcTemp = `https://maps.googleapis.com/maps/api/js?key=${apiKey}`;
|
|
42
|
+
if (loading) {
|
|
43
|
+
srcTemp = srcTemp + `&loading=${loading}`;
|
|
44
|
+
}
|
|
45
|
+
if (libraries) {
|
|
46
|
+
srcTemp = srcTemp + `&libraries=${libraries.join(",")}`;
|
|
47
|
+
}
|
|
48
|
+
if (language) {
|
|
49
|
+
srcTemp = srcTemp + `&language=${language}`;
|
|
50
|
+
}
|
|
51
|
+
return srcTemp;
|
|
52
|
+
})();
|
|
53
|
+
const [status, setStatus] = React.useState("loading");
|
|
54
|
+
const google = React.useMemo(() => {
|
|
55
|
+
if (status === "ready" && window.google) {
|
|
56
|
+
return window.google;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}, [status]);
|
|
60
|
+
const value = React.useMemo(() => {
|
|
61
|
+
if (status === "ready" && google?.maps) {
|
|
62
|
+
return {
|
|
63
|
+
status,
|
|
64
|
+
google: {
|
|
65
|
+
maps: google.maps
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
status,
|
|
71
|
+
google: {
|
|
72
|
+
maps: null
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}, [google, status]);
|
|
76
|
+
return /* @__PURE__ */React.createElement(React.Fragment, null, /* @__PURE__ */React.createElement(Script, {
|
|
77
|
+
src,
|
|
78
|
+
onReady: /* @__PURE__ */__name(() => {
|
|
79
|
+
return setStatus("ready");
|
|
80
|
+
}, "onReady"),
|
|
81
|
+
onError
|
|
82
|
+
}), /* @__PURE__ */React.createElement(GoogleMapsContext.Provider, {
|
|
83
|
+
value
|
|
84
|
+
}, children));
|
|
85
|
+
}, "GoogleMapsProvider");
|
|
86
|
+
|
|
87
|
+
// src/MapProvider.tsx
|
|
88
|
+
import * as React2 from "react";
|
|
89
|
+
var MapContext = /* @__PURE__ */React2.createContext({
|
|
90
|
+
map: null,
|
|
91
|
+
ref: {
|
|
92
|
+
current: null
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
var MapProvider = /* @__PURE__ */React2.forwardRef(({
|
|
96
|
+
children,
|
|
97
|
+
map
|
|
98
|
+
}, ref) => {
|
|
99
|
+
return /* @__PURE__ */React2.createElement(MapContext.Provider, {
|
|
100
|
+
value: {
|
|
101
|
+
map,
|
|
102
|
+
ref
|
|
103
|
+
}
|
|
104
|
+
}, children);
|
|
105
|
+
});
|
|
106
|
+
MapProvider.displayName = "MapProvider";
|
|
107
|
+
|
|
108
|
+
// src/useGeocoder.ts
|
|
109
|
+
import * as React4 from "react";
|
|
110
|
+
|
|
111
|
+
// src/useGoogleMaps.ts
|
|
112
|
+
import * as React3 from "react";
|
|
113
|
+
var useGoogleMaps = /* @__PURE__ */__name(() => {
|
|
114
|
+
const {
|
|
115
|
+
status,
|
|
116
|
+
google
|
|
117
|
+
} = React3.useContext(GoogleMapsContext);
|
|
118
|
+
const isReady = status === "ready";
|
|
119
|
+
return {
|
|
120
|
+
isReady,
|
|
121
|
+
status,
|
|
122
|
+
google
|
|
123
|
+
};
|
|
124
|
+
}, "useGoogleMaps");
|
|
125
|
+
|
|
126
|
+
// src/useGeocoder.ts
|
|
127
|
+
var useGeocoder = /* @__PURE__ */__name(() => {
|
|
128
|
+
const {
|
|
129
|
+
google
|
|
130
|
+
} = useGoogleMaps();
|
|
131
|
+
const [isGeocoderInitialized, setIsGeocoderInitialized] = React4.useState(false);
|
|
132
|
+
const geocoder = React4.useMemo(() => {
|
|
133
|
+
if (google.maps) {
|
|
134
|
+
const googleMapsGeocoder = new google.maps.Geocoder();
|
|
135
|
+
setIsGeocoderInitialized(true);
|
|
136
|
+
return googleMapsGeocoder;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}, [google.maps]);
|
|
140
|
+
return {
|
|
141
|
+
geocoder,
|
|
142
|
+
isGeocoderInitialized
|
|
143
|
+
};
|
|
144
|
+
}, "useGeocoder");
|
|
145
|
+
|
|
146
|
+
// src/useMap.ts
|
|
147
|
+
import * as React5 from "react";
|
|
148
|
+
import { useCallbackRef } from "use-callback-ref";
|
|
149
|
+
var useMap = /* @__PURE__ */__name((options = {}) => {
|
|
150
|
+
const [, forceUpdate] = React5.useState(0);
|
|
151
|
+
const ref = useCallbackRef(null, () => {
|
|
152
|
+
return forceUpdate(n => {
|
|
153
|
+
return n + 1;
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
const {
|
|
157
|
+
google,
|
|
158
|
+
isReady
|
|
159
|
+
} = useGoogleMaps();
|
|
160
|
+
const mapContext = React5.useContext(MapContext);
|
|
161
|
+
const [map, setMap] = React5.useState(mapContext.map);
|
|
162
|
+
React5.useEffect(() => {
|
|
163
|
+
if (map) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!ref.current) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!isReady) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!google.maps?.Map) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
setMap(new google.maps.Map(ref.current, options));
|
|
176
|
+
}, [map, isReady, ref, google.maps?.Map, options]);
|
|
177
|
+
const optionsStringify = JSON.stringify(options);
|
|
178
|
+
React5.useEffect(() => {
|
|
179
|
+
if (map) {
|
|
180
|
+
const parsedOptions = JSON.parse(optionsStringify);
|
|
181
|
+
map.setOptions(parsedOptions);
|
|
182
|
+
}
|
|
183
|
+
}, [optionsStringify, map]);
|
|
184
|
+
return {
|
|
185
|
+
/**
|
|
186
|
+
* Returns the map object which provides access to the [Google Maps API](https://developers.google.com/maps/documentation/javascript/overview).
|
|
187
|
+
*/
|
|
188
|
+
map,
|
|
189
|
+
/**
|
|
190
|
+
* Returns the ref object which provides access to the HTMLDivElement element
|
|
191
|
+
* that the map is rendered in.
|
|
192
|
+
*/
|
|
193
|
+
ref
|
|
194
|
+
};
|
|
195
|
+
}, "useMap");
|
|
196
|
+
|
|
197
|
+
// src/usePlacesAutocomplete/index.ts
|
|
198
|
+
import * as React7 from "react";
|
|
199
|
+
|
|
200
|
+
// src/usePlacesAutocomplete/useLatest.ts
|
|
201
|
+
import * as React6 from "react";
|
|
202
|
+
var useLatest = /* @__PURE__ */__name(val => {
|
|
203
|
+
const ref = React6.useRef(val);
|
|
204
|
+
ref.current = val;
|
|
205
|
+
return ref;
|
|
206
|
+
}, "useLatest");
|
|
207
|
+
|
|
208
|
+
// src/usePlacesAutocomplete/debounce.ts
|
|
209
|
+
var debounce = /* @__PURE__ */__name((fn, delay) => {
|
|
210
|
+
let timer;
|
|
211
|
+
function debounceFn(...args) {
|
|
212
|
+
if (timer !== null) {
|
|
213
|
+
clearTimeout(timer);
|
|
214
|
+
timer = null;
|
|
215
|
+
}
|
|
216
|
+
timer = setTimeout(() => fn.apply(this, args), delay);
|
|
217
|
+
}
|
|
218
|
+
__name(debounceFn, "debounceFn");
|
|
219
|
+
return debounceFn;
|
|
220
|
+
}, "debounce");
|
|
221
|
+
var debounce_default = debounce;
|
|
222
|
+
|
|
223
|
+
// src/usePlacesAutocomplete/index.ts
|
|
224
|
+
var usePlacesAutocomplete = /* @__PURE__ */__name(({
|
|
225
|
+
requestOptions,
|
|
226
|
+
debounce: debounce2 = 200,
|
|
227
|
+
cache = 24 * 60 * 60,
|
|
228
|
+
cacheKey,
|
|
229
|
+
callbackName,
|
|
230
|
+
defaultValue = "",
|
|
231
|
+
initOnMount = true
|
|
232
|
+
} = {}) => {
|
|
233
|
+
const [ready, setReady] = React7.useState(false);
|
|
234
|
+
const [value, setVal] = React7.useState(defaultValue);
|
|
235
|
+
const [suggestions, setSuggestions] = React7.useState({
|
|
236
|
+
loading: false,
|
|
237
|
+
status: "",
|
|
238
|
+
data: []
|
|
239
|
+
});
|
|
240
|
+
const asRef = React7.useRef(null);
|
|
241
|
+
const requestOptionsRef = useLatest(requestOptions);
|
|
242
|
+
const {
|
|
243
|
+
google
|
|
244
|
+
} = useGoogleMaps();
|
|
245
|
+
const googleMapsRef = useLatest(google.maps);
|
|
246
|
+
const upaCacheKey = cacheKey ? `upa-${cacheKey}` : "upa";
|
|
247
|
+
const init = React7.useCallback(() => {
|
|
248
|
+
if (asRef.current) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!google.maps) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const {
|
|
255
|
+
current: gMaps
|
|
256
|
+
} = googleMapsRef;
|
|
257
|
+
const placesLib = gMaps?.places || google.maps.places;
|
|
258
|
+
if (!placesLib) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
asRef.current = new placesLib.AutocompleteService();
|
|
262
|
+
setReady(true);
|
|
263
|
+
}, [google.maps]);
|
|
264
|
+
const clearSuggestions = React7.useCallback(() => {
|
|
265
|
+
setSuggestions({
|
|
266
|
+
loading: false,
|
|
267
|
+
status: "",
|
|
268
|
+
data: []
|
|
269
|
+
});
|
|
270
|
+
}, []);
|
|
271
|
+
const clearCache = React7.useCallback(() => {
|
|
272
|
+
try {
|
|
273
|
+
sessionStorage.removeItem(upaCacheKey);
|
|
274
|
+
} catch (error) {}
|
|
275
|
+
}, []);
|
|
276
|
+
const fetchPredictions = React7.useCallback(debounce_default(val => {
|
|
277
|
+
if (!val) {
|
|
278
|
+
clearSuggestions();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
setSuggestions(prevState => {
|
|
282
|
+
return {
|
|
283
|
+
...prevState,
|
|
284
|
+
loading: true
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
let cachedData = {};
|
|
288
|
+
try {
|
|
289
|
+
cachedData = JSON.parse(sessionStorage.getItem(upaCacheKey) || "{}");
|
|
290
|
+
} catch (error) {}
|
|
291
|
+
if (cache) {
|
|
292
|
+
cachedData = Object.keys(cachedData).reduce((acc, key) => {
|
|
293
|
+
if (cachedData[key].maxAge - Date.now() >= 0) {
|
|
294
|
+
acc[key] = cachedData[key];
|
|
295
|
+
}
|
|
296
|
+
return acc;
|
|
297
|
+
}, {});
|
|
298
|
+
if (cachedData[val]) {
|
|
299
|
+
setSuggestions({
|
|
300
|
+
loading: false,
|
|
301
|
+
status: "OK",
|
|
302
|
+
data: cachedData[val].data
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
asRef?.current?.getPlacePredictions({
|
|
308
|
+
...requestOptionsRef.current,
|
|
309
|
+
input: val
|
|
310
|
+
}, (data, status) => {
|
|
311
|
+
setSuggestions({
|
|
312
|
+
loading: false,
|
|
313
|
+
status,
|
|
314
|
+
data: data || []
|
|
315
|
+
});
|
|
316
|
+
if (cache && status === "OK") {
|
|
317
|
+
cachedData[val] = {
|
|
318
|
+
data,
|
|
319
|
+
maxAge: Date.now() + cache * 1e3
|
|
320
|
+
};
|
|
321
|
+
try {
|
|
322
|
+
sessionStorage.setItem(upaCacheKey, JSON.stringify(cachedData));
|
|
323
|
+
} catch (error) {}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}, debounce2), [debounce2, clearSuggestions]);
|
|
327
|
+
const setValue = React7.useCallback((val, shouldFetchData = true) => {
|
|
328
|
+
setVal(val);
|
|
329
|
+
if (asRef.current && shouldFetchData) {
|
|
330
|
+
fetchPredictions(val);
|
|
331
|
+
}
|
|
332
|
+
}, [fetchPredictions]);
|
|
333
|
+
React7.useEffect(() => {
|
|
334
|
+
if (!initOnMount) {
|
|
335
|
+
return () => {
|
|
336
|
+
return null;
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (!googleMapsRef.current && !google.maps && callbackName) {
|
|
340
|
+
window[callbackName] = init;
|
|
341
|
+
} else {
|
|
342
|
+
init();
|
|
343
|
+
}
|
|
344
|
+
return () => {
|
|
345
|
+
if (window[callbackName]) {
|
|
346
|
+
delete window[callbackName];
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}, [callbackName, init]);
|
|
350
|
+
return {
|
|
351
|
+
ready,
|
|
352
|
+
value,
|
|
353
|
+
suggestions,
|
|
354
|
+
setValue,
|
|
355
|
+
clearSuggestions,
|
|
356
|
+
clearCache,
|
|
357
|
+
init
|
|
358
|
+
};
|
|
359
|
+
}, "usePlacesAutocomplete");
|
|
360
|
+
export { GoogleMapsProvider, MapProvider, useGeocoder, useGoogleMaps, useMap, usePlacesAutocomplete };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
type GoogleMaps = typeof google.maps;
|
|
5
|
+
/**
|
|
6
|
+
* The libraries that can be loaded from the Google Maps JavaScript API.
|
|
7
|
+
* https://developers.google.com/maps/documentation/javascript/libraries?hl=pt-br#available-libraries
|
|
8
|
+
*/
|
|
9
|
+
type Libraries = 'core' | 'maps' | 'maps3d' | 'places' | 'geocoding' | 'routes' | 'marker' | 'geometry' | 'elevation' | 'streetView' | 'journeySharing' | 'visualization' | 'airQuality' | 'addressValidation';
|
|
10
|
+
type ScriptProps = {
|
|
11
|
+
src: string;
|
|
12
|
+
onReady: () => void;
|
|
13
|
+
onError?: (e: Error) => void;
|
|
14
|
+
};
|
|
15
|
+
declare const GoogleMapsProvider: ({ children, apiKey, loading, libraries, language, Script, onError, }: {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
apiKey: string;
|
|
18
|
+
libraries?: Libraries[];
|
|
19
|
+
/**
|
|
20
|
+
* https://developers.google.com/maps/faq#languagesupport
|
|
21
|
+
*/
|
|
22
|
+
language?: string;
|
|
23
|
+
loading?: "async" | false;
|
|
24
|
+
Script?: React.ComponentType<ScriptProps>;
|
|
25
|
+
onError?: (e: Error) => void;
|
|
26
|
+
}) => react_jsx_runtime.JSX.Element;
|
|
27
|
+
|
|
28
|
+
declare const MapProvider: React.ForwardRefExoticComponent<{
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
map: google.maps.Map | null;
|
|
31
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
32
|
+
|
|
33
|
+
declare const useGeocoder: () => {
|
|
34
|
+
geocoder: google.maps.Geocoder | null;
|
|
35
|
+
isGeocoderInitialized: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
declare const useMap: (options?: google.maps.MapOptions) => {
|
|
39
|
+
/**
|
|
40
|
+
* Returns the map object which provides access to the [Google Maps API](https://developers.google.com/maps/documentation/javascript/overview).
|
|
41
|
+
*/
|
|
42
|
+
map: google.maps.Map | null;
|
|
43
|
+
/**
|
|
44
|
+
* Returns the ref object which provides access to the HTMLDivElement element
|
|
45
|
+
* that the map is rendered in.
|
|
46
|
+
*/
|
|
47
|
+
ref: React.MutableRefObject<HTMLDivElement | null>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
interface HookArgs {
|
|
51
|
+
requestOptions?: Omit<google.maps.places.AutocompletionRequest, 'input'>;
|
|
52
|
+
debounce?: number;
|
|
53
|
+
cache?: number | false;
|
|
54
|
+
cacheKey?: string;
|
|
55
|
+
callbackName?: string;
|
|
56
|
+
defaultValue?: string;
|
|
57
|
+
initOnMount?: boolean;
|
|
58
|
+
}
|
|
59
|
+
type Suggestion = google.maps.places.AutocompletePrediction;
|
|
60
|
+
type Status = `${google.maps.places.PlacesServiceStatus}` | '';
|
|
61
|
+
interface Suggestions {
|
|
62
|
+
readonly loading: boolean;
|
|
63
|
+
readonly status: Status;
|
|
64
|
+
data: Suggestion[];
|
|
65
|
+
}
|
|
66
|
+
interface SetValue {
|
|
67
|
+
(val: string, shouldFetchData?: boolean): void;
|
|
68
|
+
}
|
|
69
|
+
interface HookReturn {
|
|
70
|
+
ready: boolean;
|
|
71
|
+
value: string;
|
|
72
|
+
suggestions: Suggestions;
|
|
73
|
+
setValue: SetValue;
|
|
74
|
+
clearSuggestions: () => void;
|
|
75
|
+
clearCache: () => void;
|
|
76
|
+
init: () => void;
|
|
77
|
+
}
|
|
78
|
+
declare const usePlacesAutocomplete: ({ requestOptions, debounce, cache, cacheKey, callbackName, defaultValue, initOnMount, }?: HookArgs) => HookReturn;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns the status of the Google Maps API and the google object which
|
|
82
|
+
* provides access to the [Google Maps API](https://developers.google.com/maps/documentation/javascript/overview).
|
|
83
|
+
*
|
|
84
|
+
* @returns An object containing the API status and the google object.
|
|
85
|
+
*/
|
|
86
|
+
declare const useGoogleMaps: () => {
|
|
87
|
+
isReady: boolean;
|
|
88
|
+
status: "idle" | "loading" | "ready" | "error";
|
|
89
|
+
google: {
|
|
90
|
+
maps: GoogleMaps;
|
|
91
|
+
} | {
|
|
92
|
+
maps: null;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export { GoogleMapsProvider, MapProvider, useGeocoder, useGoogleMaps, useMap, usePlacesAutocomplete };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttoss/google-maps",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "ttoss",
|
|
6
6
|
"contributors": [
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"use-callback-ref": "^1.3.2",
|
|
27
|
-
"@ttoss/react-hooks": "^2.
|
|
27
|
+
"@ttoss/react-hooks": "^2.2.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"react": ">=16.8.0",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/google.maps": "^3.58.1",
|
|
35
|
-
"@types/react": "^19.2.
|
|
35
|
+
"@types/react": "^19.2.14",
|
|
36
36
|
"jest": "^30.2.0",
|
|
37
|
-
"react": "^19.2.
|
|
37
|
+
"react": "^19.2.4",
|
|
38
38
|
"tsup": "^8.5.1",
|
|
39
|
-
"@ttoss/
|
|
40
|
-
"@ttoss/
|
|
39
|
+
"@ttoss/test-utils": "^4.1.0",
|
|
40
|
+
"@ttoss/config": "^1.36.0"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"Google",
|