@shopify/create-hydrogen 4.3.13 → 5.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.
Files changed (127) hide show
  1. package/dist/assets/hydrogen/bundle/analyzer.html +2045 -0
  2. package/dist/assets/hydrogen/i18n/domains.ts +28 -0
  3. package/dist/assets/hydrogen/i18n/mock-i18n-types.ts +3 -0
  4. package/dist/assets/hydrogen/i18n/subdomains.ts +27 -0
  5. package/dist/assets/hydrogen/i18n/subfolders.ts +29 -0
  6. package/dist/assets/hydrogen/routes/locale-check.ts +16 -0
  7. package/dist/assets/hydrogen/starter/.eslintignore +5 -0
  8. package/dist/assets/hydrogen/starter/.eslintrc.cjs +19 -0
  9. package/dist/assets/hydrogen/starter/.graphqlrc.yml +12 -0
  10. package/dist/assets/hydrogen/starter/CHANGELOG.md +709 -0
  11. package/dist/assets/hydrogen/starter/README.md +45 -0
  12. package/dist/assets/hydrogen/starter/app/assets/favicon.svg +28 -0
  13. package/dist/assets/hydrogen/starter/app/components/AddToCartButton.tsx +37 -0
  14. package/dist/assets/hydrogen/starter/app/components/Aside.tsx +76 -0
  15. package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +150 -0
  16. package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +68 -0
  17. package/dist/assets/hydrogen/starter/app/components/CartSummary.tsx +101 -0
  18. package/dist/assets/hydrogen/starter/app/components/Footer.tsx +129 -0
  19. package/dist/assets/hydrogen/starter/app/components/Header.tsx +230 -0
  20. package/dist/assets/hydrogen/starter/app/components/PageLayout.tsx +126 -0
  21. package/dist/assets/hydrogen/starter/app/components/ProductForm.tsx +80 -0
  22. package/dist/assets/hydrogen/starter/app/components/ProductImage.tsx +23 -0
  23. package/dist/assets/hydrogen/starter/app/components/ProductPrice.tsx +27 -0
  24. package/dist/assets/hydrogen/starter/app/components/Search.tsx +514 -0
  25. package/dist/assets/hydrogen/starter/app/entry.client.tsx +12 -0
  26. package/dist/assets/hydrogen/starter/app/entry.server.tsx +47 -0
  27. package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerAddressMutations.ts +61 -0
  28. package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +40 -0
  29. package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerOrderQuery.ts +87 -0
  30. package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +58 -0
  31. package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +24 -0
  32. package/dist/assets/hydrogen/starter/app/lib/fragments.ts +174 -0
  33. package/dist/assets/hydrogen/starter/app/lib/search.ts +29 -0
  34. package/dist/assets/hydrogen/starter/app/lib/session.ts +72 -0
  35. package/dist/assets/hydrogen/starter/app/lib/variants.ts +46 -0
  36. package/dist/assets/hydrogen/starter/app/root.tsx +191 -0
  37. package/dist/assets/hydrogen/starter/app/routes/$.tsx +11 -0
  38. package/dist/assets/hydrogen/starter/app/routes/[robots.txt].tsx +118 -0
  39. package/dist/assets/hydrogen/starter/app/routes/[sitemap.xml].tsx +177 -0
  40. package/dist/assets/hydrogen/starter/app/routes/_index.tsx +182 -0
  41. package/dist/assets/hydrogen/starter/app/routes/account.$.tsx +8 -0
  42. package/dist/assets/hydrogen/starter/app/routes/account._index.tsx +5 -0
  43. package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +513 -0
  44. package/dist/assets/hydrogen/starter/app/routes/account.orders.$id.tsx +195 -0
  45. package/dist/assets/hydrogen/starter/app/routes/account.orders._index.tsx +107 -0
  46. package/dist/assets/hydrogen/starter/app/routes/account.profile.tsx +136 -0
  47. package/dist/assets/hydrogen/starter/app/routes/account.tsx +88 -0
  48. package/dist/assets/hydrogen/starter/app/routes/account_.authorize.tsx +5 -0
  49. package/dist/assets/hydrogen/starter/app/routes/account_.login.tsx +5 -0
  50. package/dist/assets/hydrogen/starter/app/routes/account_.logout.tsx +10 -0
  51. package/dist/assets/hydrogen/starter/app/routes/api.predictive-search.tsx +318 -0
  52. package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +113 -0
  53. package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle._index.tsx +188 -0
  54. package/dist/assets/hydrogen/starter/app/routes/blogs._index.tsx +119 -0
  55. package/dist/assets/hydrogen/starter/app/routes/cart.$lines.tsx +69 -0
  56. package/dist/assets/hydrogen/starter/app/routes/cart.tsx +102 -0
  57. package/dist/assets/hydrogen/starter/app/routes/collections.$handle.tsx +225 -0
  58. package/dist/assets/hydrogen/starter/app/routes/collections._index.tsx +146 -0
  59. package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +185 -0
  60. package/dist/assets/hydrogen/starter/app/routes/discount.$code.tsx +47 -0
  61. package/dist/assets/hydrogen/starter/app/routes/pages.$handle.tsx +84 -0
  62. package/dist/assets/hydrogen/starter/app/routes/policies.$handle.tsx +93 -0
  63. package/dist/assets/hydrogen/starter/app/routes/policies._index.tsx +63 -0
  64. package/dist/assets/hydrogen/starter/app/routes/products.$handle.tsx +299 -0
  65. package/dist/assets/hydrogen/starter/app/routes/search.tsx +177 -0
  66. package/dist/assets/hydrogen/starter/app/styles/app.css +486 -0
  67. package/dist/assets/hydrogen/starter/app/styles/reset.css +129 -0
  68. package/dist/assets/hydrogen/starter/customer-accountapi.generated.d.ts +509 -0
  69. package/dist/assets/hydrogen/starter/env.d.ts +54 -0
  70. package/dist/assets/hydrogen/starter/package.json +50 -0
  71. package/dist/assets/hydrogen/starter/public/.gitkeep +0 -0
  72. package/dist/assets/hydrogen/starter/server.ts +119 -0
  73. package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +1211 -0
  74. package/dist/assets/hydrogen/starter/tsconfig.json +23 -0
  75. package/dist/assets/hydrogen/starter/vite.config.ts +41 -0
  76. package/dist/assets/hydrogen/tailwind/package.json +8 -0
  77. package/dist/assets/hydrogen/tailwind/tailwind.css +6 -0
  78. package/dist/assets/hydrogen/vanilla-extract/package.json +8 -0
  79. package/dist/assets/hydrogen/virtual-routes/assets/debug-network.css +592 -0
  80. package/dist/assets/hydrogen/virtual-routes/assets/favicon-dark.svg +20 -0
  81. package/dist/assets/hydrogen/virtual-routes/assets/favicon.svg +28 -0
  82. package/dist/assets/hydrogen/virtual-routes/assets/inter-variable-font.woff2 +0 -0
  83. package/dist/assets/hydrogen/virtual-routes/assets/jetbrainsmono-variable-font.woff2 +0 -0
  84. package/dist/assets/hydrogen/virtual-routes/assets/styles.css +238 -0
  85. package/dist/assets/hydrogen/virtual-routes/components/FlameChartWrapper.jsx +123 -0
  86. package/dist/assets/hydrogen/virtual-routes/components/HydrogenLogoBaseBW.jsx +32 -0
  87. package/dist/assets/hydrogen/virtual-routes/components/HydrogenLogoBaseColor.jsx +47 -0
  88. package/dist/assets/hydrogen/virtual-routes/components/IconBanner.jsx +292 -0
  89. package/dist/assets/hydrogen/virtual-routes/components/IconClose.jsx +38 -0
  90. package/dist/assets/hydrogen/virtual-routes/components/IconDiscard.jsx +44 -0
  91. package/dist/assets/hydrogen/virtual-routes/components/IconError.jsx +61 -0
  92. package/dist/assets/hydrogen/virtual-routes/components/IconGithub.jsx +23 -0
  93. package/dist/assets/hydrogen/virtual-routes/components/IconTwitter.jsx +21 -0
  94. package/dist/assets/hydrogen/virtual-routes/components/PageLayout.jsx +7 -0
  95. package/dist/assets/hydrogen/virtual-routes/components/RequestDetails.jsx +178 -0
  96. package/dist/assets/hydrogen/virtual-routes/components/RequestTable.jsx +91 -0
  97. package/dist/assets/hydrogen/virtual-routes/components/RequestWaterfall.jsx +151 -0
  98. package/dist/assets/hydrogen/virtual-routes/lib/useDebugNetworkServer.jsx +178 -0
  99. package/dist/assets/hydrogen/virtual-routes/routes/graphiql.jsx +5 -0
  100. package/dist/assets/hydrogen/virtual-routes/routes/index.jsx +265 -0
  101. package/dist/assets/hydrogen/virtual-routes/routes/subrequest-profiler.jsx +243 -0
  102. package/dist/assets/hydrogen/virtual-routes/virtual-root.jsx +64 -0
  103. package/dist/assets/hydrogen/vite/package.json +14 -0
  104. package/dist/assets/hydrogen/vite/vite.config.js +41 -0
  105. package/dist/chokidar-2CKIHN27.js +12 -0
  106. package/dist/chunk-EO6F7WJJ.js +2 -0
  107. package/dist/chunk-FB327AH7.js +5 -0
  108. package/dist/chunk-FJPX4XUR.js +2 -0
  109. package/dist/chunk-JKOXGRAA.js +10 -0
  110. package/dist/chunk-LNQWGFTB.js +45 -0
  111. package/dist/chunk-M6JXYI3V.js +23 -0
  112. package/dist/chunk-MNT4XW23.js +2 -0
  113. package/dist/chunk-N7HFZHSO.js +1145 -0
  114. package/dist/chunk-PMDMUCNY.js +2 -0
  115. package/dist/chunk-QGLB6FFL.js +3 -0
  116. package/dist/chunk-VMIOG46Y.js +2 -0
  117. package/dist/create-app.js +1867 -34
  118. package/dist/del-CZGKV5SQ.js +11 -0
  119. package/dist/devtools-ZCRGQE64.js +8 -0
  120. package/dist/error-handler-GEQXZJ25.js +2 -0
  121. package/dist/lib-NJYCLW6W.js +22 -0
  122. package/dist/morph-ZJCCGFNC.js +30499 -0
  123. package/dist/multipart-parser-6HGDQWV7.js +3 -0
  124. package/dist/open-OD6DRFEG.js +2 -0
  125. package/dist/out-7KAQXZLP.js +2 -0
  126. package/dist/yoga.wasm +0 -0
  127. package/package.json +7 -3
@@ -0,0 +1,514 @@
1
+ import {
2
+ Link,
3
+ Form,
4
+ useParams,
5
+ useFetcher,
6
+ type FormProps,
7
+ } from '@remix-run/react';
8
+ import {Image, Money, Pagination} from '@shopify/hydrogen';
9
+ import React, {useRef, useEffect} from 'react';
10
+ import {applyTrackingParams} from '~/lib/search';
11
+
12
+ import type {
13
+ PredictiveProductFragment,
14
+ PredictiveCollectionFragment,
15
+ PredictiveArticleFragment,
16
+ SearchQuery,
17
+ } from 'storefrontapi.generated';
18
+
19
+ import type {PredictiveSearchAPILoader} from '../routes/api.predictive-search';
20
+
21
+ type PredicticeSearchResultItemImage =
22
+ | PredictiveCollectionFragment['image']
23
+ | PredictiveArticleFragment['image']
24
+ | PredictiveProductFragment['variants']['nodes'][0]['image'];
25
+
26
+ type PredictiveSearchResultItemPrice =
27
+ | PredictiveProductFragment['variants']['nodes'][0]['price'];
28
+
29
+ export type NormalizedPredictiveSearchResultItem = {
30
+ __typename: string | undefined;
31
+ handle: string;
32
+ id: string;
33
+ image?: PredicticeSearchResultItemImage;
34
+ price?: PredictiveSearchResultItemPrice;
35
+ styledTitle?: string;
36
+ title: string;
37
+ url: string;
38
+ };
39
+
40
+ export type NormalizedPredictiveSearchResults = Array<
41
+ | {type: 'queries'; items: Array<NormalizedPredictiveSearchResultItem>}
42
+ | {type: 'products'; items: Array<NormalizedPredictiveSearchResultItem>}
43
+ | {type: 'collections'; items: Array<NormalizedPredictiveSearchResultItem>}
44
+ | {type: 'pages'; items: Array<NormalizedPredictiveSearchResultItem>}
45
+ | {type: 'articles'; items: Array<NormalizedPredictiveSearchResultItem>}
46
+ >;
47
+
48
+ export type NormalizedPredictiveSearch = {
49
+ results: NormalizedPredictiveSearchResults;
50
+ totalResults: number;
51
+ };
52
+
53
+ type FetchSearchResultsReturn = {
54
+ searchResults: {
55
+ results: SearchQuery | null;
56
+ totalResults: number;
57
+ };
58
+ searchTerm: string;
59
+ };
60
+
61
+ export const NO_PREDICTIVE_SEARCH_RESULTS: NormalizedPredictiveSearchResults = [
62
+ {type: 'queries', items: []},
63
+ {type: 'products', items: []},
64
+ {type: 'collections', items: []},
65
+ {type: 'pages', items: []},
66
+ {type: 'articles', items: []},
67
+ ];
68
+
69
+ export function SearchForm({searchTerm}: {searchTerm: string}) {
70
+ const inputRef = useRef<HTMLInputElement | null>(null);
71
+
72
+ // focus the input when cmd+k is pressed
73
+ useEffect(() => {
74
+ function handleKeyDown(event: KeyboardEvent) {
75
+ if (event.key === 'k' && event.metaKey) {
76
+ event.preventDefault();
77
+ inputRef.current?.focus();
78
+ }
79
+
80
+ if (event.key === 'Escape') {
81
+ inputRef.current?.blur();
82
+ }
83
+ }
84
+
85
+ document.addEventListener('keydown', handleKeyDown);
86
+
87
+ return () => {
88
+ document.removeEventListener('keydown', handleKeyDown);
89
+ };
90
+ }, []);
91
+
92
+ return (
93
+ <Form method="get">
94
+ <input
95
+ defaultValue={searchTerm}
96
+ name="q"
97
+ placeholder="Search…"
98
+ ref={inputRef}
99
+ type="search"
100
+ />
101
+ &nbsp;
102
+ <button type="submit">Search</button>
103
+ </Form>
104
+ );
105
+ }
106
+
107
+ export function SearchResults({
108
+ results,
109
+ searchTerm,
110
+ }: Pick<FetchSearchResultsReturn['searchResults'], 'results'> & {
111
+ searchTerm: string;
112
+ }) {
113
+ if (!results) {
114
+ return null;
115
+ }
116
+ const keys = Object.keys(results) as Array<keyof typeof results>;
117
+ return (
118
+ <div>
119
+ {results &&
120
+ keys.map((type) => {
121
+ const resourceResults = results[type];
122
+
123
+ if (resourceResults.nodes[0]?.__typename === 'Page') {
124
+ const pageResults = resourceResults as SearchQuery['pages'];
125
+ return resourceResults.nodes.length ? (
126
+ <SearchResultPageGrid key="pages" pages={pageResults} />
127
+ ) : null;
128
+ }
129
+
130
+ if (resourceResults.nodes[0]?.__typename === 'Product') {
131
+ const productResults = resourceResults as SearchQuery['products'];
132
+ return resourceResults.nodes.length ? (
133
+ <SearchResultsProductsGrid
134
+ key="products"
135
+ products={productResults}
136
+ searchTerm={searchTerm}
137
+ />
138
+ ) : null;
139
+ }
140
+
141
+ if (resourceResults.nodes[0]?.__typename === 'Article') {
142
+ const articleResults = resourceResults as SearchQuery['articles'];
143
+ return resourceResults.nodes.length ? (
144
+ <SearchResultArticleGrid
145
+ key="articles"
146
+ articles={articleResults}
147
+ />
148
+ ) : null;
149
+ }
150
+
151
+ return null;
152
+ })}
153
+ </div>
154
+ );
155
+ }
156
+
157
+ function SearchResultsProductsGrid({
158
+ products,
159
+ searchTerm,
160
+ }: Pick<SearchQuery, 'products'> & {searchTerm: string}) {
161
+ return (
162
+ <div className="search-result">
163
+ <h2>Products</h2>
164
+ <Pagination connection={products}>
165
+ {({nodes, isLoading, NextLink, PreviousLink}) => {
166
+ const ItemsMarkup = nodes.map((product) => {
167
+ const trackingParams = applyTrackingParams(
168
+ product,
169
+ `q=${encodeURIComponent(searchTerm)}`,
170
+ );
171
+
172
+ return (
173
+ <div className="search-results-item" key={product.id}>
174
+ <Link
175
+ prefetch="intent"
176
+ to={`/products/${product.handle}${trackingParams}`}
177
+ >
178
+ {product.variants.nodes[0].image && (
179
+ <Image
180
+ data={product.variants.nodes[0].image}
181
+ alt={product.title}
182
+ width={50}
183
+ />
184
+ )}
185
+ <div>
186
+ <p>{product.title}</p>
187
+ <small>
188
+ <Money data={product.variants.nodes[0].price} />
189
+ </small>
190
+ </div>
191
+ </Link>
192
+ </div>
193
+ );
194
+ });
195
+ return (
196
+ <div>
197
+ <div>
198
+ <PreviousLink>
199
+ {isLoading ? 'Loading...' : <span>↑ Load previous</span>}
200
+ </PreviousLink>
201
+ </div>
202
+ <div>
203
+ {ItemsMarkup}
204
+ <br />
205
+ </div>
206
+ <div>
207
+ <NextLink>
208
+ {isLoading ? 'Loading...' : <span>Load more ↓</span>}
209
+ </NextLink>
210
+ </div>
211
+ </div>
212
+ );
213
+ }}
214
+ </Pagination>
215
+ <br />
216
+ </div>
217
+ );
218
+ }
219
+
220
+ function SearchResultPageGrid({pages}: Pick<SearchQuery, 'pages'>) {
221
+ return (
222
+ <div className="search-result">
223
+ <h2>Pages</h2>
224
+ <div>
225
+ {pages?.nodes?.map((page) => (
226
+ <div className="search-results-item" key={page.id}>
227
+ <Link prefetch="intent" to={`/pages/${page.handle}`}>
228
+ {page.title}
229
+ </Link>
230
+ </div>
231
+ ))}
232
+ </div>
233
+ <br />
234
+ </div>
235
+ );
236
+ }
237
+
238
+ function SearchResultArticleGrid({articles}: Pick<SearchQuery, 'articles'>) {
239
+ return (
240
+ <div className="search-result">
241
+ <h2>Articles</h2>
242
+ <div>
243
+ {articles?.nodes?.map((article) => (
244
+ <div className="search-results-item" key={article.id}>
245
+ <Link prefetch="intent" to={`/blogs/${article.handle}`}>
246
+ {article.title}
247
+ </Link>
248
+ </div>
249
+ ))}
250
+ </div>
251
+ <br />
252
+ </div>
253
+ );
254
+ }
255
+
256
+ export function NoSearchResults() {
257
+ return <p>No results, try a different search.</p>;
258
+ }
259
+
260
+ type ChildrenRenderProps = {
261
+ fetchResults: (event: React.ChangeEvent<HTMLInputElement>) => void;
262
+ fetcher: ReturnType<typeof useFetcher<PredictiveSearchAPILoader>>;
263
+ inputRef: React.MutableRefObject<HTMLInputElement | null>;
264
+ };
265
+
266
+ type SearchFromProps = {
267
+ action?: FormProps['action'];
268
+ className?: string;
269
+ children: (passedProps: ChildrenRenderProps) => React.ReactNode;
270
+ [key: string]: unknown;
271
+ };
272
+
273
+ /**
274
+ * Search form component that sends search requests to the `/search` route
275
+ **/
276
+ export function PredictiveSearchForm({
277
+ action,
278
+ children,
279
+ className = 'predictive-search-form',
280
+ ...props
281
+ }: SearchFromProps) {
282
+ const params = useParams();
283
+ const fetcher = useFetcher<PredictiveSearchAPILoader>({
284
+ key: 'search',
285
+ });
286
+ const inputRef = useRef<HTMLInputElement | null>(null);
287
+
288
+ function fetchResults(event: React.ChangeEvent<HTMLInputElement>) {
289
+ const searchAction = action ?? '/api/predictive-search';
290
+ const newSearchTerm = event.target.value || '';
291
+ const localizedAction = params.locale
292
+ ? `/${params.locale}${searchAction}`
293
+ : searchAction;
294
+
295
+ fetcher.submit(
296
+ {q: newSearchTerm, limit: '6'},
297
+ {method: 'GET', action: localizedAction},
298
+ );
299
+ }
300
+
301
+ // ensure the passed input has a type of search, because SearchResults
302
+ // will select the element based on the input
303
+ useEffect(() => {
304
+ inputRef?.current?.setAttribute('type', 'search');
305
+ }, []);
306
+
307
+ return (
308
+ <fetcher.Form
309
+ {...props}
310
+ className={className}
311
+ onSubmit={(event) => {
312
+ event.preventDefault();
313
+ event.stopPropagation();
314
+ if (!inputRef?.current || inputRef.current.value === '') {
315
+ return;
316
+ }
317
+ inputRef.current.blur();
318
+ }}
319
+ >
320
+ {children({fetchResults, inputRef, fetcher})}
321
+ </fetcher.Form>
322
+ );
323
+ }
324
+
325
+ export function PredictiveSearchResults() {
326
+ const {results, totalResults, searchInputRef, searchTerm, state} =
327
+ usePredictiveSearch();
328
+
329
+ function goToSearchResult(event: React.MouseEvent<HTMLAnchorElement>) {
330
+ if (!searchInputRef.current) return;
331
+ searchInputRef.current.blur();
332
+ searchInputRef.current.value = '';
333
+ // close the aside
334
+ window.location.href = event.currentTarget.href;
335
+ }
336
+
337
+ if (state === 'loading') {
338
+ return <div>Loading...</div>;
339
+ }
340
+
341
+ if (!totalResults) {
342
+ return <NoPredictiveSearchResults searchTerm={searchTerm} />;
343
+ }
344
+
345
+ return (
346
+ <div className="predictive-search-results">
347
+ <div>
348
+ {results.map(({type, items}) => (
349
+ <PredictiveSearchResult
350
+ goToSearchResult={goToSearchResult}
351
+ items={items}
352
+ key={type}
353
+ searchTerm={searchTerm}
354
+ type={type}
355
+ />
356
+ ))}
357
+ </div>
358
+ {searchTerm.current && (
359
+ <Link onClick={goToSearchResult} to={`/search?q=${searchTerm.current}`}>
360
+ <p>
361
+ View all results for <q>{searchTerm.current}</q>
362
+ &nbsp; →
363
+ </p>
364
+ </Link>
365
+ )}
366
+ </div>
367
+ );
368
+ }
369
+
370
+ function NoPredictiveSearchResults({
371
+ searchTerm,
372
+ }: {
373
+ searchTerm: React.MutableRefObject<string>;
374
+ }) {
375
+ if (!searchTerm.current) {
376
+ return null;
377
+ }
378
+ return (
379
+ <p>
380
+ No results found for <q>{searchTerm.current}</q>
381
+ </p>
382
+ );
383
+ }
384
+
385
+ type SearchResultTypeProps = {
386
+ goToSearchResult: (event: React.MouseEvent<HTMLAnchorElement>) => void;
387
+ items: NormalizedPredictiveSearchResultItem[];
388
+ searchTerm: UseSearchReturn['searchTerm'];
389
+ type: NormalizedPredictiveSearchResults[number]['type'];
390
+ };
391
+
392
+ function PredictiveSearchResult({
393
+ goToSearchResult,
394
+ items,
395
+ searchTerm,
396
+ type,
397
+ }: SearchResultTypeProps) {
398
+ const isSuggestions = type === 'queries';
399
+ const categoryUrl = `/search?q=${
400
+ searchTerm.current
401
+ }&type=${pluralToSingularSearchType(type)}`;
402
+
403
+ return (
404
+ <div className="predictive-search-result" key={type}>
405
+ <Link prefetch="intent" to={categoryUrl} onClick={goToSearchResult}>
406
+ <h5>{isSuggestions ? 'Suggestions' : type}</h5>
407
+ </Link>
408
+ <ul>
409
+ {items.map((item: NormalizedPredictiveSearchResultItem) => (
410
+ <SearchResultItem
411
+ goToSearchResult={goToSearchResult}
412
+ item={item}
413
+ key={item.id}
414
+ />
415
+ ))}
416
+ </ul>
417
+ </div>
418
+ );
419
+ }
420
+
421
+ type SearchResultItemProps = Pick<SearchResultTypeProps, 'goToSearchResult'> & {
422
+ item: NormalizedPredictiveSearchResultItem;
423
+ };
424
+
425
+ function SearchResultItem({goToSearchResult, item}: SearchResultItemProps) {
426
+ return (
427
+ <li className="predictive-search-result-item" key={item.id}>
428
+ <Link onClick={goToSearchResult} to={item.url}>
429
+ {item.image?.url && (
430
+ <Image
431
+ alt={item.image.altText ?? ''}
432
+ src={item.image.url}
433
+ width={50}
434
+ height={50}
435
+ />
436
+ )}
437
+ <div>
438
+ {item.styledTitle ? (
439
+ <div
440
+ dangerouslySetInnerHTML={{
441
+ __html: item.styledTitle,
442
+ }}
443
+ />
444
+ ) : (
445
+ <span>{item.title}</span>
446
+ )}
447
+ {item?.price && (
448
+ <small>
449
+ <Money data={item.price} />
450
+ </small>
451
+ )}
452
+ </div>
453
+ </Link>
454
+ </li>
455
+ );
456
+ }
457
+
458
+ type UseSearchReturn = NormalizedPredictiveSearch & {
459
+ searchInputRef: React.MutableRefObject<HTMLInputElement | null>;
460
+ searchTerm: React.MutableRefObject<string>;
461
+ state: ReturnType<typeof useFetcher<PredictiveSearchAPILoader>>['state'];
462
+ };
463
+
464
+ function usePredictiveSearch(): UseSearchReturn {
465
+ const searchFetcher = useFetcher<FetchSearchResultsReturn>({key: 'search'});
466
+ const searchTerm = useRef<string>('');
467
+ const searchInputRef = useRef<HTMLInputElement | null>(null);
468
+
469
+ if (searchFetcher?.state === 'loading') {
470
+ searchTerm.current = (searchFetcher.formData?.get('q') || '') as string;
471
+ }
472
+
473
+ const search = (searchFetcher?.data?.searchResults || {
474
+ results: NO_PREDICTIVE_SEARCH_RESULTS,
475
+ totalResults: 0,
476
+ }) as NormalizedPredictiveSearch;
477
+
478
+ // capture the search input element as a ref
479
+ useEffect(() => {
480
+ if (searchInputRef.current) return;
481
+ searchInputRef.current = document.querySelector('input[type="search"]');
482
+ }, []);
483
+
484
+ return {...search, searchInputRef, searchTerm, state: searchFetcher.state};
485
+ }
486
+
487
+ /**
488
+ * Converts a plural search type to a singular search type
489
+ *
490
+ * @example
491
+ * ```js
492
+ * pluralToSingularSearchType('articles'); // => 'ARTICLE'
493
+ * pluralToSingularSearchType(['articles', 'products']); // => 'ARTICLE,PRODUCT'
494
+ * ```
495
+ */
496
+ function pluralToSingularSearchType(
497
+ type:
498
+ | NormalizedPredictiveSearchResults[number]['type']
499
+ | Array<NormalizedPredictiveSearchResults[number]['type']>,
500
+ ) {
501
+ const plural = {
502
+ articles: 'ARTICLE',
503
+ collections: 'COLLECTION',
504
+ pages: 'PAGE',
505
+ products: 'PRODUCT',
506
+ queries: 'QUERY',
507
+ };
508
+
509
+ if (typeof type === 'string') {
510
+ return plural[type];
511
+ }
512
+
513
+ return type.map((t) => plural[t]).join(',');
514
+ }
@@ -0,0 +1,12 @@
1
+ import {RemixBrowser} from '@remix-run/react';
2
+ import {startTransition, StrictMode} from 'react';
3
+ import {hydrateRoot} from 'react-dom/client';
4
+
5
+ startTransition(() => {
6
+ hydrateRoot(
7
+ document,
8
+ <StrictMode>
9
+ <RemixBrowser />
10
+ </StrictMode>,
11
+ );
12
+ });
@@ -0,0 +1,47 @@
1
+ import type {EntryContext, AppLoadContext} from '@shopify/remix-oxygen';
2
+ import {RemixServer} from '@remix-run/react';
3
+ import isbot from 'isbot';
4
+ import {renderToReadableStream} from 'react-dom/server';
5
+ import {createContentSecurityPolicy} from '@shopify/hydrogen';
6
+
7
+ export default async function handleRequest(
8
+ request: Request,
9
+ responseStatusCode: number,
10
+ responseHeaders: Headers,
11
+ remixContext: EntryContext,
12
+ context: AppLoadContext,
13
+ ) {
14
+ const {nonce, header, NonceProvider} = createContentSecurityPolicy({
15
+ shop: {
16
+ checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
17
+ storeDomain: context.env.PUBLIC_STORE_DOMAIN,
18
+ }
19
+ });
20
+
21
+ const body = await renderToReadableStream(
22
+ <NonceProvider>
23
+ <RemixServer context={remixContext} url={request.url} />
24
+ </NonceProvider>,
25
+ {
26
+ nonce,
27
+ signal: request.signal,
28
+ onError(error) {
29
+ // eslint-disable-next-line no-console
30
+ console.error(error);
31
+ responseStatusCode = 500;
32
+ },
33
+ },
34
+ );
35
+
36
+ if (isbot(request.headers.get('user-agent'))) {
37
+ await body.allReady;
38
+ }
39
+
40
+ responseHeaders.set('Content-Type', 'text/html');
41
+ responseHeaders.set('Content-Security-Policy', header);
42
+
43
+ return new Response(body, {
44
+ headers: responseHeaders,
45
+ status: responseStatusCode,
46
+ });
47
+ }
@@ -0,0 +1,61 @@
1
+ // NOTE: https://shopify.dev/docs/api/customer/latest/mutations/customerAddressUpdate
2
+ export const UPDATE_ADDRESS_MUTATION = `#graphql
3
+ mutation customerAddressUpdate(
4
+ $address: CustomerAddressInput!
5
+ $addressId: ID!
6
+ $defaultAddress: Boolean
7
+ ) {
8
+ customerAddressUpdate(
9
+ address: $address
10
+ addressId: $addressId
11
+ defaultAddress: $defaultAddress
12
+ ) {
13
+ customerAddress {
14
+ id
15
+ }
16
+ userErrors {
17
+ code
18
+ field
19
+ message
20
+ }
21
+ }
22
+ }
23
+ ` as const;
24
+
25
+ // NOTE: https://shopify.dev/docs/api/customer/latest/mutations/customerAddressDelete
26
+ export const DELETE_ADDRESS_MUTATION = `#graphql
27
+ mutation customerAddressDelete(
28
+ $addressId: ID!,
29
+ ) {
30
+ customerAddressDelete(addressId: $addressId) {
31
+ deletedAddressId
32
+ userErrors {
33
+ code
34
+ field
35
+ message
36
+ }
37
+ }
38
+ }
39
+ ` as const;
40
+
41
+ // NOTE: https://shopify.dev/docs/api/customer/latest/mutations/customerAddressCreate
42
+ export const CREATE_ADDRESS_MUTATION = `#graphql
43
+ mutation customerAddressCreate(
44
+ $address: CustomerAddressInput!
45
+ $defaultAddress: Boolean
46
+ ) {
47
+ customerAddressCreate(
48
+ address: $address
49
+ defaultAddress: $defaultAddress
50
+ ) {
51
+ customerAddress {
52
+ id
53
+ }
54
+ userErrors {
55
+ code
56
+ field
57
+ message
58
+ }
59
+ }
60
+ }
61
+ ` as const;
@@ -0,0 +1,40 @@
1
+ // NOTE: https://shopify.dev/docs/api/customer/latest/objects/Customer
2
+ export const CUSTOMER_FRAGMENT = `#graphql
3
+ fragment Customer on Customer {
4
+ id
5
+ firstName
6
+ lastName
7
+ defaultAddress {
8
+ ...Address
9
+ }
10
+ addresses(first: 6) {
11
+ nodes {
12
+ ...Address
13
+ }
14
+ }
15
+ }
16
+ fragment Address on CustomerAddress {
17
+ id
18
+ formatted
19
+ firstName
20
+ lastName
21
+ company
22
+ address1
23
+ address2
24
+ territoryCode
25
+ zoneCode
26
+ city
27
+ zip
28
+ phoneNumber
29
+ }
30
+ ` as const;
31
+
32
+ // NOTE: https://shopify.dev/docs/api/customer/latest/queries/customer
33
+ export const CUSTOMER_DETAILS_QUERY = `#graphql
34
+ query CustomerDetails {
35
+ customer {
36
+ ...Customer
37
+ }
38
+ }
39
+ ${CUSTOMER_FRAGMENT}
40
+ ` as const;