react-datocms 3.0.14 → 3.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-datocms",
3
- "version": "3.0.14",
3
+ "version": "3.1.1",
4
4
  "types": "dist/types/index.d.ts",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -43,7 +43,7 @@
43
43
  "watch": "rimraf dist && tsc --watch",
44
44
  "prepare": "npm run test && npm run build",
45
45
  "test": "jest --coverage",
46
- "toc": "doctoc README.md"
46
+ "toc": "doctoc --github docs"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">= 16.12.0"
@@ -76,10 +76,11 @@
76
76
  ]
77
77
  },
78
78
  "dependencies": {
79
- "datocms-listen": "^0.1.7",
79
+ "datocms-listen": "^0.1.9",
80
80
  "datocms-structured-text-generic-html-renderer": "^2.0.1",
81
81
  "datocms-structured-text-utils": "^2.0.1",
82
82
  "react-intersection-observer": "^8.33.1",
83
+ "react-string-replace": "^1.1.0",
83
84
  "universal-base64": "^2.1.0",
84
85
  "use-deep-compare-effect": "^1.6.1"
85
86
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './Image/index';
2
2
  export * from './Seo/index';
3
3
  export * from './useQuerySubscription/index';
4
- export * from './StructuredText/index';
4
+ export * from './StructuredText/index';
5
+ export * from './useSiteSearch/index';
@@ -1,16 +1,16 @@
1
- import { useState } from "react";
1
+ import { useState } from 'react';
2
2
  import {
3
3
  subscribeToQuery,
4
4
  UnsubscribeFn,
5
5
  ChannelErrorData,
6
6
  ConnectionStatus,
7
7
  Options,
8
- } from "datocms-listen";
9
- import { useDeepCompareEffectNoCheck as useDeepCompareEffect } from "use-deep-compare-effect";
8
+ } from 'datocms-listen';
9
+ import { useDeepCompareEffectNoCheck as useDeepCompareEffect } from 'use-deep-compare-effect';
10
10
 
11
11
  export type SubscribeToQueryOptions<QueryResult, QueryVariables> = Omit<
12
12
  Options<QueryResult, QueryVariables>,
13
- "onStatusChange" | "onUpdate" | "onChannelError"
13
+ 'onStatusChange' | 'onUpdate' | 'onChannelError'
14
14
  >;
15
15
 
16
16
  export type EnabledQueryListenerOptions<QueryResult, QueryVariables> = {
@@ -33,14 +33,14 @@ export type QueryListenerOptions<QueryResult, QueryVariables> =
33
33
 
34
34
  export function useQuerySubscription<
35
35
  QueryResult = any,
36
- QueryVariables = Record<string, any>
36
+ QueryVariables = Record<string, any>,
37
37
  >(options: QueryListenerOptions<QueryResult, QueryVariables>) {
38
38
  const { enabled, initialData, ...other } = options;
39
39
 
40
40
  const [error, setError] = useState<ChannelErrorData | null>(null);
41
41
  const [data, setData] = useState<QueryResult | null>(null);
42
42
  const [status, setStatus] = useState<ConnectionStatus>(
43
- enabled ? "connecting" : "closed"
43
+ enabled ? 'connecting' : 'closed',
44
44
  );
45
45
 
46
46
  const subscribeToQueryOptions = other as EnabledQueryListenerOptions<
@@ -50,7 +50,7 @@ export function useQuerySubscription<
50
50
 
51
51
  useDeepCompareEffect(() => {
52
52
  if (enabled === false) {
53
- setStatus("closed");
53
+ setStatus('closed');
54
54
 
55
55
  return () => {
56
56
  // we don't have to perform any uninstall
@@ -0,0 +1,272 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import reactStringReplace from 'react-string-replace';
3
+
4
+ type SearchResultInstancesHrefSchema = {
5
+ page?: {
6
+ offset?: number;
7
+ limit?: number;
8
+ [k: string]: unknown;
9
+ };
10
+ filter: {
11
+ fuzzy?: string;
12
+ query: string;
13
+ build_trigger_id?: string;
14
+ locale?: string;
15
+ [k: string]: unknown;
16
+ };
17
+ [k: string]: unknown;
18
+ };
19
+
20
+ type SearchResultInstancesTargetSchema = {
21
+ data: RawSearchResult[];
22
+ meta: {
23
+ total_count: number;
24
+ };
25
+ };
26
+
27
+ export type RawSearchResult = {
28
+ type: 'search_result';
29
+ id: string;
30
+ attributes: {
31
+ title: string;
32
+ body_excerpt: string;
33
+ url: string;
34
+ score: number;
35
+ highlight: {
36
+ title?: string[] | null;
37
+ body?: string[] | null;
38
+ };
39
+ };
40
+ };
41
+
42
+ declare class GenericClient {
43
+ config: {
44
+ apiToken: string | null;
45
+ };
46
+ searchResults: {
47
+ rawList(
48
+ queryParams: SearchResultInstancesHrefSchema,
49
+ ): Promise<SearchResultInstancesTargetSchema>;
50
+ };
51
+ }
52
+
53
+ type Highlighter = (
54
+ match: string,
55
+ key: string,
56
+ context: 'title' | 'bodyExcerpt',
57
+ ) => React.ReactNode;
58
+
59
+ export type UseSiteSearchConfig<Client extends GenericClient> = {
60
+ client: Client;
61
+ buildTriggerId: string;
62
+ fuzzySearch?: boolean;
63
+ resultsPerPage?: number;
64
+ highlightMatch?: Highlighter;
65
+ initialState?: {
66
+ locale?: string;
67
+ page?: number;
68
+ query?: string;
69
+ };
70
+ };
71
+
72
+ type SearchResult = {
73
+ id: string;
74
+ title: React.ReactNode;
75
+ bodyExcerpt: React.ReactNode;
76
+ url: string;
77
+ raw: RawSearchResult;
78
+ };
79
+
80
+ export type UseSiteSearchData = {
81
+ pageResults: SearchResult[];
82
+ totalResults: number;
83
+ totalPages: number;
84
+ };
85
+
86
+ export type UseSiteSearchResult = {
87
+ state: {
88
+ query: string;
89
+ setQuery: (newQuery: string) => void;
90
+ locale: string | undefined;
91
+ setLocale: (newLocale: string) => void;
92
+ page: number;
93
+ setPage: (newPage: number) => void;
94
+ };
95
+ data?: UseSiteSearchData;
96
+ error?: string;
97
+ };
98
+
99
+ const defaultHighlighter: Highlighter = (text, key) => (
100
+ <mark key={key}>{text}</mark>
101
+ );
102
+
103
+ function MatchHighlighter({
104
+ children,
105
+ highlighter,
106
+ context,
107
+ }: {
108
+ children: string;
109
+ highlighter: Highlighter;
110
+ context: 'title' | 'bodyExcerpt';
111
+ }) {
112
+ return (
113
+ <>
114
+ {reactStringReplace(children, /\[h\](.+?)\[\/h\]/g, (match, index) =>
115
+ highlighter(match, index.toString(), context),
116
+ )}
117
+ </>
118
+ );
119
+ }
120
+
121
+ export function useSiteSearch<Client extends GenericClient>(
122
+ config: UseSiteSearchConfig<Client>,
123
+ ): UseSiteSearchResult {
124
+ const [state, setState] = useState<{
125
+ query: string;
126
+ page: number;
127
+ locale: string | undefined;
128
+ }>({
129
+ query: config.initialState?.query || '',
130
+ page: config.initialState?.page || 0,
131
+ locale: config.initialState?.locale,
132
+ });
133
+
134
+ const [error, setError] = useState<string | undefined>();
135
+ const [response, setResponse] = useState<
136
+ SearchResultInstancesTargetSchema | undefined
137
+ >();
138
+
139
+ const resultsPerPage = config.resultsPerPage || 8;
140
+
141
+ useEffect(() => {
142
+ let isCancelled = false;
143
+
144
+ const run = async () => {
145
+ try {
146
+ setError(undefined);
147
+
148
+ if (!state.query) {
149
+ setResponse({ data: [], meta: { total_count: 0 } });
150
+ return;
151
+ }
152
+
153
+ setResponse(undefined);
154
+
155
+ const request: SearchResultInstancesHrefSchema = {
156
+ filter: {
157
+ query: state.query,
158
+ locale: state.locale,
159
+ build_trigger_id: config.buildTriggerId,
160
+ },
161
+ page: {
162
+ limit: resultsPerPage,
163
+ offset: resultsPerPage * state.page,
164
+ },
165
+ };
166
+
167
+ if (config.fuzzySearch) {
168
+ request.fuzzy = 'true';
169
+ }
170
+
171
+ const response = await config.client.searchResults.rawList(request);
172
+
173
+ if (!isCancelled) {
174
+ setResponse(response);
175
+ }
176
+ } catch (e) {
177
+ if (isCancelled) {
178
+ return;
179
+ }
180
+
181
+ if (e instanceof Error) {
182
+ setError(e.message);
183
+ } else {
184
+ setError('Unknown error!');
185
+ }
186
+ }
187
+ };
188
+
189
+ run();
190
+
191
+ return () => {
192
+ isCancelled = true;
193
+ };
194
+ }, [
195
+ resultsPerPage,
196
+ state,
197
+ config.buildTriggerId,
198
+ config.fuzzySearch,
199
+ config.client.config.apiToken,
200
+ ]);
201
+
202
+ const publicSetQuery = useCallback(
203
+ (newQuery: string) => {
204
+ setState((oldState) => ({ ...oldState, query: newQuery, page: 0 }));
205
+ },
206
+ [setState],
207
+ );
208
+
209
+ const publicSetPage = useCallback(
210
+ (newPage: number) => {
211
+ setState((oldState) => ({ ...oldState, page: newPage }));
212
+ },
213
+ [setState],
214
+ );
215
+
216
+ const publicSetLocale = useCallback(
217
+ (newLocale: string | undefined) => {
218
+ setState((oldState) => ({ ...oldState, locale: newLocale, page: 0 }));
219
+ },
220
+ [setState],
221
+ );
222
+
223
+ const highlighter = config.highlightMatch || defaultHighlighter;
224
+
225
+ return {
226
+ state: {
227
+ query: state.query,
228
+ setQuery: publicSetQuery,
229
+ page: state.page,
230
+ setPage: publicSetPage,
231
+ locale: state.locale,
232
+ setLocale: publicSetLocale,
233
+ },
234
+
235
+ error,
236
+ data:
237
+ state.query === ''
238
+ ? {
239
+ pageResults: [],
240
+ totalResults: 0,
241
+ totalPages: 0,
242
+ }
243
+ : response
244
+ ? {
245
+ pageResults: response.data.map((rawSearchResult) => ({
246
+ id: rawSearchResult.id,
247
+ url: rawSearchResult.attributes.url,
248
+ title: rawSearchResult.attributes.highlight.title ? (
249
+ <MatchHighlighter highlighter={highlighter} context="title">
250
+ {rawSearchResult.attributes.highlight.title[0]}
251
+ </MatchHighlighter>
252
+ ) : (
253
+ rawSearchResult.attributes.title
254
+ ),
255
+ bodyExcerpt: rawSearchResult.attributes.highlight.body ? (
256
+ <MatchHighlighter
257
+ highlighter={highlighter}
258
+ context="bodyExcerpt"
259
+ >
260
+ {rawSearchResult.attributes.highlight.body[0]}
261
+ </MatchHighlighter>
262
+ ) : (
263
+ rawSearchResult.attributes.body_excerpt
264
+ ),
265
+ raw: rawSearchResult,
266
+ })),
267
+ totalResults: response.meta.total_count,
268
+ totalPages: Math.ceil(response.meta.total_count / resultsPerPage),
269
+ }
270
+ : undefined,
271
+ };
272
+ }