@shopify/cli-hydrogen 5.0.2 → 5.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.
Files changed (249) hide show
  1. package/dist/commands/hydrogen/build.js +21 -6
  2. package/dist/commands/hydrogen/check.js +2 -2
  3. package/dist/commands/hydrogen/codegen-unstable.js +14 -25
  4. package/dist/commands/hydrogen/dev.js +55 -43
  5. package/dist/commands/hydrogen/env/list.js +25 -24
  6. package/dist/commands/hydrogen/env/list.test.js +46 -43
  7. package/dist/commands/hydrogen/env/pull.js +53 -25
  8. package/dist/commands/hydrogen/env/pull.test.js +123 -42
  9. package/dist/commands/hydrogen/generate/route.js +31 -132
  10. package/dist/commands/hydrogen/generate/route.test.js +34 -126
  11. package/dist/commands/hydrogen/init.js +46 -127
  12. package/dist/commands/hydrogen/init.test.js +352 -100
  13. package/dist/commands/hydrogen/link.js +70 -69
  14. package/dist/commands/hydrogen/link.test.js +72 -107
  15. package/dist/commands/hydrogen/list.js +22 -12
  16. package/dist/commands/hydrogen/list.test.js +51 -48
  17. package/dist/commands/hydrogen/login.js +31 -0
  18. package/dist/commands/hydrogen/logout.js +21 -0
  19. package/dist/commands/hydrogen/preview.js +1 -1
  20. package/dist/commands/hydrogen/setup/css.js +79 -0
  21. package/dist/commands/hydrogen/setup/markets.js +53 -0
  22. package/dist/commands/hydrogen/setup.js +133 -0
  23. package/dist/commands/hydrogen/shortcut.js +2 -45
  24. package/dist/commands/hydrogen/shortcut.test.js +10 -37
  25. package/dist/generator-templates/assets/css-modules/package.json +6 -0
  26. package/dist/generator-templates/assets/postcss/package.json +10 -0
  27. package/dist/generator-templates/assets/postcss/postcss.config.js +8 -0
  28. package/dist/generator-templates/assets/tailwind/package.json +13 -0
  29. package/dist/generator-templates/assets/tailwind/postcss.config.js +10 -0
  30. package/dist/generator-templates/assets/tailwind/tailwind.config.js +8 -0
  31. package/dist/generator-templates/assets/tailwind/tailwind.css +3 -0
  32. package/dist/generator-templates/assets/vanilla-extract/package.json +9 -0
  33. package/dist/generator-templates/starter/.eslintignore +5 -0
  34. package/dist/generator-templates/starter/.eslintrc.js +18 -0
  35. package/dist/generator-templates/starter/.graphqlrc.yml +1 -0
  36. package/dist/generator-templates/starter/README.md +40 -0
  37. package/dist/generator-templates/starter/app/components/Aside.tsx +47 -0
  38. package/dist/generator-templates/starter/app/components/Cart.tsx +340 -0
  39. package/dist/generator-templates/starter/app/components/Footer.tsx +99 -0
  40. package/dist/generator-templates/starter/app/components/Header.tsx +178 -0
  41. package/dist/generator-templates/starter/app/components/Layout.tsx +95 -0
  42. package/dist/generator-templates/starter/app/components/Search.tsx +480 -0
  43. package/dist/generator-templates/starter/app/entry.client.tsx +12 -0
  44. package/dist/generator-templates/starter/app/entry.server.tsx +33 -0
  45. package/dist/generator-templates/starter/app/root.tsx +264 -0
  46. package/dist/generator-templates/starter/app/routes/$.tsx +7 -0
  47. package/dist/generator-templates/{routes → starter/app/routes}/[robots.txt].tsx +47 -69
  48. package/dist/generator-templates/starter/app/routes/[sitemap.xml].tsx +174 -0
  49. package/dist/generator-templates/starter/app/routes/_index.tsx +145 -0
  50. package/dist/generator-templates/starter/app/routes/account.$.tsx +9 -0
  51. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +563 -0
  52. package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +309 -0
  53. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +196 -0
  54. package/dist/generator-templates/starter/app/routes/account.profile.tsx +289 -0
  55. package/dist/generator-templates/starter/app/routes/account.tsx +203 -0
  56. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +157 -0
  57. package/dist/generator-templates/starter/app/routes/account_.login.tsx +143 -0
  58. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +33 -0
  59. package/dist/generator-templates/starter/app/routes/account_.recover.tsx +124 -0
  60. package/dist/generator-templates/starter/app/routes/account_.register.tsx +207 -0
  61. package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +136 -0
  62. package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +342 -0
  63. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +88 -0
  64. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +162 -0
  65. package/dist/generator-templates/starter/app/routes/blogs._index.tsx +94 -0
  66. package/dist/generator-templates/starter/app/routes/cart.tsx +104 -0
  67. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +184 -0
  68. package/dist/generator-templates/starter/app/routes/collections._index.tsx +120 -0
  69. package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +57 -0
  70. package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +94 -0
  71. package/dist/generator-templates/starter/app/routes/policies._index.tsx +63 -0
  72. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +418 -0
  73. package/dist/generator-templates/starter/app/routes/search.tsx +168 -0
  74. package/dist/generator-templates/starter/app/styles/app.css +473 -0
  75. package/dist/generator-templates/starter/app/styles/reset.css +129 -0
  76. package/dist/generator-templates/starter/app/utils.ts +46 -0
  77. package/dist/generator-templates/starter/package.json +43 -0
  78. package/dist/generator-templates/starter/public/favicon.svg +28 -0
  79. package/dist/generator-templates/starter/remix.config.js +26 -0
  80. package/dist/generator-templates/starter/remix.env.d.ts +39 -0
  81. package/dist/generator-templates/starter/server.ts +253 -0
  82. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +1906 -0
  83. package/dist/generator-templates/starter/tsconfig.json +22 -0
  84. package/dist/lib/auth.js +123 -0
  85. package/dist/lib/auth.test.js +157 -0
  86. package/dist/lib/build.js +51 -0
  87. package/dist/lib/check-version.js +3 -3
  88. package/dist/lib/check-version.test.js +24 -0
  89. package/dist/lib/codegen.js +26 -17
  90. package/dist/lib/environment-variables.js +68 -0
  91. package/dist/lib/environment-variables.test.js +147 -0
  92. package/dist/lib/file.js +41 -0
  93. package/dist/lib/file.test.js +69 -0
  94. package/dist/lib/flags.js +39 -2
  95. package/dist/lib/format-code.js +26 -0
  96. package/dist/lib/gid.js +12 -0
  97. package/dist/lib/{graphql.test.js → gid.test.js} +1 -1
  98. package/dist/lib/graphql/admin/client.js +27 -0
  99. package/dist/lib/graphql/admin/client.test.js +51 -0
  100. package/dist/lib/graphql/admin/create-storefront.js +13 -15
  101. package/dist/lib/graphql/admin/create-storefront.test.js +64 -0
  102. package/dist/lib/graphql/admin/fetch-job.js +6 -15
  103. package/dist/lib/graphql/admin/link-storefront.js +7 -11
  104. package/dist/lib/graphql/admin/link-storefront.test.js +38 -0
  105. package/dist/lib/graphql/admin/list-environments.js +2 -2
  106. package/dist/lib/graphql/admin/list-environments.test.js +44 -0
  107. package/dist/lib/graphql/admin/list-storefronts.js +7 -11
  108. package/dist/lib/graphql/admin/list-storefronts.test.js +44 -0
  109. package/dist/lib/graphql/admin/pull-variables.js +3 -3
  110. package/dist/lib/graphql/admin/pull-variables.test.js +37 -0
  111. package/dist/lib/graphql/business-platform/user-account.js +83 -0
  112. package/dist/lib/graphql/business-platform/user-account.test.js +80 -0
  113. package/dist/lib/log.js +216 -9
  114. package/dist/lib/log.test.js +92 -0
  115. package/dist/lib/mini-oxygen.js +19 -9
  116. package/dist/lib/missing-routes.js +0 -2
  117. package/dist/lib/onboarding/common.js +456 -0
  118. package/dist/lib/onboarding/index.js +2 -0
  119. package/dist/lib/onboarding/local.js +229 -0
  120. package/dist/lib/onboarding/remote.js +89 -0
  121. package/dist/lib/remix-config.js +135 -0
  122. package/dist/lib/remix-version-check.js +51 -0
  123. package/dist/lib/remix-version-check.test.js +38 -0
  124. package/dist/lib/remix-version-interop.js +6 -6
  125. package/dist/lib/remix-version-interop.test.js +12 -2
  126. package/dist/lib/render-errors.js +13 -11
  127. package/dist/lib/setups/css/assets.js +89 -0
  128. package/dist/lib/setups/css/css-modules.js +22 -0
  129. package/dist/lib/setups/css/index.js +44 -0
  130. package/dist/lib/setups/css/postcss.js +34 -0
  131. package/dist/lib/setups/css/replacers.js +137 -0
  132. package/dist/lib/setups/css/tailwind.js +54 -0
  133. package/dist/lib/setups/css/vanilla-extract.js +22 -0
  134. package/dist/lib/setups/i18n/domains.test.js +25 -0
  135. package/dist/lib/setups/i18n/index.js +46 -0
  136. package/dist/lib/setups/i18n/replacers.js +227 -0
  137. package/dist/lib/setups/i18n/subdomains.test.js +25 -0
  138. package/dist/lib/setups/i18n/subfolders.test.js +25 -0
  139. package/dist/lib/setups/i18n/templates/domains.js +14 -0
  140. package/dist/lib/setups/i18n/templates/domains.ts +25 -0
  141. package/dist/lib/setups/i18n/templates/subdomains.js +14 -0
  142. package/dist/lib/setups/i18n/templates/subdomains.ts +24 -0
  143. package/dist/lib/setups/i18n/templates/subfolders.js +14 -0
  144. package/dist/lib/setups/i18n/templates/subfolders.ts +28 -0
  145. package/dist/lib/setups/routes/generate.js +244 -0
  146. package/dist/lib/setups/routes/generate.test.js +313 -0
  147. package/dist/lib/shell.js +52 -5
  148. package/dist/lib/shell.test.js +42 -16
  149. package/dist/lib/shopify-config.js +23 -18
  150. package/dist/lib/shopify-config.test.js +63 -73
  151. package/dist/lib/template-downloader.js +9 -7
  152. package/dist/lib/transpile-ts.js +9 -29
  153. package/dist/virtual-routes/routes/index.jsx +40 -19
  154. package/oclif.manifest.json +710 -1
  155. package/package.json +20 -21
  156. package/dist/commands/hydrogen/build.d.ts +0 -23
  157. package/dist/commands/hydrogen/check.d.ts +0 -15
  158. package/dist/commands/hydrogen/codegen-unstable.d.ts +0 -15
  159. package/dist/commands/hydrogen/dev.d.ts +0 -21
  160. package/dist/commands/hydrogen/env/list.d.ts +0 -18
  161. package/dist/commands/hydrogen/env/pull.d.ts +0 -22
  162. package/dist/commands/hydrogen/g.d.ts +0 -10
  163. package/dist/commands/hydrogen/generate/route.d.ts +0 -32
  164. package/dist/commands/hydrogen/generate/route.test.d.ts +0 -1
  165. package/dist/commands/hydrogen/generate/routes.d.ts +0 -16
  166. package/dist/commands/hydrogen/init.d.ts +0 -24
  167. package/dist/commands/hydrogen/init.test.d.ts +0 -1
  168. package/dist/commands/hydrogen/link.d.ts +0 -23
  169. package/dist/commands/hydrogen/link.test.d.ts +0 -1
  170. package/dist/commands/hydrogen/list.d.ts +0 -21
  171. package/dist/commands/hydrogen/list.test.d.ts +0 -1
  172. package/dist/commands/hydrogen/preview.d.ts +0 -17
  173. package/dist/commands/hydrogen/shortcut.d.ts +0 -9
  174. package/dist/commands/hydrogen/shortcut.test.d.ts +0 -1
  175. package/dist/commands/hydrogen/unlink.d.ts +0 -16
  176. package/dist/commands/hydrogen/unlink.test.d.ts +0 -1
  177. package/dist/create-app.d.ts +0 -1
  178. package/dist/generator-templates/routes/[sitemap.xml].tsx +0 -235
  179. package/dist/generator-templates/routes/account/login.tsx +0 -103
  180. package/dist/generator-templates/routes/account/register.tsx +0 -103
  181. package/dist/generator-templates/routes/cart.tsx +0 -81
  182. package/dist/generator-templates/routes/collections/$collectionHandle.tsx +0 -104
  183. package/dist/generator-templates/routes/collections/index.tsx +0 -102
  184. package/dist/generator-templates/routes/graphiql.tsx +0 -10
  185. package/dist/generator-templates/routes/index.tsx +0 -40
  186. package/dist/generator-templates/routes/pages/$pageHandle.tsx +0 -112
  187. package/dist/generator-templates/routes/policies/$policyHandle.tsx +0 -140
  188. package/dist/generator-templates/routes/policies/index.tsx +0 -117
  189. package/dist/generator-templates/routes/products/$productHandle.tsx +0 -92
  190. package/dist/hooks/init.d.ts +0 -5
  191. package/dist/lib/admin-session.d.ts +0 -6
  192. package/dist/lib/admin-session.js +0 -16
  193. package/dist/lib/admin-session.test.d.ts +0 -1
  194. package/dist/lib/admin-session.test.js +0 -27
  195. package/dist/lib/admin-urls.d.ts +0 -8
  196. package/dist/lib/check-lockfile.d.ts +0 -3
  197. package/dist/lib/check-lockfile.test.d.ts +0 -1
  198. package/dist/lib/check-version.d.ts +0 -16
  199. package/dist/lib/check-version.test.d.ts +0 -1
  200. package/dist/lib/codegen.d.ts +0 -26
  201. package/dist/lib/combined-environment-variables.d.ts +0 -8
  202. package/dist/lib/combined-environment-variables.js +0 -57
  203. package/dist/lib/combined-environment-variables.test.d.ts +0 -1
  204. package/dist/lib/combined-environment-variables.test.js +0 -111
  205. package/dist/lib/config.d.ts +0 -20
  206. package/dist/lib/config.js +0 -141
  207. package/dist/lib/flags.d.ts +0 -27
  208. package/dist/lib/flags.test.d.ts +0 -1
  209. package/dist/lib/graphql/admin/create-storefront.d.ts +0 -17
  210. package/dist/lib/graphql/admin/fetch-job.d.ts +0 -23
  211. package/dist/lib/graphql/admin/link-storefront.d.ts +0 -14
  212. package/dist/lib/graphql/admin/list-environments.d.ts +0 -21
  213. package/dist/lib/graphql/admin/list-storefronts.d.ts +0 -25
  214. package/dist/lib/graphql/admin/pull-variables.d.ts +0 -21
  215. package/dist/lib/graphql.d.ts +0 -21
  216. package/dist/lib/graphql.js +0 -18
  217. package/dist/lib/graphql.test.d.ts +0 -1
  218. package/dist/lib/log.d.ts +0 -6
  219. package/dist/lib/mini-oxygen.d.ts +0 -22
  220. package/dist/lib/missing-routes.d.ts +0 -8
  221. package/dist/lib/missing-routes.test.d.ts +0 -1
  222. package/dist/lib/missing-storefronts.d.ts +0 -5
  223. package/dist/lib/missing-storefronts.js +0 -18
  224. package/dist/lib/process.d.ts +0 -6
  225. package/dist/lib/pull-environment-variables.d.ts +0 -20
  226. package/dist/lib/pull-environment-variables.js +0 -57
  227. package/dist/lib/pull-environment-variables.test.d.ts +0 -1
  228. package/dist/lib/pull-environment-variables.test.js +0 -174
  229. package/dist/lib/remix-version-interop.d.ts +0 -11
  230. package/dist/lib/remix-version-interop.test.d.ts +0 -1
  231. package/dist/lib/render-errors.d.ts +0 -16
  232. package/dist/lib/shell.d.ts +0 -11
  233. package/dist/lib/shell.test.d.ts +0 -1
  234. package/dist/lib/shop.d.ts +0 -7
  235. package/dist/lib/shop.js +0 -32
  236. package/dist/lib/shop.test.d.ts +0 -1
  237. package/dist/lib/shop.test.js +0 -78
  238. package/dist/lib/shopify-config.d.ts +0 -35
  239. package/dist/lib/shopify-config.test.d.ts +0 -1
  240. package/dist/lib/string.d.ts +0 -3
  241. package/dist/lib/string.test.d.ts +0 -1
  242. package/dist/lib/template-downloader.d.ts +0 -6
  243. package/dist/lib/transpile-ts.d.ts +0 -16
  244. package/dist/lib/user-errors.d.ts +0 -9
  245. package/dist/lib/user-errors.js +0 -11
  246. package/dist/lib/virtual-routes.d.ts +0 -7
  247. package/dist/lib/virtual-routes.test.d.ts +0 -1
  248. /package/dist/{commands/hydrogen/env/list.test.d.ts → lib/setups/css/common.js} +0 -0
  249. /package/dist/{commands/hydrogen/env/pull.test.d.ts → lib/setups/i18n/mock-i18n-types.js} +0 -0
@@ -0,0 +1,480 @@
1
+ import {
2
+ useParams,
3
+ useFetcher,
4
+ Link,
5
+ Form,
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 {useFetchers} from '@remix-run/react';
11
+
12
+ import type {
13
+ PredictiveProductFragment,
14
+ PredictiveCollectionFragment,
15
+ PredictiveArticleFragment,
16
+ SearchQuery,
17
+ } from 'storefrontapi.generated';
18
+
19
+ type PredicticeSearchResultItemImage =
20
+ | PredictiveCollectionFragment['image']
21
+ | PredictiveArticleFragment['image']
22
+ | PredictiveProductFragment['variants']['nodes'][0]['image'];
23
+
24
+ type PredictiveSearchResultItemPrice =
25
+ | PredictiveProductFragment['variants']['nodes'][0]['price'];
26
+
27
+ export type NormalizedPredictiveSearchResultItem = {
28
+ __typename: string | undefined;
29
+ handle: string;
30
+ id: string;
31
+ image?: PredicticeSearchResultItemImage;
32
+ price?: PredictiveSearchResultItemPrice;
33
+ styledTitle?: string;
34
+ title: string;
35
+ url: string;
36
+ };
37
+
38
+ export type NormalizedPredictiveSearchResults = Array<
39
+ | {type: 'queries'; items: Array<NormalizedPredictiveSearchResultItem>}
40
+ | {type: 'products'; items: Array<NormalizedPredictiveSearchResultItem>}
41
+ | {type: 'collections'; items: Array<NormalizedPredictiveSearchResultItem>}
42
+ | {type: 'pages'; items: Array<NormalizedPredictiveSearchResultItem>}
43
+ | {type: 'articles'; items: Array<NormalizedPredictiveSearchResultItem>}
44
+ >;
45
+
46
+ export type NormalizedPredictiveSearch = {
47
+ results: NormalizedPredictiveSearchResults;
48
+ totalResults: number;
49
+ };
50
+
51
+ type FetchSearchResultsReturn = {
52
+ searchResults: {
53
+ results: SearchQuery | null;
54
+ totalResults: number;
55
+ };
56
+ searchTerm: string;
57
+ };
58
+
59
+ export const NO_PREDICTIVE_SEARCH_RESULTS: NormalizedPredictiveSearchResults = [
60
+ {type: 'queries', items: []},
61
+ {type: 'products', items: []},
62
+ {type: 'collections', items: []},
63
+ {type: 'pages', items: []},
64
+ {type: 'articles', items: []},
65
+ ];
66
+
67
+ export function SearchForm({searchTerm}: {searchTerm: string}) {
68
+ const inputRef = useRef<HTMLInputElement | null>(null);
69
+
70
+ // focus the input when cmd+k is pressed
71
+ useEffect(() => {
72
+ function handleKeyDown(event: KeyboardEvent) {
73
+ if (event.key === 'k' && event.metaKey) {
74
+ event.preventDefault();
75
+ inputRef.current?.focus();
76
+ }
77
+
78
+ if (event.key === 'Escape') {
79
+ inputRef.current?.blur();
80
+ }
81
+ }
82
+
83
+ document.addEventListener('keydown', handleKeyDown);
84
+
85
+ return () => {
86
+ document.removeEventListener('keydown', handleKeyDown);
87
+ };
88
+ }, []);
89
+
90
+ return (
91
+ <Form method="get">
92
+ <input
93
+ defaultValue={searchTerm}
94
+ name="q"
95
+ placeholder="Search…"
96
+ ref={inputRef}
97
+ type="search"
98
+ />
99
+ &nbsp;
100
+ <button type="submit">Search</button>
101
+ </Form>
102
+ );
103
+ }
104
+
105
+ export function SearchResults({
106
+ results,
107
+ }: Pick<FetchSearchResultsReturn['searchResults'], 'results'>) {
108
+ if (!results) {
109
+ return null;
110
+ }
111
+ const keys = Object.keys(results) as Array<keyof typeof results>;
112
+ return (
113
+ <div>
114
+ {results &&
115
+ keys.map((type) => {
116
+ const resourceResults = results[type];
117
+
118
+ if (resourceResults.nodes[0]?.__typename === 'Page') {
119
+ const pageResults = resourceResults as SearchQuery['pages'];
120
+ return resourceResults.nodes.length ? (
121
+ <SearchResultPageGrid key="pages" pages={pageResults} />
122
+ ) : null;
123
+ }
124
+
125
+ if (resourceResults.nodes[0]?.__typename === 'Product') {
126
+ const productResults = resourceResults as SearchQuery['products'];
127
+ return resourceResults.nodes.length ? (
128
+ <SearchResultsProductsGrid
129
+ key="products"
130
+ products={productResults}
131
+ />
132
+ ) : null;
133
+ }
134
+
135
+ if (resourceResults.nodes[0]?.__typename === 'Article') {
136
+ const articleResults = resourceResults as SearchQuery['articles'];
137
+ return resourceResults.nodes.length ? (
138
+ <SearchResultArticleGrid
139
+ key="articles"
140
+ articles={articleResults}
141
+ />
142
+ ) : null;
143
+ }
144
+
145
+ return null;
146
+ })}
147
+ </div>
148
+ );
149
+ }
150
+
151
+ function SearchResultsProductsGrid({products}: Pick<SearchQuery, 'products'>) {
152
+ return (
153
+ <div className="search-result">
154
+ <h3>Products</h3>
155
+ <Pagination connection={products}>
156
+ {({nodes, isLoading, NextLink, PreviousLink}) => {
157
+ const itemsMarkup = nodes.map((product) => (
158
+ <div className="search-results-item" key={product.id}>
159
+ <Link prefetch="intent" to={`/products/${product.handle}`}>
160
+ <span>{product.title}</span>
161
+ </Link>
162
+ </div>
163
+ ));
164
+ return (
165
+ <div>
166
+ <div>
167
+ <PreviousLink>
168
+ {isLoading ? 'Loading...' : <span>↑ Load previous</span>}
169
+ </PreviousLink>
170
+ </div>
171
+ <div>
172
+ {itemsMarkup}
173
+ <br />
174
+ </div>
175
+ <div>
176
+ <NextLink>
177
+ {isLoading ? 'Loading...' : <span>Load more ↓</span>}
178
+ </NextLink>
179
+ </div>
180
+ </div>
181
+ );
182
+ }}
183
+ </Pagination>
184
+ <br />
185
+ </div>
186
+ );
187
+ }
188
+
189
+ function SearchResultPageGrid({pages}: Pick<SearchQuery, 'pages'>) {
190
+ return (
191
+ <div className="search-result">
192
+ <h2>Pages</h2>
193
+ <div>
194
+ {pages?.nodes?.map((page) => (
195
+ <div className="search-results-item" key={page.id}>
196
+ <Link prefetch="intent" to={`/pages/${page.handle}`}>
197
+ {page.title}
198
+ </Link>
199
+ </div>
200
+ ))}
201
+ </div>
202
+ <br />
203
+ </div>
204
+ );
205
+ }
206
+
207
+ function SearchResultArticleGrid({articles}: Pick<SearchQuery, 'articles'>) {
208
+ return (
209
+ <div className="search-result">
210
+ <h2>Articles</h2>
211
+ <div>
212
+ {articles?.nodes?.map((article) => (
213
+ <div className="search-results-item" key={article.id}>
214
+ <Link prefetch="intent" to={`/blog/${article.handle}`}>
215
+ {article.title}
216
+ </Link>
217
+ </div>
218
+ ))}
219
+ </div>
220
+ <br />
221
+ </div>
222
+ );
223
+ }
224
+
225
+ export function NoSearchResults() {
226
+ return <p>No results, try a different search.</p>;
227
+ }
228
+
229
+ type ChildrenRenderProps = {
230
+ fetchResults: (event: React.ChangeEvent<HTMLInputElement>) => void;
231
+ fetcher: ReturnType<typeof useFetcher<NormalizedPredictiveSearchResults>>;
232
+ inputRef: React.MutableRefObject<HTMLInputElement | null>;
233
+ };
234
+
235
+ type SearchFromProps = {
236
+ action?: FormProps['action'];
237
+ method?: FormProps['method'];
238
+ className?: string;
239
+ children: (passedProps: ChildrenRenderProps) => React.ReactNode;
240
+ [key: string]: unknown;
241
+ };
242
+
243
+ /**
244
+ * Search form component that posts search requests to the `/search` route
245
+ **/
246
+ export function PredictiveSearchForm({
247
+ action,
248
+ children,
249
+ className = 'predictive-search-form',
250
+ method = 'POST',
251
+ ...props
252
+ }: SearchFromProps) {
253
+ const params = useParams();
254
+ const fetcher = useFetcher<NormalizedPredictiveSearchResults>();
255
+ const inputRef = useRef<HTMLInputElement | null>(null);
256
+
257
+ function fetchResults(event: React.ChangeEvent<HTMLInputElement>) {
258
+ const searchAction = action ?? '/api/predictive-search';
259
+ const localizedAction = params.locale
260
+ ? `/${params.locale}${searchAction}`
261
+ : searchAction;
262
+ const newSearchTerm = event.target.value || '';
263
+ fetcher.submit(
264
+ {q: newSearchTerm, limit: '6'},
265
+ {method, action: localizedAction},
266
+ );
267
+ }
268
+
269
+ // ensure the passed input has a type of search, because SearchResults
270
+ // will select the element based on the input
271
+ useEffect(() => {
272
+ inputRef?.current?.setAttribute('type', 'search');
273
+ }, []);
274
+
275
+ return (
276
+ <fetcher.Form
277
+ {...props}
278
+ className={className}
279
+ onSubmit={(event) => {
280
+ event.preventDefault();
281
+ event.stopPropagation();
282
+ if (!inputRef?.current || inputRef.current.value === '') {
283
+ return;
284
+ }
285
+ inputRef.current.blur();
286
+ }}
287
+ >
288
+ {children({fetchResults, inputRef, fetcher})}
289
+ </fetcher.Form>
290
+ );
291
+ }
292
+
293
+ export function PredictiveSearchResults() {
294
+ const {results, totalResults, searchInputRef, searchTerm} =
295
+ usePredictiveSearch();
296
+
297
+ function goToSearchResult(event: React.MouseEvent<HTMLAnchorElement>) {
298
+ if (!searchInputRef.current) return;
299
+ searchInputRef.current.blur();
300
+ searchInputRef.current.value = '';
301
+ // close the aside
302
+ window.location.href = event.currentTarget.href;
303
+ }
304
+
305
+ if (!totalResults) {
306
+ return <NoPredictiveSearchResults searchTerm={searchTerm} />;
307
+ }
308
+ return (
309
+ <div className="predictive-search-results">
310
+ <div>
311
+ {results.map(({type, items}) => (
312
+ <PredictiveSearchResult
313
+ goToSearchResult={goToSearchResult}
314
+ items={items}
315
+ key={type}
316
+ searchTerm={searchTerm}
317
+ type={type}
318
+ />
319
+ ))}
320
+ </div>
321
+ {/* view all results /search?q=term */}
322
+ {searchTerm.current && (
323
+ <Link onClick={goToSearchResult} to={`/search?q=${searchTerm.current}`}>
324
+ <p>
325
+ View all results for <q>{searchTerm.current}</q>
326
+ &nbsp; →
327
+ </p>
328
+ </Link>
329
+ )}
330
+ </div>
331
+ );
332
+ }
333
+
334
+ function NoPredictiveSearchResults({
335
+ searchTerm,
336
+ }: {
337
+ searchTerm: React.MutableRefObject<string>;
338
+ }) {
339
+ if (!searchTerm.current) {
340
+ return null;
341
+ }
342
+ return (
343
+ <p>
344
+ No results found for <q>{searchTerm.current}</q>
345
+ </p>
346
+ );
347
+ }
348
+
349
+ type SearchResultTypeProps = {
350
+ goToSearchResult: (event: React.MouseEvent<HTMLAnchorElement>) => void;
351
+ items: NormalizedPredictiveSearchResultItem[];
352
+ searchTerm: UseSearchReturn['searchTerm'];
353
+ type: NormalizedPredictiveSearchResults[number]['type'];
354
+ };
355
+
356
+ function PredictiveSearchResult({
357
+ goToSearchResult,
358
+ items,
359
+ searchTerm,
360
+ type,
361
+ }: SearchResultTypeProps) {
362
+ const isSuggestions = type === 'queries';
363
+ const categoryUrl = `/search?q=${
364
+ searchTerm.current
365
+ }&type=${pluralToSingularSearchType(type)}`;
366
+
367
+ return (
368
+ <div className="predictive-search-result" key={type}>
369
+ <Link prefetch="intent" to={categoryUrl} onClick={goToSearchResult}>
370
+ <h5>{isSuggestions ? 'Suggestions' : type}</h5>
371
+ </Link>
372
+ <ul>
373
+ {items.map((item: NormalizedPredictiveSearchResultItem) => (
374
+ <SearchResultItem
375
+ goToSearchResult={goToSearchResult}
376
+ item={item}
377
+ key={item.id}
378
+ />
379
+ ))}
380
+ </ul>
381
+ </div>
382
+ );
383
+ }
384
+
385
+ type SearchResultItemProps = Pick<SearchResultTypeProps, 'goToSearchResult'> & {
386
+ item: NormalizedPredictiveSearchResultItem;
387
+ };
388
+
389
+ function SearchResultItem({goToSearchResult, item}: SearchResultItemProps) {
390
+ return (
391
+ <li className="predictive-search-result-item" key={item.id}>
392
+ <Link onClick={goToSearchResult} to={item.url}>
393
+ {item.image?.url && (
394
+ <Image
395
+ alt={item.image.altText ?? ''}
396
+ src={item.image.url}
397
+ width={50}
398
+ height={50}
399
+ />
400
+ )}
401
+ <div>
402
+ {item.styledTitle ? (
403
+ <div
404
+ dangerouslySetInnerHTML={{
405
+ __html: item.styledTitle,
406
+ }}
407
+ />
408
+ ) : (
409
+ <span>{item.title}</span>
410
+ )}
411
+ {item?.price && (
412
+ <small>
413
+ <Money data={item.price} />
414
+ </small>
415
+ )}
416
+ </div>
417
+ </Link>
418
+ </li>
419
+ );
420
+ }
421
+
422
+ type UseSearchReturn = NormalizedPredictiveSearch & {
423
+ searchInputRef: React.MutableRefObject<HTMLInputElement | null>;
424
+ searchTerm: React.MutableRefObject<string>;
425
+ };
426
+
427
+ function usePredictiveSearch(): UseSearchReturn {
428
+ const fetchers = useFetchers();
429
+ const searchTerm = useRef<string>('');
430
+ const searchInputRef = useRef<HTMLInputElement | null>(null);
431
+ const searchFetcher = fetchers.find((fetcher) => fetcher.data?.searchResults);
432
+
433
+ if (searchFetcher?.state === 'loading') {
434
+ searchTerm.current = (searchFetcher.formData?.get('q') || '') as string;
435
+ }
436
+
437
+ const search = (searchFetcher?.data?.searchResults || {
438
+ results: NO_PREDICTIVE_SEARCH_RESULTS,
439
+ totalResults: 0,
440
+ }) as NormalizedPredictiveSearch;
441
+
442
+ // capture the search input element as a ref
443
+ useEffect(() => {
444
+ if (searchInputRef.current) return;
445
+ searchInputRef.current = document.querySelector('input[type="search"]');
446
+ }, []);
447
+
448
+ return {...search, searchInputRef, searchTerm};
449
+ }
450
+
451
+ /**
452
+ * Converts a plural search type to a singular search type
453
+ * @param type - The plural search type
454
+ * @returns The singular search type
455
+ *
456
+ * @example
457
+ * ```ts
458
+ * pluralToSingularSearchType('articles') // => 'ARTICLE'
459
+ * pluralToSingularSearchType(['articles', 'products']) // => 'ARTICLE,PRODUCT'
460
+ * ```
461
+ */
462
+ function pluralToSingularSearchType(
463
+ type:
464
+ | NormalizedPredictiveSearchResults[number]['type']
465
+ | Array<NormalizedPredictiveSearchResults[number]['type']>,
466
+ ) {
467
+ const plural = {
468
+ articles: 'ARTICLE',
469
+ collections: 'COLLECTION',
470
+ pages: 'PAGE',
471
+ products: 'PRODUCT',
472
+ queries: 'QUERY',
473
+ };
474
+
475
+ if (typeof type === 'string') {
476
+ return plural[type];
477
+ }
478
+
479
+ return type.map((t) => plural[t]).join(',');
480
+ }
@@ -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,33 @@
1
+ import type {EntryContext} from '@shopify/remix-oxygen';
2
+ import {RemixServer} from '@remix-run/react';
3
+ import isbot from 'isbot';
4
+ import {renderToReadableStream} from 'react-dom/server';
5
+
6
+ export default async function handleRequest(
7
+ request: Request,
8
+ responseStatusCode: number,
9
+ responseHeaders: Headers,
10
+ remixContext: EntryContext,
11
+ ) {
12
+ const body = await renderToReadableStream(
13
+ <RemixServer context={remixContext} url={request.url} />,
14
+ {
15
+ signal: request.signal,
16
+ onError(error) {
17
+ // eslint-disable-next-line no-console
18
+ console.error(error);
19
+ responseStatusCode = 500;
20
+ },
21
+ },
22
+ );
23
+
24
+ if (isbot(request.headers.get('user-agent'))) {
25
+ await body.allReady;
26
+ }
27
+
28
+ responseHeaders.set('Content-Type', 'text/html');
29
+ return new Response(body, {
30
+ headers: responseHeaders,
31
+ status: responseStatusCode,
32
+ });
33
+ }