@shopify/cli-hydrogen 5.0.2 → 5.1.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 (243) hide show
  1. package/dist/commands/hydrogen/build.js +16 -2
  2. package/dist/commands/hydrogen/codegen-unstable.js +13 -24
  3. package/dist/commands/hydrogen/dev.js +45 -39
  4. package/dist/commands/hydrogen/env/list.js +25 -24
  5. package/dist/commands/hydrogen/env/list.test.js +46 -43
  6. package/dist/commands/hydrogen/env/pull.js +53 -25
  7. package/dist/commands/hydrogen/env/pull.test.js +123 -42
  8. package/dist/commands/hydrogen/generate/route.js +31 -132
  9. package/dist/commands/hydrogen/generate/route.test.js +34 -126
  10. package/dist/commands/hydrogen/init.js +46 -127
  11. package/dist/commands/hydrogen/init.test.js +352 -100
  12. package/dist/commands/hydrogen/link.js +70 -69
  13. package/dist/commands/hydrogen/link.test.js +72 -107
  14. package/dist/commands/hydrogen/list.js +22 -12
  15. package/dist/commands/hydrogen/list.test.js +51 -48
  16. package/dist/commands/hydrogen/login.js +31 -0
  17. package/dist/commands/hydrogen/logout.js +21 -0
  18. package/dist/commands/hydrogen/setup/css.js +79 -0
  19. package/dist/commands/hydrogen/setup/markets.js +53 -0
  20. package/dist/commands/hydrogen/setup.js +133 -0
  21. package/dist/commands/hydrogen/shortcut.js +2 -45
  22. package/dist/commands/hydrogen/shortcut.test.js +10 -37
  23. package/dist/generator-templates/assets/css-modules/package.json +6 -0
  24. package/dist/generator-templates/assets/postcss/package.json +10 -0
  25. package/dist/generator-templates/assets/postcss/postcss.config.js +8 -0
  26. package/dist/generator-templates/assets/tailwind/package.json +13 -0
  27. package/dist/generator-templates/assets/tailwind/postcss.config.js +10 -0
  28. package/dist/generator-templates/assets/tailwind/tailwind.config.js +8 -0
  29. package/dist/generator-templates/assets/tailwind/tailwind.css +3 -0
  30. package/dist/generator-templates/assets/vanilla-extract/package.json +9 -0
  31. package/dist/generator-templates/starter/.eslintignore +5 -0
  32. package/dist/generator-templates/starter/.eslintrc.js +18 -0
  33. package/dist/generator-templates/starter/.graphqlrc.yml +1 -0
  34. package/dist/generator-templates/starter/README.md +40 -0
  35. package/dist/generator-templates/starter/app/components/Aside.tsx +47 -0
  36. package/dist/generator-templates/starter/app/components/Cart.tsx +340 -0
  37. package/dist/generator-templates/starter/app/components/Footer.tsx +99 -0
  38. package/dist/generator-templates/starter/app/components/Header.tsx +178 -0
  39. package/dist/generator-templates/starter/app/components/Layout.tsx +95 -0
  40. package/dist/generator-templates/starter/app/components/Search.tsx +480 -0
  41. package/dist/generator-templates/starter/app/entry.client.tsx +12 -0
  42. package/dist/generator-templates/starter/app/entry.server.tsx +33 -0
  43. package/dist/generator-templates/starter/app/root.tsx +270 -0
  44. package/dist/generator-templates/starter/app/routes/$.tsx +7 -0
  45. package/dist/generator-templates/{routes → starter/app/routes}/[robots.txt].tsx +47 -69
  46. package/dist/generator-templates/starter/app/routes/[sitemap.xml].tsx +174 -0
  47. package/dist/generator-templates/starter/app/routes/_index.tsx +145 -0
  48. package/dist/generator-templates/starter/app/routes/account.$.tsx +9 -0
  49. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +563 -0
  50. package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +309 -0
  51. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +196 -0
  52. package/dist/generator-templates/starter/app/routes/account.profile.tsx +289 -0
  53. package/dist/generator-templates/starter/app/routes/account.tsx +203 -0
  54. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +157 -0
  55. package/dist/generator-templates/starter/app/routes/account_.login.tsx +143 -0
  56. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +33 -0
  57. package/dist/generator-templates/starter/app/routes/account_.recover.tsx +124 -0
  58. package/dist/generator-templates/starter/app/routes/account_.register.tsx +207 -0
  59. package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +136 -0
  60. package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +342 -0
  61. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +88 -0
  62. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +162 -0
  63. package/dist/generator-templates/starter/app/routes/blogs._index.tsx +94 -0
  64. package/dist/generator-templates/starter/app/routes/cart.tsx +104 -0
  65. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +184 -0
  66. package/dist/generator-templates/starter/app/routes/collections._index.tsx +120 -0
  67. package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +57 -0
  68. package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +94 -0
  69. package/dist/generator-templates/starter/app/routes/policies._index.tsx +63 -0
  70. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +418 -0
  71. package/dist/generator-templates/starter/app/routes/search.tsx +168 -0
  72. package/dist/generator-templates/starter/app/styles/app.css +473 -0
  73. package/dist/generator-templates/starter/app/styles/reset.css +129 -0
  74. package/dist/generator-templates/starter/app/utils.ts +46 -0
  75. package/dist/generator-templates/starter/package.json +43 -0
  76. package/dist/generator-templates/starter/public/favicon.svg +28 -0
  77. package/dist/generator-templates/starter/remix.config.js +26 -0
  78. package/dist/generator-templates/starter/remix.env.d.ts +39 -0
  79. package/dist/generator-templates/starter/server.ts +253 -0
  80. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +1906 -0
  81. package/dist/generator-templates/starter/tsconfig.json +22 -0
  82. package/dist/lib/auth.js +123 -0
  83. package/dist/lib/auth.test.js +157 -0
  84. package/dist/lib/build.js +51 -0
  85. package/dist/lib/check-version.js +3 -3
  86. package/dist/lib/check-version.test.js +24 -0
  87. package/dist/lib/codegen.js +26 -17
  88. package/dist/lib/environment-variables.js +68 -0
  89. package/dist/lib/environment-variables.test.js +147 -0
  90. package/dist/lib/file.js +41 -0
  91. package/dist/lib/file.test.js +69 -0
  92. package/dist/lib/flags.js +39 -2
  93. package/dist/lib/format-code.js +26 -0
  94. package/dist/lib/gid.js +12 -0
  95. package/dist/lib/{graphql.test.js → gid.test.js} +1 -1
  96. package/dist/lib/graphql/admin/client.js +27 -0
  97. package/dist/lib/graphql/admin/client.test.js +51 -0
  98. package/dist/lib/graphql/admin/create-storefront.js +13 -15
  99. package/dist/lib/graphql/admin/create-storefront.test.js +64 -0
  100. package/dist/lib/graphql/admin/fetch-job.js +6 -15
  101. package/dist/lib/graphql/admin/link-storefront.js +7 -11
  102. package/dist/lib/graphql/admin/link-storefront.test.js +38 -0
  103. package/dist/lib/graphql/admin/list-environments.js +2 -2
  104. package/dist/lib/graphql/admin/list-environments.test.js +44 -0
  105. package/dist/lib/graphql/admin/list-storefronts.js +7 -11
  106. package/dist/lib/graphql/admin/list-storefronts.test.js +44 -0
  107. package/dist/lib/graphql/admin/pull-variables.js +3 -3
  108. package/dist/lib/graphql/admin/pull-variables.test.js +37 -0
  109. package/dist/lib/graphql/business-platform/user-account.js +83 -0
  110. package/dist/lib/graphql/business-platform/user-account.test.js +80 -0
  111. package/dist/lib/log.js +185 -9
  112. package/dist/lib/log.test.js +92 -0
  113. package/dist/lib/mini-oxygen.js +19 -9
  114. package/dist/lib/missing-routes.js +0 -2
  115. package/dist/lib/onboarding/common.js +456 -0
  116. package/dist/lib/onboarding/index.js +2 -0
  117. package/dist/lib/onboarding/local.js +229 -0
  118. package/dist/lib/onboarding/remote.js +89 -0
  119. package/dist/lib/remix-version-interop.js +5 -5
  120. package/dist/lib/remix-version-interop.test.js +11 -1
  121. package/dist/lib/render-errors.js +13 -11
  122. package/dist/lib/setups/css/assets.js +89 -0
  123. package/dist/lib/setups/css/css-modules.js +22 -0
  124. package/dist/lib/setups/css/index.js +44 -0
  125. package/dist/lib/setups/css/postcss.js +34 -0
  126. package/dist/lib/setups/css/replacers.js +137 -0
  127. package/dist/lib/setups/css/tailwind.js +54 -0
  128. package/dist/lib/setups/css/vanilla-extract.js +22 -0
  129. package/dist/lib/setups/i18n/domains.test.js +25 -0
  130. package/dist/lib/setups/i18n/index.js +46 -0
  131. package/dist/lib/setups/i18n/replacers.js +227 -0
  132. package/dist/lib/setups/i18n/subdomains.test.js +25 -0
  133. package/dist/lib/setups/i18n/subfolders.test.js +25 -0
  134. package/dist/lib/setups/i18n/templates/domains.js +14 -0
  135. package/dist/lib/setups/i18n/templates/domains.ts +25 -0
  136. package/dist/lib/setups/i18n/templates/subdomains.js +14 -0
  137. package/dist/lib/setups/i18n/templates/subdomains.ts +24 -0
  138. package/dist/lib/setups/i18n/templates/subfolders.js +14 -0
  139. package/dist/lib/setups/i18n/templates/subfolders.ts +28 -0
  140. package/dist/lib/setups/routes/generate.js +244 -0
  141. package/dist/lib/setups/routes/generate.test.js +313 -0
  142. package/dist/lib/shell.js +52 -5
  143. package/dist/lib/shell.test.js +42 -16
  144. package/dist/lib/shopify-config.js +23 -18
  145. package/dist/lib/shopify-config.test.js +63 -73
  146. package/dist/lib/template-downloader.js +9 -7
  147. package/dist/lib/transpile-ts.js +9 -29
  148. package/dist/virtual-routes/routes/index.jsx +40 -19
  149. package/oclif.manifest.json +710 -1
  150. package/package.json +16 -16
  151. package/dist/commands/hydrogen/build.d.ts +0 -23
  152. package/dist/commands/hydrogen/check.d.ts +0 -15
  153. package/dist/commands/hydrogen/codegen-unstable.d.ts +0 -15
  154. package/dist/commands/hydrogen/dev.d.ts +0 -21
  155. package/dist/commands/hydrogen/env/list.d.ts +0 -18
  156. package/dist/commands/hydrogen/env/pull.d.ts +0 -22
  157. package/dist/commands/hydrogen/g.d.ts +0 -10
  158. package/dist/commands/hydrogen/generate/route.d.ts +0 -32
  159. package/dist/commands/hydrogen/generate/route.test.d.ts +0 -1
  160. package/dist/commands/hydrogen/generate/routes.d.ts +0 -16
  161. package/dist/commands/hydrogen/init.d.ts +0 -24
  162. package/dist/commands/hydrogen/init.test.d.ts +0 -1
  163. package/dist/commands/hydrogen/link.d.ts +0 -23
  164. package/dist/commands/hydrogen/link.test.d.ts +0 -1
  165. package/dist/commands/hydrogen/list.d.ts +0 -21
  166. package/dist/commands/hydrogen/list.test.d.ts +0 -1
  167. package/dist/commands/hydrogen/preview.d.ts +0 -17
  168. package/dist/commands/hydrogen/shortcut.d.ts +0 -9
  169. package/dist/commands/hydrogen/shortcut.test.d.ts +0 -1
  170. package/dist/commands/hydrogen/unlink.d.ts +0 -16
  171. package/dist/commands/hydrogen/unlink.test.d.ts +0 -1
  172. package/dist/create-app.d.ts +0 -1
  173. package/dist/generator-templates/routes/[sitemap.xml].tsx +0 -235
  174. package/dist/generator-templates/routes/account/login.tsx +0 -103
  175. package/dist/generator-templates/routes/account/register.tsx +0 -103
  176. package/dist/generator-templates/routes/cart.tsx +0 -81
  177. package/dist/generator-templates/routes/collections/$collectionHandle.tsx +0 -104
  178. package/dist/generator-templates/routes/collections/index.tsx +0 -102
  179. package/dist/generator-templates/routes/graphiql.tsx +0 -10
  180. package/dist/generator-templates/routes/index.tsx +0 -40
  181. package/dist/generator-templates/routes/pages/$pageHandle.tsx +0 -112
  182. package/dist/generator-templates/routes/policies/$policyHandle.tsx +0 -140
  183. package/dist/generator-templates/routes/policies/index.tsx +0 -117
  184. package/dist/generator-templates/routes/products/$productHandle.tsx +0 -92
  185. package/dist/hooks/init.d.ts +0 -5
  186. package/dist/lib/admin-session.d.ts +0 -6
  187. package/dist/lib/admin-session.js +0 -16
  188. package/dist/lib/admin-session.test.d.ts +0 -1
  189. package/dist/lib/admin-session.test.js +0 -27
  190. package/dist/lib/admin-urls.d.ts +0 -8
  191. package/dist/lib/check-lockfile.d.ts +0 -3
  192. package/dist/lib/check-lockfile.test.d.ts +0 -1
  193. package/dist/lib/check-version.d.ts +0 -16
  194. package/dist/lib/check-version.test.d.ts +0 -1
  195. package/dist/lib/codegen.d.ts +0 -26
  196. package/dist/lib/combined-environment-variables.d.ts +0 -8
  197. package/dist/lib/combined-environment-variables.js +0 -57
  198. package/dist/lib/combined-environment-variables.test.d.ts +0 -1
  199. package/dist/lib/combined-environment-variables.test.js +0 -111
  200. package/dist/lib/config.d.ts +0 -20
  201. package/dist/lib/flags.d.ts +0 -27
  202. package/dist/lib/flags.test.d.ts +0 -1
  203. package/dist/lib/graphql/admin/create-storefront.d.ts +0 -17
  204. package/dist/lib/graphql/admin/fetch-job.d.ts +0 -23
  205. package/dist/lib/graphql/admin/link-storefront.d.ts +0 -14
  206. package/dist/lib/graphql/admin/list-environments.d.ts +0 -21
  207. package/dist/lib/graphql/admin/list-storefronts.d.ts +0 -25
  208. package/dist/lib/graphql/admin/pull-variables.d.ts +0 -21
  209. package/dist/lib/graphql.d.ts +0 -21
  210. package/dist/lib/graphql.js +0 -18
  211. package/dist/lib/graphql.test.d.ts +0 -1
  212. package/dist/lib/log.d.ts +0 -6
  213. package/dist/lib/mini-oxygen.d.ts +0 -22
  214. package/dist/lib/missing-routes.d.ts +0 -8
  215. package/dist/lib/missing-routes.test.d.ts +0 -1
  216. package/dist/lib/missing-storefronts.d.ts +0 -5
  217. package/dist/lib/missing-storefronts.js +0 -18
  218. package/dist/lib/process.d.ts +0 -6
  219. package/dist/lib/pull-environment-variables.d.ts +0 -20
  220. package/dist/lib/pull-environment-variables.js +0 -57
  221. package/dist/lib/pull-environment-variables.test.d.ts +0 -1
  222. package/dist/lib/pull-environment-variables.test.js +0 -174
  223. package/dist/lib/remix-version-interop.d.ts +0 -11
  224. package/dist/lib/remix-version-interop.test.d.ts +0 -1
  225. package/dist/lib/render-errors.d.ts +0 -16
  226. package/dist/lib/shell.d.ts +0 -11
  227. package/dist/lib/shell.test.d.ts +0 -1
  228. package/dist/lib/shop.d.ts +0 -7
  229. package/dist/lib/shop.js +0 -32
  230. package/dist/lib/shop.test.d.ts +0 -1
  231. package/dist/lib/shop.test.js +0 -78
  232. package/dist/lib/shopify-config.d.ts +0 -35
  233. package/dist/lib/shopify-config.test.d.ts +0 -1
  234. package/dist/lib/string.d.ts +0 -3
  235. package/dist/lib/string.test.d.ts +0 -1
  236. package/dist/lib/template-downloader.d.ts +0 -6
  237. package/dist/lib/transpile-ts.d.ts +0 -16
  238. package/dist/lib/user-errors.d.ts +0 -9
  239. package/dist/lib/user-errors.js +0 -11
  240. package/dist/lib/virtual-routes.d.ts +0 -7
  241. package/dist/lib/virtual-routes.test.d.ts +0 -1
  242. /package/dist/{commands/hydrogen/env/list.test.d.ts → lib/setups/css/common.js} +0 -0
  243. /package/dist/{commands/hydrogen/env/pull.test.d.ts → lib/setups/i18n/mock-i18n-types.js} +0 -0
@@ -0,0 +1,418 @@
1
+ import {Suspense} from 'react';
2
+ import type {V2_MetaFunction} from '@shopify/remix-oxygen';
3
+ import {defer, redirect, type LoaderArgs} from '@shopify/remix-oxygen';
4
+ import type {FetcherWithComponents} from '@remix-run/react';
5
+ import {Await, Link, useLoaderData} from '@remix-run/react';
6
+ import type {
7
+ ProductFragment,
8
+ ProductVariantsQuery,
9
+ ProductVariantFragment,
10
+ } from 'storefrontapi.generated';
11
+
12
+ import {
13
+ Image,
14
+ Money,
15
+ VariantSelector,
16
+ type VariantOption,
17
+ getSelectedProductOptions,
18
+ CartForm,
19
+ } from '@shopify/hydrogen';
20
+ import type {CartLineInput} from '@shopify/hydrogen/storefront-api-types';
21
+ import {getVariantUrl} from '~/utils';
22
+
23
+ export const meta: V2_MetaFunction = ({data}) => {
24
+ return [{title: `Hydrogen | ${data.product.title}`}];
25
+ };
26
+
27
+ export async function loader({params, request, context}: LoaderArgs) {
28
+ const {handle} = params;
29
+ const {storefront} = context;
30
+
31
+ const selectedOptions = getSelectedProductOptions(request).filter(
32
+ (option) =>
33
+ // Filter out Shopify predictive search query params
34
+ !option.name.startsWith('_sid') &&
35
+ !option.name.startsWith('_pos') &&
36
+ !option.name.startsWith('_psq') &&
37
+ !option.name.startsWith('_ss') &&
38
+ !option.name.startsWith('_v'),
39
+ );
40
+
41
+ if (!handle) {
42
+ throw new Error('Expected product handle to be defined');
43
+ }
44
+
45
+ // await the query for the critical product data
46
+ const {product} = await storefront.query(PRODUCT_QUERY, {
47
+ variables: {handle, selectedOptions},
48
+ });
49
+
50
+ // In order to show which variants are available in the UI, we need to query
51
+ // all of them. But there might be a *lot*, so instead separate the variants
52
+ // into it's own separate query that is deferred. So there's a brief moment
53
+ // where variant options might show as available when they're not, but after
54
+ // this deffered query resolves, the UI will update.
55
+ const variants = storefront.query(VARIANTS_QUERY, {
56
+ variables: {handle},
57
+ });
58
+
59
+ if (!product?.id) {
60
+ throw new Response(null, {status: 404});
61
+ }
62
+
63
+ const firstVariant = product.variants.nodes[0];
64
+ const firstVariantIsDefault = Boolean(
65
+ firstVariant.selectedOptions.find(
66
+ (option) => option.name === 'Title' && option.value === 'Default Title',
67
+ ),
68
+ );
69
+
70
+ if (firstVariantIsDefault) {
71
+ product.selectedVariant = firstVariant;
72
+ } else {
73
+ // if no selected variant was returned from the selected options,
74
+ // we redirect to the first variant's url with it's selected options applied
75
+ if (!product.selectedVariant) {
76
+ return redirectToFirstVariant({product, request});
77
+ }
78
+ }
79
+ return defer({product, variants});
80
+ }
81
+
82
+ function redirectToFirstVariant({
83
+ product,
84
+ request,
85
+ }: {
86
+ product: ProductFragment;
87
+ request: Request;
88
+ }) {
89
+ const url = new URL(request.url);
90
+ const firstVariant = product.variants.nodes[0];
91
+
92
+ throw redirect(
93
+ getVariantUrl({
94
+ pathname: url.pathname,
95
+ handle: product.handle,
96
+ selectedOptions: firstVariant.selectedOptions,
97
+ searchParams: new URLSearchParams(url.search),
98
+ }),
99
+ {
100
+ status: 302,
101
+ },
102
+ );
103
+ }
104
+
105
+ export default function Product() {
106
+ const {product, variants} = useLoaderData<typeof loader>();
107
+ const {selectedVariant} = product;
108
+ return (
109
+ <div className="product">
110
+ <ProductImage image={selectedVariant?.image} />
111
+ <ProductMain
112
+ selectedVariant={selectedVariant}
113
+ product={product}
114
+ variants={variants}
115
+ />
116
+ </div>
117
+ );
118
+ }
119
+
120
+ function ProductImage({image}: {image: ProductVariantFragment['image']}) {
121
+ if (!image) {
122
+ return <div className="product-image" />;
123
+ }
124
+ return (
125
+ <div className="product-image">
126
+ <Image
127
+ alt={image.altText || 'Product Image'}
128
+ aspectRatio="1/1"
129
+ data={image}
130
+ key={image.id}
131
+ sizes="(min-width: 45em) 50vw, 100vw"
132
+ />
133
+ </div>
134
+ );
135
+ }
136
+
137
+ function ProductMain({
138
+ selectedVariant,
139
+ product,
140
+ variants,
141
+ }: {
142
+ product: ProductFragment;
143
+ selectedVariant: ProductFragment['selectedVariant'];
144
+ variants: Promise<ProductVariantsQuery>;
145
+ }) {
146
+ const {title, descriptionHtml} = product;
147
+ return (
148
+ <div className="product-main">
149
+ <h1>{title}</h1>
150
+ <ProductPrice selectedVariant={selectedVariant} />
151
+ <br />
152
+ <Suspense
153
+ fallback={
154
+ <ProductForm
155
+ product={product}
156
+ selectedVariant={selectedVariant}
157
+ variants={[]}
158
+ />
159
+ }
160
+ >
161
+ <Await
162
+ errorElement="There was a problem loading product variants"
163
+ resolve={variants}
164
+ >
165
+ {(data) => (
166
+ <ProductForm
167
+ product={product}
168
+ selectedVariant={selectedVariant}
169
+ variants={data.product?.variants.nodes || []}
170
+ />
171
+ )}
172
+ </Await>
173
+ </Suspense>
174
+ <br />
175
+ <br />
176
+ <p>
177
+ <strong>Description</strong>
178
+ </p>
179
+ <br />
180
+ <div dangerouslySetInnerHTML={{__html: descriptionHtml}} />
181
+ <br />
182
+ </div>
183
+ );
184
+ }
185
+
186
+ function ProductPrice({
187
+ selectedVariant,
188
+ }: {
189
+ selectedVariant: ProductFragment['selectedVariant'];
190
+ }) {
191
+ return (
192
+ <div className="product-price">
193
+ {selectedVariant?.compareAtPrice ? (
194
+ <>
195
+ <p>Sale</p>
196
+ <br />
197
+ <div className="product-price-on-sale">
198
+ {selectedVariant ? <Money data={selectedVariant.price} /> : null}
199
+ <s>
200
+ <Money data={selectedVariant.compareAtPrice} />
201
+ </s>
202
+ </div>
203
+ </>
204
+ ) : (
205
+ selectedVariant?.price && <Money data={selectedVariant?.price} />
206
+ )}
207
+ </div>
208
+ );
209
+ }
210
+
211
+ function ProductForm({
212
+ product,
213
+ selectedVariant,
214
+ variants,
215
+ }: {
216
+ product: ProductFragment;
217
+ selectedVariant: ProductFragment['selectedVariant'];
218
+ variants: Array<ProductVariantFragment>;
219
+ }) {
220
+ return (
221
+ <div className="product-form">
222
+ <VariantSelector
223
+ handle={product.handle}
224
+ options={product.options}
225
+ variants={variants}
226
+ >
227
+ {({option}) => <ProductOptions key={option.name} option={option} />}
228
+ </VariantSelector>
229
+ <br />
230
+ <AddToCartButton
231
+ disabled={!selectedVariant || !selectedVariant.availableForSale}
232
+ onClick={() => {
233
+ window.location.href = window.location.href + '#cart-aside';
234
+ }}
235
+ lines={
236
+ selectedVariant
237
+ ? [
238
+ {
239
+ merchandiseId: selectedVariant.id,
240
+ quantity: 1,
241
+ },
242
+ ]
243
+ : []
244
+ }
245
+ >
246
+ {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'}
247
+ </AddToCartButton>
248
+ </div>
249
+ );
250
+ }
251
+
252
+ function ProductOptions({option}: {option: VariantOption}) {
253
+ return (
254
+ <div className="product-options" key={option.name}>
255
+ <h5>{option.name}</h5>
256
+ <div className="product-options-grid">
257
+ {option.values.map(({value, isAvailable, isActive, to}) => {
258
+ return (
259
+ <Link
260
+ className="product-options-item"
261
+ key={option.name + value}
262
+ prefetch="intent"
263
+ preventScrollReset
264
+ replace
265
+ to={to}
266
+ style={{
267
+ border: isActive ? '1px solid black' : '1px solid transparent',
268
+ opacity: isAvailable ? 1 : 0.3,
269
+ }}
270
+ >
271
+ {value}
272
+ </Link>
273
+ );
274
+ })}
275
+ </div>
276
+ <br />
277
+ </div>
278
+ );
279
+ }
280
+
281
+ function AddToCartButton({
282
+ analytics,
283
+ children,
284
+ disabled,
285
+ lines,
286
+ onClick,
287
+ }: {
288
+ analytics?: unknown;
289
+ children: React.ReactNode;
290
+ disabled?: boolean;
291
+ lines: CartLineInput[];
292
+ onClick?: () => void;
293
+ }) {
294
+ return (
295
+ <CartForm route="/cart" inputs={{lines}} action={CartForm.ACTIONS.LinesAdd}>
296
+ {(fetcher: FetcherWithComponents<any>) => (
297
+ <>
298
+ <input
299
+ name="analytics"
300
+ type="hidden"
301
+ value={JSON.stringify(analytics)}
302
+ />
303
+ <button
304
+ type="submit"
305
+ onClick={onClick}
306
+ disabled={disabled ?? fetcher.state !== 'idle'}
307
+ >
308
+ {children}
309
+ </button>
310
+ </>
311
+ )}
312
+ </CartForm>
313
+ );
314
+ }
315
+
316
+ const PRODUCT_VARIANT_FRAGMENT = `#graphql
317
+ fragment ProductVariant on ProductVariant {
318
+ availableForSale
319
+ compareAtPrice {
320
+ amount
321
+ currencyCode
322
+ }
323
+ id
324
+ image {
325
+ __typename
326
+ id
327
+ url
328
+ altText
329
+ width
330
+ height
331
+ }
332
+ price {
333
+ amount
334
+ currencyCode
335
+ }
336
+ product {
337
+ title
338
+ handle
339
+ }
340
+ quantityAvailable
341
+ selectedOptions {
342
+ name
343
+ value
344
+ }
345
+ sku
346
+ title
347
+ unitPrice {
348
+ amount
349
+ currencyCode
350
+ }
351
+ }
352
+ ` as const;
353
+
354
+ const PRODUCT_FRAGMENT = `#graphql
355
+ fragment Product on Product {
356
+ id
357
+ title
358
+ vendor
359
+ handle
360
+ descriptionHtml
361
+ description
362
+ options {
363
+ name
364
+ values
365
+ }
366
+ selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) {
367
+ ...ProductVariant
368
+ }
369
+ variants(first: 1) {
370
+ nodes {
371
+ ...ProductVariant
372
+ }
373
+ }
374
+ seo {
375
+ description
376
+ title
377
+ }
378
+ }
379
+ ${PRODUCT_VARIANT_FRAGMENT}
380
+ ` as const;
381
+
382
+ const PRODUCT_QUERY = `#graphql
383
+ query Product(
384
+ $country: CountryCode
385
+ $handle: String!
386
+ $language: LanguageCode
387
+ $selectedOptions: [SelectedOptionInput!]!
388
+ ) @inContext(country: $country, language: $language) {
389
+ product(handle: $handle) {
390
+ ...Product
391
+ }
392
+ }
393
+ ${PRODUCT_FRAGMENT}
394
+ ` as const;
395
+
396
+ const PRODUCT_VARIANTS_FRAGMENT = `#graphql
397
+ fragment ProductVariants on Product {
398
+ variants(first: 250) {
399
+ nodes {
400
+ ...ProductVariant
401
+ }
402
+ }
403
+ }
404
+ ${PRODUCT_VARIANT_FRAGMENT}
405
+ ` as const;
406
+
407
+ const VARIANTS_QUERY = `#graphql
408
+ ${PRODUCT_VARIANTS_FRAGMENT}
409
+ query ProductVariants(
410
+ $country: CountryCode
411
+ $language: LanguageCode
412
+ $handle: String!
413
+ ) @inContext(country: $country, language: $language) {
414
+ product(handle: $handle) {
415
+ ...ProductVariants
416
+ }
417
+ }
418
+ ` as const;
@@ -0,0 +1,168 @@
1
+ import type {V2_MetaFunction} from '@shopify/remix-oxygen';
2
+ import {defer, type LoaderArgs} from '@shopify/remix-oxygen';
3
+ import {useLoaderData} from '@remix-run/react';
4
+ import {getPaginationVariables} from '@shopify/hydrogen';
5
+
6
+ import {SearchForm, SearchResults, NoSearchResults} from '~/components/Search';
7
+
8
+ export const meta: V2_MetaFunction = () => {
9
+ return [{title: `Hydrogen | Search`}];
10
+ };
11
+
12
+ export async function loader({request, context}: LoaderArgs) {
13
+ const url = new URL(request.url);
14
+ const searchParams = new URLSearchParams(url.search);
15
+ const variables = getPaginationVariables(request, {pageBy: 8});
16
+ const searchTerm = String(searchParams.get('q') || '');
17
+
18
+ if (!searchTerm) {
19
+ return {
20
+ searchResults: {results: null, totalResults: 0},
21
+ searchTerm,
22
+ };
23
+ }
24
+
25
+ const data = await context.storefront.query(SEARCH_QUERY, {
26
+ variables: {
27
+ query: searchTerm,
28
+ ...variables,
29
+ },
30
+ });
31
+
32
+ if (!data) {
33
+ throw new Error('No search data returned from Shopify API');
34
+ }
35
+
36
+ const totalResults = Object.values(data).reduce((total, value) => {
37
+ return total + value.nodes.length;
38
+ }, 0);
39
+
40
+ const searchResults = {
41
+ results: data,
42
+ totalResults,
43
+ };
44
+
45
+ return defer({searchTerm, searchResults});
46
+ }
47
+
48
+ export default function SearchPage() {
49
+ const {searchTerm, searchResults} = useLoaderData<typeof loader>();
50
+ return (
51
+ <div className="search">
52
+ <h1>Search</h1>
53
+ <SearchForm searchTerm={searchTerm} />
54
+ {!searchTerm || !searchResults.totalResults ? (
55
+ <NoSearchResults />
56
+ ) : (
57
+ <SearchResults results={searchResults.results} />
58
+ )}
59
+ </div>
60
+ );
61
+ }
62
+
63
+ const SEARCH_QUERY = `#graphql
64
+ fragment SearchProduct on Product {
65
+ __typename
66
+ handle
67
+ id
68
+ publishedAt
69
+ title
70
+ trackingParameters
71
+ vendor
72
+ variants(first: 1) {
73
+ nodes {
74
+ id
75
+ image {
76
+ url
77
+ altText
78
+ width
79
+ height
80
+ }
81
+ price {
82
+ amount
83
+ currencyCode
84
+ }
85
+ compareAtPrice {
86
+ amount
87
+ currencyCode
88
+ }
89
+ selectedOptions {
90
+ name
91
+ value
92
+ }
93
+ product {
94
+ handle
95
+ title
96
+ }
97
+ }
98
+ }
99
+ }
100
+ fragment SearchPage on Page {
101
+ __typename
102
+ handle
103
+ id
104
+ title
105
+ trackingParameters
106
+ }
107
+ fragment SearchArticle on Article {
108
+ __typename
109
+ handle
110
+ id
111
+ title
112
+ trackingParameters
113
+ }
114
+ query search(
115
+ $country: CountryCode
116
+ $endCursor: String
117
+ $first: Int
118
+ $language: LanguageCode
119
+ $last: Int
120
+ $query: String!
121
+ $startCursor: String
122
+ ) @inContext(country: $country, language: $language) {
123
+ products: search(
124
+ query: $query,
125
+ unavailableProducts: HIDE,
126
+ types: [PRODUCT],
127
+ first: $first,
128
+ sortKey: RELEVANCE,
129
+ last: $last,
130
+ before: $startCursor,
131
+ after: $endCursor
132
+ ) {
133
+ nodes {
134
+ ...on Product {
135
+ ...SearchProduct
136
+ }
137
+ }
138
+ pageInfo {
139
+ hasNextPage
140
+ hasPreviousPage
141
+ startCursor
142
+ endCursor
143
+ }
144
+ }
145
+ pages: search(
146
+ query: $query,
147
+ types: [PAGE],
148
+ first: 10
149
+ ) {
150
+ nodes {
151
+ ...on Page {
152
+ ...SearchPage
153
+ }
154
+ }
155
+ }
156
+ articles: search(
157
+ query: $query,
158
+ types: [ARTICLE],
159
+ first: 10
160
+ ) {
161
+ nodes {
162
+ ...on Article {
163
+ ...SearchArticle
164
+ }
165
+ }
166
+ }
167
+ }
168
+ ` as const;