@shopify/cli 3.65.1 → 3.65.3
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/assets/hydrogen/i18n/domains.ts +4 -11
- package/dist/assets/hydrogen/i18n/mock-i18n-types.ts +4 -2
- package/dist/assets/hydrogen/i18n/subdomains.ts +4 -11
- package/dist/assets/hydrogen/i18n/subfolders.ts +4 -11
- package/dist/assets/hydrogen/starter/CHANGELOG.md +165 -0
- package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +5 -2
- package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +2 -2
- package/dist/assets/hydrogen/starter/app/components/PageLayout.tsx +65 -19
- package/dist/assets/hydrogen/starter/app/components/PaginatedResourceSection.tsx +42 -0
- package/dist/assets/hydrogen/starter/app/components/SearchForm.tsx +68 -0
- package/dist/assets/hydrogen/starter/app/components/SearchFormPredictive.tsx +76 -0
- package/dist/assets/hydrogen/starter/app/components/SearchResults.tsx +164 -0
- package/dist/assets/hydrogen/starter/app/components/SearchResultsPredictive.tsx +322 -0
- package/dist/assets/hydrogen/starter/app/entry.client.tsx +10 -8
- package/dist/assets/hydrogen/starter/app/entry.server.tsx +1 -1
- package/dist/assets/hydrogen/starter/app/lib/context.ts +43 -0
- package/dist/assets/hydrogen/starter/app/lib/fragments.ts +53 -0
- package/dist/assets/hydrogen/starter/app/lib/search.ts +74 -24
- package/dist/assets/hydrogen/starter/app/root.tsx +4 -7
- package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +2 -3
- package/dist/assets/hydrogen/starter/app/routes/account.orders._index.tsx +5 -19
- package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle._index.tsx +11 -24
- package/dist/assets/hydrogen/starter/app/routes/blogs._index.tsx +14 -27
- package/dist/assets/hydrogen/starter/app/routes/collections.$handle.tsx +12 -30
- package/dist/assets/hydrogen/starter/app/routes/collections._index.tsx +13 -27
- package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +9 -31
- package/dist/assets/hydrogen/starter/app/routes/search.tsx +312 -73
- package/dist/assets/hydrogen/starter/app/styles/reset.css +12 -2
- package/dist/assets/hydrogen/starter/env.d.ts +11 -30
- package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.jpg +0 -0
- package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.md +391 -0
- package/dist/assets/hydrogen/starter/guides/search/search.jpg +0 -0
- package/dist/assets/hydrogen/starter/guides/search/search.md +333 -0
- package/dist/assets/hydrogen/starter/package.json +4 -4
- package/dist/assets/hydrogen/starter/server.ts +18 -74
- package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +242 -172
- package/dist/assets/hydrogen/virtual-routes/components/{PageLayout.jsx → Layout.jsx} +2 -2
- package/dist/assets/hydrogen/virtual-routes/virtual-root.jsx +7 -6
- package/dist/{chunk-H42RFZDD.js → chunk-3ABSSTBQ.js} +4 -4
- package/dist/{chunk-M7WMYV4S.js → chunk-3D4VZQOH.js} +2 -2
- package/dist/{chunk-J7BYFGNJ.js → chunk-3GSKXZGY.js} +2 -2
- package/dist/{chunk-TDWX3KIR.js → chunk-3LDWVYMD.js} +2 -2
- package/dist/{chunk-N2BXKOJG.js → chunk-646BIVHE.js} +4 -4
- package/dist/{chunk-CZ3SHYYH.js → chunk-7WAEFADN.js} +4 -4
- package/dist/{chunk-EKT2GUGH.js → chunk-7WGBIPDW.js} +2 -2
- package/dist/{chunk-M6KGRVDD.js → chunk-AX77SAMU.js} +3 -3
- package/dist/{chunk-4HAEQQTQ.js → chunk-BQBBVYYU.js} +4 -4
- package/dist/{chunk-5YD4FDOS.js → chunk-BZLNTDGG.js} +3 -3
- package/dist/{chunk-VWALMO2Z.js → chunk-CSCEGIBZ.js} +3 -3
- package/dist/{chunk-F2Y7KYHZ.js → chunk-EIUQV76I.js} +5 -5
- package/dist/{chunk-MODBIZ4R.js → chunk-GN74L7IW.js} +2 -2
- package/dist/{chunk-5EAVIJTQ.js → chunk-HYCRESCR.js} +2 -2
- package/dist/{chunk-GDARYUPU.js → chunk-K7KD247K.js} +188 -243
- package/dist/{chunk-PZM45AUI.js → chunk-KIUXMPTX.js} +3 -3
- package/dist/{chunk-PYMSCBPA.js → chunk-LAJ4OEME.js} +2 -2
- package/dist/{chunk-YVHV3H5H.js → chunk-MIQBXNSN.js} +4 -4
- package/dist/{chunk-BLKDGMHM.js → chunk-MV6A3QHA.js} +4 -4
- package/dist/{chunk-CFFAWVDL.js → chunk-N3YORLAS.js} +2 -2
- package/dist/{chunk-EU5ZOEUT.js → chunk-NBTEOGQW.js} +2 -2
- package/dist/{chunk-ZXJU6UP4.js → chunk-O3JOUAA5.js} +4 -4
- package/dist/{chunk-EZ5DG73H.js → chunk-PEAIOYXD.js} +4 -4
- package/dist/{chunk-YDS7NZBQ.js → chunk-R5GT4GBL.js} +4 -4
- package/dist/{chunk-6M65VRAT.js → chunk-S7FJTFYR.js} +5 -5
- package/dist/{chunk-DX2RXOQ5.js → chunk-S7RH664J.js} +3 -3
- package/dist/{chunk-WMECC32P.js → chunk-SKF2SKWO.js} +3 -3
- package/dist/{chunk-27HGZPUX.js → chunk-SMKCVFDT.js} +3 -3
- package/dist/{chunk-EID6L4PR.js → chunk-T4Y7NDNJ.js} +2 -2
- package/dist/{chunk-PY33KMCK.js → chunk-TWWJNMTO.js} +2 -2
- package/dist/{chunk-YXPGPWR2.js → chunk-U2PN6QZ2.js} +5 -5
- package/dist/{chunk-3REVOIEW.js → chunk-UBCH575K.js} +5 -5
- package/dist/{chunk-A4NQWDPT.js → chunk-XLURAR5E.js} +3 -3
- package/dist/{chunk-ZZKUI3DP.js → chunk-YPG7LXPN.js} +3 -3
- package/dist/cli/commands/auth/logout.js +10 -10
- package/dist/cli/commands/auth/logout.test.js +11 -11
- package/dist/cli/commands/debug/command-flags.js +9 -9
- package/dist/cli/commands/demo/catalog.js +10 -10
- package/dist/cli/commands/demo/generate-file.js +10 -10
- package/dist/cli/commands/demo/index.js +10 -10
- package/dist/cli/commands/demo/print-ai-prompt.js +10 -10
- package/dist/cli/commands/docs/generate.js +9 -9
- package/dist/cli/commands/docs/generate.test.js +9 -9
- package/dist/cli/commands/help.js +9 -9
- package/dist/cli/commands/kitchen-sink/async.js +10 -10
- package/dist/cli/commands/kitchen-sink/async.test.js +10 -10
- package/dist/cli/commands/kitchen-sink/index.js +12 -12
- package/dist/cli/commands/kitchen-sink/index.test.js +12 -12
- package/dist/cli/commands/kitchen-sink/prompts.js +10 -10
- package/dist/cli/commands/kitchen-sink/prompts.test.js +10 -10
- package/dist/cli/commands/kitchen-sink/static.js +10 -10
- package/dist/cli/commands/kitchen-sink/static.test.js +10 -10
- package/dist/cli/commands/search.js +10 -10
- package/dist/cli/commands/upgrade.js +9 -9
- package/dist/cli/commands/version.js +10 -10
- package/dist/cli/commands/version.test.js +10 -10
- package/dist/cli/services/commands/search.js +2 -2
- package/dist/cli/services/commands/search.test.js +2 -2
- package/dist/cli/services/commands/version.js +4 -4
- package/dist/cli/services/commands/version.test.js +5 -5
- package/dist/cli/services/demo.js +2 -2
- package/dist/cli/services/demo.test.js +2 -2
- package/dist/cli/services/kitchen-sink/async.js +2 -2
- package/dist/cli/services/kitchen-sink/prompts.js +2 -2
- package/dist/cli/services/kitchen-sink/static.js +2 -2
- package/dist/cli/services/upgrade.js +3 -3
- package/dist/cli/services/upgrade.test.js +5 -5
- package/dist/{custom-oclif-loader-JHNX2EGV.js → custom-oclif-loader-BT7EH2NN.js} +3 -3
- package/dist/{error-handler-4UJ6363X.js → error-handler-OSEY6KVA.js} +8 -8
- package/dist/hooks/postrun.js +6 -6
- package/dist/hooks/prerun.js +4 -4
- package/dist/index.js +1333 -1279
- package/dist/{local-V7RONWNU.js → local-OQXN5NM2.js} +2 -2
- package/dist/{morph-DN4AZJZW.js → morph-IQTWRBBT.js} +16 -12
- package/dist/{node-3H4OKRLA.js → node-YQVH3Y7J.js} +13 -13
- package/dist/{node-package-manager-XM7EXHQA.js → node-package-manager-VW2DN7R4.js} +3 -3
- package/dist/{system-F63VIZ5U.js → system-347PZWVP.js} +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/{ui-BXWWRIFS.js → ui-S7L55PBH.js} +2 -2
- package/dist/{workerd-A5NCF6UA.js → workerd-OLKE7G4X.js} +12 -12
- package/oclif.manifest.json +39 -2
- package/package.json +7 -7
- package/dist/assets/hydrogen/starter/app/components/Search.tsx +0 -514
- package/dist/assets/hydrogen/starter/app/routes/api.predictive-search.tsx +0 -318
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
json,
|
|
3
|
+
type LoaderFunctionArgs,
|
|
4
|
+
type ActionFunctionArgs,
|
|
5
|
+
} from '@shopify/remix-oxygen';
|
|
2
6
|
import {useLoaderData, type MetaFunction} from '@remix-run/react';
|
|
3
7
|
import {getPaginationVariables, Analytics} from '@shopify/hydrogen';
|
|
4
|
-
|
|
5
|
-
import {
|
|
8
|
+
import {SearchForm} from '~/components/SearchForm';
|
|
9
|
+
import {SearchResults} from '~/components/SearchResults';
|
|
10
|
+
import {
|
|
11
|
+
type RegularSearchReturn,
|
|
12
|
+
type PredictiveSearchReturn,
|
|
13
|
+
getEmptyPredictiveSearchResult,
|
|
14
|
+
} from '~/lib/search';
|
|
6
15
|
|
|
7
16
|
export const meta: MetaFunction = () => {
|
|
8
17
|
return [{title: `Hydrogen | Search`}];
|
|
@@ -10,66 +19,68 @@ export const meta: MetaFunction = () => {
|
|
|
10
19
|
|
|
11
20
|
export async function loader({request, context}: LoaderFunctionArgs) {
|
|
12
21
|
const url = new URL(request.url);
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
22
|
+
const isPredictive = url.searchParams.has('predictive');
|
|
23
|
+
const searchPromise = isPredictive
|
|
24
|
+
? predictiveSearch({request, context})
|
|
25
|
+
: regularSearch({request, context});
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
searchTerm,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const {errors, ...data} = await context.storefront.query(SEARCH_QUERY, {
|
|
25
|
-
variables: {
|
|
26
|
-
query: searchTerm,
|
|
27
|
-
...variables,
|
|
28
|
-
},
|
|
27
|
+
searchPromise.catch((error: Error) => {
|
|
28
|
+
console.error(error);
|
|
29
|
+
return {term: '', result: null, error: error.message};
|
|
29
30
|
});
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
throw new Error('No search data returned from Shopify API');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const totalResults = Object.values(data).reduce((total, value) => {
|
|
36
|
-
return total + value.nodes.length;
|
|
37
|
-
}, 0);
|
|
38
|
-
|
|
39
|
-
const searchResults = {
|
|
40
|
-
results: data,
|
|
41
|
-
totalResults,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
return defer({
|
|
45
|
-
searchTerm,
|
|
46
|
-
searchResults,
|
|
47
|
-
});
|
|
32
|
+
return json(await searchPromise);
|
|
48
33
|
}
|
|
49
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Renders the /search route
|
|
37
|
+
*/
|
|
50
38
|
export default function SearchPage() {
|
|
51
|
-
const {
|
|
39
|
+
const {type, term, result, error} = useLoaderData<typeof loader>();
|
|
40
|
+
if (type === 'predictive') return null;
|
|
52
41
|
|
|
53
42
|
return (
|
|
54
43
|
<div className="search">
|
|
55
44
|
<h1>Search</h1>
|
|
56
|
-
<SearchForm
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
<SearchForm>
|
|
46
|
+
{({inputRef}) => (
|
|
47
|
+
<>
|
|
48
|
+
<input
|
|
49
|
+
defaultValue={term}
|
|
50
|
+
name="q"
|
|
51
|
+
placeholder="Search…"
|
|
52
|
+
ref={inputRef}
|
|
53
|
+
type="search"
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
<button type="submit">Search</button>
|
|
57
|
+
</>
|
|
58
|
+
)}
|
|
59
|
+
</SearchForm>
|
|
60
|
+
{error && <p style={{color: 'red'}}>{error}</p>}
|
|
61
|
+
{!term || !result?.total ? (
|
|
62
|
+
<SearchResults.Empty />
|
|
59
63
|
) : (
|
|
60
|
-
<SearchResults
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
<SearchResults result={result} term={term}>
|
|
65
|
+
{({articles, pages, products, term}) => (
|
|
66
|
+
<div>
|
|
67
|
+
<SearchResults.Products products={products} term={term} />
|
|
68
|
+
<SearchResults.Pages pages={pages} term={term} />
|
|
69
|
+
<SearchResults.Articles articles={articles} term={term} />
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</SearchResults>
|
|
64
73
|
)}
|
|
65
|
-
<Analytics.SearchView
|
|
66
|
-
data={{searchTerm, searchResults}}
|
|
67
|
-
/>
|
|
74
|
+
<Analytics.SearchView data={{searchTerm: term, searchResults: result}} />
|
|
68
75
|
</div>
|
|
69
76
|
);
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Regular search query and fragments
|
|
81
|
+
* (adjust as needed)
|
|
82
|
+
*/
|
|
83
|
+
const SEARCH_PRODUCT_FRAGMENT = `#graphql
|
|
73
84
|
fragment SearchProduct on Product {
|
|
74
85
|
__typename
|
|
75
86
|
handle
|
|
@@ -106,6 +117,9 @@ const SEARCH_QUERY = `#graphql
|
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
}
|
|
120
|
+
` as const;
|
|
121
|
+
|
|
122
|
+
const SEARCH_PAGE_FRAGMENT = `#graphql
|
|
109
123
|
fragment SearchPage on Page {
|
|
110
124
|
__typename
|
|
111
125
|
handle
|
|
@@ -113,6 +127,9 @@ const SEARCH_QUERY = `#graphql
|
|
|
113
127
|
title
|
|
114
128
|
trackingParameters
|
|
115
129
|
}
|
|
130
|
+
` as const;
|
|
131
|
+
|
|
132
|
+
const SEARCH_ARTICLE_FRAGMENT = `#graphql
|
|
116
133
|
fragment SearchArticle on Article {
|
|
117
134
|
__typename
|
|
118
135
|
handle
|
|
@@ -120,41 +137,43 @@ const SEARCH_QUERY = `#graphql
|
|
|
120
137
|
title
|
|
121
138
|
trackingParameters
|
|
122
139
|
}
|
|
123
|
-
|
|
140
|
+
` as const;
|
|
141
|
+
|
|
142
|
+
const PAGE_INFO_FRAGMENT = `#graphql
|
|
143
|
+
fragment PageInfoFragment on PageInfo {
|
|
144
|
+
hasNextPage
|
|
145
|
+
hasPreviousPage
|
|
146
|
+
startCursor
|
|
147
|
+
endCursor
|
|
148
|
+
}
|
|
149
|
+
` as const;
|
|
150
|
+
|
|
151
|
+
// NOTE: https://shopify.dev/docs/api/storefront/latest/queries/search
|
|
152
|
+
export const SEARCH_QUERY = `#graphql
|
|
153
|
+
query RegularSearch(
|
|
124
154
|
$country: CountryCode
|
|
125
155
|
$endCursor: String
|
|
126
156
|
$first: Int
|
|
127
157
|
$language: LanguageCode
|
|
128
158
|
$last: Int
|
|
129
|
-
$
|
|
159
|
+
$term: String!
|
|
130
160
|
$startCursor: String
|
|
131
161
|
) @inContext(country: $country, language: $language) {
|
|
132
|
-
|
|
133
|
-
query: $
|
|
134
|
-
|
|
135
|
-
types: [PRODUCT],
|
|
162
|
+
articles: search(
|
|
163
|
+
query: $term,
|
|
164
|
+
types: [ARTICLE],
|
|
136
165
|
first: $first,
|
|
137
|
-
sortKey: RELEVANCE,
|
|
138
|
-
last: $last,
|
|
139
|
-
before: $startCursor,
|
|
140
|
-
after: $endCursor
|
|
141
166
|
) {
|
|
142
167
|
nodes {
|
|
143
|
-
...on
|
|
144
|
-
...
|
|
168
|
+
...on Article {
|
|
169
|
+
...SearchArticle
|
|
145
170
|
}
|
|
146
171
|
}
|
|
147
|
-
pageInfo {
|
|
148
|
-
hasNextPage
|
|
149
|
-
hasPreviousPage
|
|
150
|
-
startCursor
|
|
151
|
-
endCursor
|
|
152
|
-
}
|
|
153
172
|
}
|
|
154
173
|
pages: search(
|
|
155
|
-
query: $
|
|
174
|
+
query: $term,
|
|
156
175
|
types: [PAGE],
|
|
157
|
-
first:
|
|
176
|
+
first: $first,
|
|
158
177
|
) {
|
|
159
178
|
nodes {
|
|
160
179
|
...on Page {
|
|
@@ -162,16 +181,236 @@ const SEARCH_QUERY = `#graphql
|
|
|
162
181
|
}
|
|
163
182
|
}
|
|
164
183
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
first:
|
|
184
|
+
products: search(
|
|
185
|
+
after: $endCursor,
|
|
186
|
+
before: $startCursor,
|
|
187
|
+
first: $first,
|
|
188
|
+
last: $last,
|
|
189
|
+
query: $term,
|
|
190
|
+
sortKey: RELEVANCE,
|
|
191
|
+
types: [PRODUCT],
|
|
192
|
+
unavailableProducts: HIDE,
|
|
169
193
|
) {
|
|
170
194
|
nodes {
|
|
171
|
-
...on
|
|
172
|
-
...
|
|
195
|
+
...on Product {
|
|
196
|
+
...SearchProduct
|
|
173
197
|
}
|
|
174
198
|
}
|
|
199
|
+
pageInfo {
|
|
200
|
+
...PageInfoFragment
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
${SEARCH_PRODUCT_FRAGMENT}
|
|
205
|
+
${SEARCH_PAGE_FRAGMENT}
|
|
206
|
+
${SEARCH_ARTICLE_FRAGMENT}
|
|
207
|
+
${PAGE_INFO_FRAGMENT}
|
|
208
|
+
` as const;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Regular search fetcher
|
|
212
|
+
*/
|
|
213
|
+
async function regularSearch({
|
|
214
|
+
request,
|
|
215
|
+
context,
|
|
216
|
+
}: Pick<
|
|
217
|
+
LoaderFunctionArgs,
|
|
218
|
+
'request' | 'context'
|
|
219
|
+
>): Promise<RegularSearchReturn> {
|
|
220
|
+
const {storefront} = context;
|
|
221
|
+
const url = new URL(request.url);
|
|
222
|
+
const variables = getPaginationVariables(request, {pageBy: 8});
|
|
223
|
+
const term = String(url.searchParams.get('q') || '');
|
|
224
|
+
|
|
225
|
+
// Search articles, pages, and products for the `q` term
|
|
226
|
+
const {errors, ...items} = await storefront.query(SEARCH_QUERY, {
|
|
227
|
+
variables: {...variables, term},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (!items) {
|
|
231
|
+
throw new Error('No search data returned from Shopify API');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const total = Object.values(items).reduce(
|
|
235
|
+
(acc, {nodes}) => acc + nodes.length,
|
|
236
|
+
0,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const error = errors
|
|
240
|
+
? errors.map(({message}) => message).join(', ')
|
|
241
|
+
: undefined;
|
|
242
|
+
|
|
243
|
+
return {type: 'regular', term, error, result: {total, items}};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Predictive search query and fragments
|
|
248
|
+
* (adjust as needed)
|
|
249
|
+
*/
|
|
250
|
+
const PREDICTIVE_SEARCH_ARTICLE_FRAGMENT = `#graphql
|
|
251
|
+
fragment PredictiveArticle on Article {
|
|
252
|
+
__typename
|
|
253
|
+
id
|
|
254
|
+
title
|
|
255
|
+
handle
|
|
256
|
+
blog {
|
|
257
|
+
handle
|
|
175
258
|
}
|
|
259
|
+
image {
|
|
260
|
+
url
|
|
261
|
+
altText
|
|
262
|
+
width
|
|
263
|
+
height
|
|
264
|
+
}
|
|
265
|
+
trackingParameters
|
|
176
266
|
}
|
|
177
267
|
` as const;
|
|
268
|
+
|
|
269
|
+
const PREDICTIVE_SEARCH_COLLECTION_FRAGMENT = `#graphql
|
|
270
|
+
fragment PredictiveCollection on Collection {
|
|
271
|
+
__typename
|
|
272
|
+
id
|
|
273
|
+
title
|
|
274
|
+
handle
|
|
275
|
+
image {
|
|
276
|
+
url
|
|
277
|
+
altText
|
|
278
|
+
width
|
|
279
|
+
height
|
|
280
|
+
}
|
|
281
|
+
trackingParameters
|
|
282
|
+
}
|
|
283
|
+
` as const;
|
|
284
|
+
|
|
285
|
+
const PREDICTIVE_SEARCH_PAGE_FRAGMENT = `#graphql
|
|
286
|
+
fragment PredictivePage on Page {
|
|
287
|
+
__typename
|
|
288
|
+
id
|
|
289
|
+
title
|
|
290
|
+
handle
|
|
291
|
+
trackingParameters
|
|
292
|
+
}
|
|
293
|
+
` as const;
|
|
294
|
+
|
|
295
|
+
const PREDICTIVE_SEARCH_PRODUCT_FRAGMENT = `#graphql
|
|
296
|
+
fragment PredictiveProduct on Product {
|
|
297
|
+
__typename
|
|
298
|
+
id
|
|
299
|
+
title
|
|
300
|
+
handle
|
|
301
|
+
trackingParameters
|
|
302
|
+
variants(first: 1) {
|
|
303
|
+
nodes {
|
|
304
|
+
id
|
|
305
|
+
image {
|
|
306
|
+
url
|
|
307
|
+
altText
|
|
308
|
+
width
|
|
309
|
+
height
|
|
310
|
+
}
|
|
311
|
+
price {
|
|
312
|
+
amount
|
|
313
|
+
currencyCode
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
` as const;
|
|
319
|
+
|
|
320
|
+
const PREDICTIVE_SEARCH_QUERY_FRAGMENT = `#graphql
|
|
321
|
+
fragment PredictiveQuery on SearchQuerySuggestion {
|
|
322
|
+
__typename
|
|
323
|
+
text
|
|
324
|
+
styledText
|
|
325
|
+
trackingParameters
|
|
326
|
+
}
|
|
327
|
+
` as const;
|
|
328
|
+
|
|
329
|
+
// NOTE: https://shopify.dev/docs/api/storefront/latest/queries/predictiveSearch
|
|
330
|
+
const PREDICTIVE_SEARCH_QUERY = `#graphql
|
|
331
|
+
query PredictiveSearch(
|
|
332
|
+
$country: CountryCode
|
|
333
|
+
$language: LanguageCode
|
|
334
|
+
$limit: Int!
|
|
335
|
+
$limitScope: PredictiveSearchLimitScope!
|
|
336
|
+
$term: String!
|
|
337
|
+
$types: [PredictiveSearchType!]
|
|
338
|
+
) @inContext(country: $country, language: $language) {
|
|
339
|
+
predictiveSearch(
|
|
340
|
+
limit: $limit,
|
|
341
|
+
limitScope: $limitScope,
|
|
342
|
+
query: $term,
|
|
343
|
+
types: $types,
|
|
344
|
+
) {
|
|
345
|
+
articles {
|
|
346
|
+
...PredictiveArticle
|
|
347
|
+
}
|
|
348
|
+
collections {
|
|
349
|
+
...PredictiveCollection
|
|
350
|
+
}
|
|
351
|
+
pages {
|
|
352
|
+
...PredictivePage
|
|
353
|
+
}
|
|
354
|
+
products {
|
|
355
|
+
...PredictiveProduct
|
|
356
|
+
}
|
|
357
|
+
queries {
|
|
358
|
+
...PredictiveQuery
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
${PREDICTIVE_SEARCH_ARTICLE_FRAGMENT}
|
|
363
|
+
${PREDICTIVE_SEARCH_COLLECTION_FRAGMENT}
|
|
364
|
+
${PREDICTIVE_SEARCH_PAGE_FRAGMENT}
|
|
365
|
+
${PREDICTIVE_SEARCH_PRODUCT_FRAGMENT}
|
|
366
|
+
${PREDICTIVE_SEARCH_QUERY_FRAGMENT}
|
|
367
|
+
` as const;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Predictive search fetcher
|
|
371
|
+
*/
|
|
372
|
+
async function predictiveSearch({
|
|
373
|
+
request,
|
|
374
|
+
context,
|
|
375
|
+
}: Pick<
|
|
376
|
+
ActionFunctionArgs,
|
|
377
|
+
'request' | 'context'
|
|
378
|
+
>): Promise<PredictiveSearchReturn> {
|
|
379
|
+
const {storefront} = context;
|
|
380
|
+
const url = new URL(request.url);
|
|
381
|
+
const term = String(url.searchParams.get('q') || '').trim();
|
|
382
|
+
const limit = Number(url.searchParams.get('limit') || 10);
|
|
383
|
+
const type = 'predictive';
|
|
384
|
+
|
|
385
|
+
if (!term) return {type, term, result: getEmptyPredictiveSearchResult()};
|
|
386
|
+
|
|
387
|
+
// Predictively search articles, collections, pages, products, and queries (suggestions)
|
|
388
|
+
const {predictiveSearch: items, errors} = await storefront.query(
|
|
389
|
+
PREDICTIVE_SEARCH_QUERY,
|
|
390
|
+
{
|
|
391
|
+
variables: {
|
|
392
|
+
// customize search options as needed
|
|
393
|
+
limit,
|
|
394
|
+
limitScope: 'EACH',
|
|
395
|
+
term,
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (errors) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`Shopify API errors: ${errors.map(({message}) => message).join(', ')}`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!items) {
|
|
407
|
+
throw new Error('No predictive search data returned from Shopify API');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const total = Object.values(items).reduce(
|
|
411
|
+
(acc, item) => acc + item.length,
|
|
412
|
+
0,
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
return {type, term, result: {items, total}};
|
|
416
|
+
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
body {
|
|
2
|
-
font-family:
|
|
3
|
-
|
|
2
|
+
font-family:
|
|
3
|
+
system-ui,
|
|
4
|
+
-apple-system,
|
|
5
|
+
BlinkMacSystemFont,
|
|
6
|
+
'Segoe UI',
|
|
7
|
+
Roboto,
|
|
8
|
+
Oxygen,
|
|
9
|
+
Ubuntu,
|
|
10
|
+
Cantarell,
|
|
11
|
+
'Open Sans',
|
|
12
|
+
'Helvetica Neue',
|
|
13
|
+
sans-serif;
|
|
4
14
|
margin: 0;
|
|
5
15
|
padding: 0;
|
|
6
16
|
}
|
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
import '@total-typescript/ts-reset';
|
|
7
7
|
|
|
8
8
|
import type {
|
|
9
|
-
|
|
10
|
-
CustomerAccount,
|
|
11
|
-
HydrogenCart,
|
|
9
|
+
HydrogenContext,
|
|
12
10
|
HydrogenSessionData,
|
|
11
|
+
HydrogenEnv,
|
|
13
12
|
} from '@shopify/hydrogen';
|
|
14
|
-
import type {
|
|
13
|
+
import type {createAppLoadContext} from '~/lib/context';
|
|
15
14
|
|
|
16
15
|
declare global {
|
|
17
16
|
/**
|
|
@@ -19,36 +18,18 @@ declare global {
|
|
|
19
18
|
*/
|
|
20
19
|
const process: {env: {NODE_ENV: 'production' | 'development'}};
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*/
|
|
25
|
-
interface Env {
|
|
26
|
-
SESSION_SECRET: string;
|
|
27
|
-
PUBLIC_STOREFRONT_API_TOKEN: string;
|
|
28
|
-
PRIVATE_STOREFRONT_API_TOKEN: string;
|
|
29
|
-
PUBLIC_STORE_DOMAIN: string;
|
|
30
|
-
PUBLIC_STOREFRONT_ID: string;
|
|
31
|
-
PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID: string;
|
|
32
|
-
PUBLIC_CUSTOMER_ACCOUNT_API_URL: string;
|
|
33
|
-
PUBLIC_CHECKOUT_DOMAIN: string;
|
|
21
|
+
interface Env extends HydrogenEnv {
|
|
22
|
+
// declare additional Env parameter use in the fetch handler and Remix loader context here
|
|
34
23
|
}
|
|
35
24
|
}
|
|
36
25
|
|
|
37
26
|
declare module '@shopify/remix-oxygen' {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
interface AppLoadContext {
|
|
42
|
-
env: Env;
|
|
43
|
-
cart: HydrogenCart;
|
|
44
|
-
storefront: Storefront;
|
|
45
|
-
customerAccount: CustomerAccount;
|
|
46
|
-
session: AppSession;
|
|
47
|
-
waitUntil: ExecutionContext['waitUntil'];
|
|
27
|
+
interface AppLoadContext
|
|
28
|
+
extends Awaited<ReturnType<typeof createAppLoadContext>> {
|
|
29
|
+
// to change context type, change the return of createAppLoadContext() instead
|
|
48
30
|
}
|
|
49
31
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
interface SessionData extends HydrogenSessionData {}
|
|
32
|
+
interface SessionData extends HydrogenSessionData {
|
|
33
|
+
// declare local additions to the Remix session data here
|
|
34
|
+
}
|
|
54
35
|
}
|