@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,93 @@
1
+ import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
+ import {Link, useLoaderData, type MetaFunction} from '@remix-run/react';
3
+ import {type Shop} from '@shopify/hydrogen/storefront-api-types';
4
+
5
+ type SelectedPolicies = keyof Pick<
6
+ Shop,
7
+ 'privacyPolicy' | 'shippingPolicy' | 'termsOfService' | 'refundPolicy'
8
+ >;
9
+
10
+ export const meta: MetaFunction<typeof loader> = ({data}) => {
11
+ return [{title: `Hydrogen | ${data?.policy.title ?? ''}`}];
12
+ };
13
+
14
+ export async function loader({params, context}: LoaderFunctionArgs) {
15
+ if (!params.handle) {
16
+ throw new Response('No handle was passed in', {status: 404});
17
+ }
18
+
19
+ const policyName = params.handle.replace(
20
+ /-([a-z])/g,
21
+ (_: unknown, m1: string) => m1.toUpperCase(),
22
+ ) as SelectedPolicies;
23
+
24
+ const data = await context.storefront.query(POLICY_CONTENT_QUERY, {
25
+ variables: {
26
+ privacyPolicy: false,
27
+ shippingPolicy: false,
28
+ termsOfService: false,
29
+ refundPolicy: false,
30
+ [policyName]: true,
31
+ language: context.storefront.i18n?.language,
32
+ },
33
+ });
34
+
35
+ const policy = data.shop?.[policyName];
36
+
37
+ if (!policy) {
38
+ throw new Response('Could not find the policy', {status: 404});
39
+ }
40
+
41
+ return json({policy});
42
+ }
43
+
44
+ export default function Policy() {
45
+ const {policy} = useLoaderData<typeof loader>();
46
+
47
+ return (
48
+ <div className="policy">
49
+ <br />
50
+ <br />
51
+ <div>
52
+ <Link to="/policies">← Back to Policies</Link>
53
+ </div>
54
+ <br />
55
+ <h1>{policy.title}</h1>
56
+ <div dangerouslySetInnerHTML={{__html: policy.body}} />
57
+ </div>
58
+ );
59
+ }
60
+
61
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/objects/Shop
62
+ const POLICY_CONTENT_QUERY = `#graphql
63
+ fragment Policy on ShopPolicy {
64
+ body
65
+ handle
66
+ id
67
+ title
68
+ url
69
+ }
70
+ query Policy(
71
+ $country: CountryCode
72
+ $language: LanguageCode
73
+ $privacyPolicy: Boolean!
74
+ $refundPolicy: Boolean!
75
+ $shippingPolicy: Boolean!
76
+ $termsOfService: Boolean!
77
+ ) @inContext(language: $language, country: $country) {
78
+ shop {
79
+ privacyPolicy @include(if: $privacyPolicy) {
80
+ ...Policy
81
+ }
82
+ shippingPolicy @include(if: $shippingPolicy) {
83
+ ...Policy
84
+ }
85
+ termsOfService @include(if: $termsOfService) {
86
+ ...Policy
87
+ }
88
+ refundPolicy @include(if: $refundPolicy) {
89
+ ...Policy
90
+ }
91
+ }
92
+ }
93
+ ` as const;
@@ -0,0 +1,63 @@
1
+ import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
+ import {useLoaderData, Link} from '@remix-run/react';
3
+
4
+ export async function loader({context}: LoaderFunctionArgs) {
5
+ const data = await context.storefront.query(POLICIES_QUERY);
6
+ const policies = Object.values(data.shop || {});
7
+
8
+ if (!policies.length) {
9
+ throw new Response('No policies found', {status: 404});
10
+ }
11
+
12
+ return json({policies});
13
+ }
14
+
15
+ export default function Policies() {
16
+ const {policies} = useLoaderData<typeof loader>();
17
+
18
+ return (
19
+ <div className="policies">
20
+ <h1>Policies</h1>
21
+ <div>
22
+ {policies.map((policy) => {
23
+ if (!policy) return null;
24
+ return (
25
+ <fieldset key={policy.id}>
26
+ <Link to={`/policies/${policy.handle}`}>{policy.title}</Link>
27
+ </fieldset>
28
+ );
29
+ })}
30
+ </div>
31
+ </div>
32
+ );
33
+ }
34
+
35
+ const POLICIES_QUERY = `#graphql
36
+ fragment PolicyItem on ShopPolicy {
37
+ id
38
+ title
39
+ handle
40
+ }
41
+ query Policies ($country: CountryCode, $language: LanguageCode)
42
+ @inContext(country: $country, language: $language) {
43
+ shop {
44
+ privacyPolicy {
45
+ ...PolicyItem
46
+ }
47
+ shippingPolicy {
48
+ ...PolicyItem
49
+ }
50
+ termsOfService {
51
+ ...PolicyItem
52
+ }
53
+ refundPolicy {
54
+ ...PolicyItem
55
+ }
56
+ subscriptionPolicy {
57
+ id
58
+ title
59
+ handle
60
+ }
61
+ }
62
+ }
63
+ ` as const;
@@ -0,0 +1,299 @@
1
+ import {Suspense} from 'react';
2
+ import {defer, redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
3
+ import {Await, useLoaderData, type MetaFunction} from '@remix-run/react';
4
+ import type {ProductFragment} from 'storefrontapi.generated';
5
+ import {
6
+ getSelectedProductOptions,
7
+ Analytics,
8
+ useOptimisticVariant,
9
+ } from '@shopify/hydrogen';
10
+ import type {SelectedOption} from '@shopify/hydrogen/storefront-api-types';
11
+ import {getVariantUrl} from '~/lib/variants';
12
+ import {ProductPrice} from '~/components/ProductPrice';
13
+ import {ProductImage} from '~/components/ProductImage';
14
+ import {ProductForm} from '~/components/ProductForm';
15
+
16
+ export const meta: MetaFunction<typeof loader> = ({data}) => {
17
+ return [{title: `Hydrogen | ${data?.product.title ?? ''}`}];
18
+ };
19
+
20
+ export async function loader(args: LoaderFunctionArgs) {
21
+ // Start fetching non-critical data without blocking time to first byte
22
+ const deferredData = loadDeferredData(args);
23
+
24
+ // Await the critical data required to render initial state of the page
25
+ const criticalData = await loadCriticalData(args);
26
+
27
+ return defer({...deferredData, ...criticalData});
28
+ }
29
+
30
+ /**
31
+ * Load data necessary for rendering content above the fold. This is the critical data
32
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
33
+ */
34
+ async function loadCriticalData({
35
+ context,
36
+ params,
37
+ request,
38
+ }: LoaderFunctionArgs) {
39
+ const {handle} = params;
40
+ const {storefront} = context;
41
+
42
+ if (!handle) {
43
+ throw new Error('Expected product handle to be defined');
44
+ }
45
+
46
+ const [{product}] = await Promise.all([
47
+ storefront.query(PRODUCT_QUERY, {
48
+ variables: {handle, selectedOptions: getSelectedProductOptions(request)},
49
+ }),
50
+ // Add other queries here, so that they are loaded in parallel
51
+ ]);
52
+
53
+ if (!product?.id) {
54
+ throw new Response(null, {status: 404});
55
+ }
56
+
57
+ const firstVariant = product.variants.nodes[0];
58
+ const firstVariantIsDefault = Boolean(
59
+ firstVariant.selectedOptions.find(
60
+ (option: SelectedOption) =>
61
+ option.name === 'Title' && option.value === 'Default Title',
62
+ ),
63
+ );
64
+
65
+ if (firstVariantIsDefault) {
66
+ product.selectedVariant = firstVariant;
67
+ } else {
68
+ // if no selected variant was returned from the selected options,
69
+ // we redirect to the first variant's url with it's selected options applied
70
+ if (!product.selectedVariant) {
71
+ throw redirectToFirstVariant({product, request});
72
+ }
73
+ }
74
+
75
+ return {
76
+ product,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Load data for rendering content below the fold. This data is deferred and will be
82
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
83
+ * Make sure to not throw any errors here, as it will cause the page to 500.
84
+ */
85
+ function loadDeferredData({context, params}: LoaderFunctionArgs) {
86
+ // In order to show which variants are available in the UI, we need to query
87
+ // all of them. But there might be a *lot*, so instead separate the variants
88
+ // into it's own separate query that is deferred. So there's a brief moment
89
+ // where variant options might show as available when they're not, but after
90
+ // this deffered query resolves, the UI will update.
91
+ const variants = context.storefront
92
+ .query(VARIANTS_QUERY, {
93
+ variables: {handle: params.handle!},
94
+ })
95
+ .catch((error) => {
96
+ // Log query errors, but don't throw them so the page can still render
97
+ console.error(error);
98
+ return null;
99
+ });
100
+
101
+ return {
102
+ variants,
103
+ };
104
+ }
105
+
106
+ function redirectToFirstVariant({
107
+ product,
108
+ request,
109
+ }: {
110
+ product: ProductFragment;
111
+ request: Request;
112
+ }) {
113
+ const url = new URL(request.url);
114
+ const firstVariant = product.variants.nodes[0];
115
+
116
+ return redirect(
117
+ getVariantUrl({
118
+ pathname: url.pathname,
119
+ handle: product.handle,
120
+ selectedOptions: firstVariant.selectedOptions,
121
+ searchParams: new URLSearchParams(url.search),
122
+ }),
123
+ {
124
+ status: 302,
125
+ },
126
+ );
127
+ }
128
+
129
+ export default function Product() {
130
+ const {product, variants} = useLoaderData<typeof loader>();
131
+ const selectedVariant = useOptimisticVariant(
132
+ product.selectedVariant,
133
+ variants,
134
+ );
135
+
136
+ const {title, descriptionHtml} = product;
137
+
138
+ return (
139
+ <div className="product">
140
+ <ProductImage image={selectedVariant?.image} />
141
+ <div className="product-main">
142
+ <h1>{title}</h1>
143
+ <ProductPrice
144
+ price={selectedVariant?.price}
145
+ compareAtPrice={selectedVariant?.compareAtPrice}
146
+ />
147
+ <br />
148
+ <Suspense
149
+ fallback={
150
+ <ProductForm
151
+ product={product}
152
+ selectedVariant={selectedVariant}
153
+ variants={[]}
154
+ />
155
+ }
156
+ >
157
+ <Await
158
+ errorElement="There was a problem loading product variants"
159
+ resolve={variants}
160
+ >
161
+ {(data) => (
162
+ <ProductForm
163
+ product={product}
164
+ selectedVariant={selectedVariant}
165
+ variants={data?.product?.variants.nodes || []}
166
+ />
167
+ )}
168
+ </Await>
169
+ </Suspense>
170
+ <br />
171
+ <br />
172
+ <p>
173
+ <strong>Description</strong>
174
+ </p>
175
+ <br />
176
+ <div dangerouslySetInnerHTML={{__html: descriptionHtml}} />
177
+ <br />
178
+ </div>
179
+ <Analytics.ProductView
180
+ data={{
181
+ products: [
182
+ {
183
+ id: product.id,
184
+ title: product.title,
185
+ price: selectedVariant?.price.amount || '0',
186
+ vendor: product.vendor,
187
+ variantId: selectedVariant?.id || '',
188
+ variantTitle: selectedVariant?.title || '',
189
+ quantity: 1,
190
+ },
191
+ ],
192
+ }}
193
+ />
194
+ </div>
195
+ );
196
+ }
197
+
198
+ const PRODUCT_VARIANT_FRAGMENT = `#graphql
199
+ fragment ProductVariant on ProductVariant {
200
+ availableForSale
201
+ compareAtPrice {
202
+ amount
203
+ currencyCode
204
+ }
205
+ id
206
+ image {
207
+ __typename
208
+ id
209
+ url
210
+ altText
211
+ width
212
+ height
213
+ }
214
+ price {
215
+ amount
216
+ currencyCode
217
+ }
218
+ product {
219
+ title
220
+ handle
221
+ }
222
+ selectedOptions {
223
+ name
224
+ value
225
+ }
226
+ sku
227
+ title
228
+ unitPrice {
229
+ amount
230
+ currencyCode
231
+ }
232
+ }
233
+ ` as const;
234
+
235
+ const PRODUCT_FRAGMENT = `#graphql
236
+ fragment Product on Product {
237
+ id
238
+ title
239
+ vendor
240
+ handle
241
+ descriptionHtml
242
+ description
243
+ options {
244
+ name
245
+ values
246
+ }
247
+ selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
248
+ ...ProductVariant
249
+ }
250
+ variants(first: 1) {
251
+ nodes {
252
+ ...ProductVariant
253
+ }
254
+ }
255
+ seo {
256
+ description
257
+ title
258
+ }
259
+ }
260
+ ${PRODUCT_VARIANT_FRAGMENT}
261
+ ` as const;
262
+
263
+ const PRODUCT_QUERY = `#graphql
264
+ query Product(
265
+ $country: CountryCode
266
+ $handle: String!
267
+ $language: LanguageCode
268
+ $selectedOptions: [SelectedOptionInput!]!
269
+ ) @inContext(country: $country, language: $language) {
270
+ product(handle: $handle) {
271
+ ...Product
272
+ }
273
+ }
274
+ ${PRODUCT_FRAGMENT}
275
+ ` as const;
276
+
277
+ const PRODUCT_VARIANTS_FRAGMENT = `#graphql
278
+ fragment ProductVariants on Product {
279
+ variants(first: 250) {
280
+ nodes {
281
+ ...ProductVariant
282
+ }
283
+ }
284
+ }
285
+ ${PRODUCT_VARIANT_FRAGMENT}
286
+ ` as const;
287
+
288
+ const VARIANTS_QUERY = `#graphql
289
+ ${PRODUCT_VARIANTS_FRAGMENT}
290
+ query ProductVariants(
291
+ $country: CountryCode
292
+ $language: LanguageCode
293
+ $handle: String!
294
+ ) @inContext(country: $country, language: $language) {
295
+ product(handle: $handle) {
296
+ ...ProductVariants
297
+ }
298
+ }
299
+ ` as const;
@@ -0,0 +1,177 @@
1
+ import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
+ import {useLoaderData, type MetaFunction} from '@remix-run/react';
3
+ import {getPaginationVariables, Analytics} from '@shopify/hydrogen';
4
+
5
+ import {SearchForm, SearchResults, NoSearchResults} from '~/components/Search';
6
+
7
+ export const meta: MetaFunction = () => {
8
+ return [{title: `Hydrogen | Search`}];
9
+ };
10
+
11
+ export async function loader({request, context}: LoaderFunctionArgs) {
12
+ const url = new URL(request.url);
13
+ const searchParams = new URLSearchParams(url.search);
14
+ const variables = getPaginationVariables(request, {pageBy: 8});
15
+ const searchTerm = String(searchParams.get('q') || '');
16
+
17
+ if (!searchTerm) {
18
+ return {
19
+ searchResults: {results: null, totalResults: 0},
20
+ searchTerm,
21
+ };
22
+ }
23
+
24
+ const {errors, ...data} = await context.storefront.query(SEARCH_QUERY, {
25
+ variables: {
26
+ query: searchTerm,
27
+ ...variables,
28
+ },
29
+ });
30
+
31
+ if (!data) {
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
+ });
48
+ }
49
+
50
+ export default function SearchPage() {
51
+ const {searchTerm, searchResults} = useLoaderData<typeof loader>();
52
+
53
+ return (
54
+ <div className="search">
55
+ <h1>Search</h1>
56
+ <SearchForm searchTerm={searchTerm} />
57
+ {!searchTerm || !searchResults.totalResults ? (
58
+ <NoSearchResults />
59
+ ) : (
60
+ <SearchResults
61
+ results={searchResults.results}
62
+ searchTerm={searchTerm}
63
+ />
64
+ )}
65
+ <Analytics.SearchView
66
+ data={{searchTerm, searchResults}}
67
+ />
68
+ </div>
69
+ );
70
+ }
71
+
72
+ const SEARCH_QUERY = `#graphql
73
+ fragment SearchProduct on Product {
74
+ __typename
75
+ handle
76
+ id
77
+ publishedAt
78
+ title
79
+ trackingParameters
80
+ vendor
81
+ variants(first: 1) {
82
+ nodes {
83
+ id
84
+ image {
85
+ url
86
+ altText
87
+ width
88
+ height
89
+ }
90
+ price {
91
+ amount
92
+ currencyCode
93
+ }
94
+ compareAtPrice {
95
+ amount
96
+ currencyCode
97
+ }
98
+ selectedOptions {
99
+ name
100
+ value
101
+ }
102
+ product {
103
+ handle
104
+ title
105
+ }
106
+ }
107
+ }
108
+ }
109
+ fragment SearchPage on Page {
110
+ __typename
111
+ handle
112
+ id
113
+ title
114
+ trackingParameters
115
+ }
116
+ fragment SearchArticle on Article {
117
+ __typename
118
+ handle
119
+ id
120
+ title
121
+ trackingParameters
122
+ }
123
+ query search(
124
+ $country: CountryCode
125
+ $endCursor: String
126
+ $first: Int
127
+ $language: LanguageCode
128
+ $last: Int
129
+ $query: String!
130
+ $startCursor: String
131
+ ) @inContext(country: $country, language: $language) {
132
+ products: search(
133
+ query: $query,
134
+ unavailableProducts: HIDE,
135
+ types: [PRODUCT],
136
+ first: $first,
137
+ sortKey: RELEVANCE,
138
+ last: $last,
139
+ before: $startCursor,
140
+ after: $endCursor
141
+ ) {
142
+ nodes {
143
+ ...on Product {
144
+ ...SearchProduct
145
+ }
146
+ }
147
+ pageInfo {
148
+ hasNextPage
149
+ hasPreviousPage
150
+ startCursor
151
+ endCursor
152
+ }
153
+ }
154
+ pages: search(
155
+ query: $query,
156
+ types: [PAGE],
157
+ first: 10
158
+ ) {
159
+ nodes {
160
+ ...on Page {
161
+ ...SearchPage
162
+ }
163
+ }
164
+ }
165
+ articles: search(
166
+ query: $query,
167
+ types: [ARTICLE],
168
+ first: 10
169
+ ) {
170
+ nodes {
171
+ ...on Article {
172
+ ...SearchArticle
173
+ }
174
+ }
175
+ }
176
+ }
177
+ ` as const;