@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,225 @@
1
+ import {defer, redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
+ import {useLoaderData, Link, type MetaFunction} from '@remix-run/react';
3
+ import {
4
+ Pagination,
5
+ getPaginationVariables,
6
+ Image,
7
+ Money,
8
+ Analytics,
9
+ } from '@shopify/hydrogen';
10
+ import type {ProductItemFragment} from 'storefrontapi.generated';
11
+ import {useVariantUrl} from '~/lib/variants';
12
+
13
+ export const meta: MetaFunction<typeof loader> = ({data}) => {
14
+ return [{title: `Hydrogen | ${data?.collection.title ?? ''} Collection`}];
15
+ };
16
+
17
+ export async function loader(args: LoaderFunctionArgs) {
18
+ // Start fetching non-critical data without blocking time to first byte
19
+ const deferredData = loadDeferredData(args);
20
+
21
+ // Await the critical data required to render initial state of the page
22
+ const criticalData = await loadCriticalData(args);
23
+
24
+ return defer({...deferredData, ...criticalData});
25
+ }
26
+
27
+ /**
28
+ * Load data necessary for rendering content above the fold. This is the critical data
29
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
30
+ */
31
+ async function loadCriticalData({
32
+ context,
33
+ params,
34
+ request,
35
+ }: LoaderFunctionArgs) {
36
+ const {handle} = params;
37
+ const {storefront} = context;
38
+ const paginationVariables = getPaginationVariables(request, {
39
+ pageBy: 8,
40
+ });
41
+
42
+ if (!handle) {
43
+ throw redirect('/collections');
44
+ }
45
+
46
+ const [{collection}] = await Promise.all([
47
+ storefront.query(COLLECTION_QUERY, {
48
+ variables: {handle, ...paginationVariables},
49
+ // Add other queries here, so that they are loaded in parallel
50
+ }),
51
+ ]);
52
+
53
+ if (!collection) {
54
+ throw new Response(`Collection ${handle} not found`, {
55
+ status: 404,
56
+ });
57
+ }
58
+
59
+ return {
60
+ collection,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Load data for rendering content below the fold. This data is deferred and will be
66
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
67
+ * Make sure to not throw any errors here, as it will cause the page to 500.
68
+ */
69
+ function loadDeferredData({context}: LoaderFunctionArgs) {
70
+ return {};
71
+ }
72
+
73
+ export default function Collection() {
74
+ const {collection} = useLoaderData<typeof loader>();
75
+
76
+ return (
77
+ <div className="collection">
78
+ <h1>{collection.title}</h1>
79
+ <p className="collection-description">{collection.description}</p>
80
+ <Pagination connection={collection.products}>
81
+ {({nodes, isLoading, PreviousLink, NextLink}) => (
82
+ <>
83
+ <PreviousLink>
84
+ {isLoading ? 'Loading...' : <span>↑ Load previous</span>}
85
+ </PreviousLink>
86
+ <ProductsGrid products={nodes} />
87
+ <br />
88
+ <NextLink>
89
+ {isLoading ? 'Loading...' : <span>Load more ↓</span>}
90
+ </NextLink>
91
+ </>
92
+ )}
93
+ </Pagination>
94
+ <Analytics.CollectionView
95
+ data={{
96
+ collection: {
97
+ id: collection.id,
98
+ handle: collection.handle,
99
+ },
100
+ }}
101
+ />
102
+ </div>
103
+ );
104
+ }
105
+
106
+ function ProductsGrid({products}: {products: ProductItemFragment[]}) {
107
+ return (
108
+ <div className="products-grid">
109
+ {products.map((product, index) => {
110
+ return (
111
+ <ProductItem
112
+ key={product.id}
113
+ product={product}
114
+ loading={index < 8 ? 'eager' : undefined}
115
+ />
116
+ );
117
+ })}
118
+ </div>
119
+ );
120
+ }
121
+
122
+ function ProductItem({
123
+ product,
124
+ loading,
125
+ }: {
126
+ product: ProductItemFragment;
127
+ loading?: 'eager' | 'lazy';
128
+ }) {
129
+ const variant = product.variants.nodes[0];
130
+ const variantUrl = useVariantUrl(product.handle, variant.selectedOptions);
131
+ return (
132
+ <Link
133
+ className="product-item"
134
+ key={product.id}
135
+ prefetch="intent"
136
+ to={variantUrl}
137
+ >
138
+ {product.featuredImage && (
139
+ <Image
140
+ alt={product.featuredImage.altText || product.title}
141
+ aspectRatio="1/1"
142
+ data={product.featuredImage}
143
+ loading={loading}
144
+ sizes="(min-width: 45em) 400px, 100vw"
145
+ />
146
+ )}
147
+ <h4>{product.title}</h4>
148
+ <small>
149
+ <Money data={product.priceRange.minVariantPrice} />
150
+ </small>
151
+ </Link>
152
+ );
153
+ }
154
+
155
+ const PRODUCT_ITEM_FRAGMENT = `#graphql
156
+ fragment MoneyProductItem on MoneyV2 {
157
+ amount
158
+ currencyCode
159
+ }
160
+ fragment ProductItem on Product {
161
+ id
162
+ handle
163
+ title
164
+ featuredImage {
165
+ id
166
+ altText
167
+ url
168
+ width
169
+ height
170
+ }
171
+ priceRange {
172
+ minVariantPrice {
173
+ ...MoneyProductItem
174
+ }
175
+ maxVariantPrice {
176
+ ...MoneyProductItem
177
+ }
178
+ }
179
+ variants(first: 1) {
180
+ nodes {
181
+ selectedOptions {
182
+ name
183
+ value
184
+ }
185
+ }
186
+ }
187
+ }
188
+ ` as const;
189
+
190
+ // NOTE: https://shopify.dev/docs/api/storefront/2022-04/objects/collection
191
+ const COLLECTION_QUERY = `#graphql
192
+ ${PRODUCT_ITEM_FRAGMENT}
193
+ query Collection(
194
+ $handle: String!
195
+ $country: CountryCode
196
+ $language: LanguageCode
197
+ $first: Int
198
+ $last: Int
199
+ $startCursor: String
200
+ $endCursor: String
201
+ ) @inContext(country: $country, language: $language) {
202
+ collection(handle: $handle) {
203
+ id
204
+ handle
205
+ title
206
+ description
207
+ products(
208
+ first: $first,
209
+ last: $last,
210
+ before: $startCursor,
211
+ after: $endCursor
212
+ ) {
213
+ nodes {
214
+ ...ProductItem
215
+ }
216
+ pageInfo {
217
+ hasPreviousPage
218
+ hasNextPage
219
+ endCursor
220
+ startCursor
221
+ }
222
+ }
223
+ }
224
+ }
225
+ ` as const;
@@ -0,0 +1,146 @@
1
+ import {useLoaderData, Link} from '@remix-run/react';
2
+ import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
3
+ import {Pagination, getPaginationVariables, Image} from '@shopify/hydrogen';
4
+ import type {CollectionFragment} from 'storefrontapi.generated';
5
+
6
+ export async function loader(args: LoaderFunctionArgs) {
7
+ // Start fetching non-critical data without blocking time to first byte
8
+ const deferredData = loadDeferredData(args);
9
+
10
+ // Await the critical data required to render initial state of the page
11
+ const criticalData = await loadCriticalData(args);
12
+
13
+ return defer({...deferredData, ...criticalData});
14
+ }
15
+
16
+ /**
17
+ * Load data necessary for rendering content above the fold. This is the critical data
18
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
19
+ */
20
+ async function loadCriticalData({context, request}: LoaderFunctionArgs) {
21
+ const paginationVariables = getPaginationVariables(request, {
22
+ pageBy: 4,
23
+ });
24
+
25
+ const [{collections}] = await Promise.all([
26
+ context.storefront.query(COLLECTIONS_QUERY, {
27
+ variables: paginationVariables,
28
+ }),
29
+ // Add other queries here, so that they are loaded in parallel
30
+ ]);
31
+
32
+ return {collections};
33
+ }
34
+
35
+ /**
36
+ * Load data for rendering content below the fold. This data is deferred and will be
37
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
38
+ * Make sure to not throw any errors here, as it will cause the page to 500.
39
+ */
40
+ function loadDeferredData({context}: LoaderFunctionArgs) {
41
+ return {};
42
+ }
43
+
44
+ export default function Collections() {
45
+ const {collections} = useLoaderData<typeof loader>();
46
+
47
+ return (
48
+ <div className="collections">
49
+ <h1>Collections</h1>
50
+ <Pagination connection={collections}>
51
+ {({nodes, isLoading, PreviousLink, NextLink}) => (
52
+ <div>
53
+ <PreviousLink>
54
+ {isLoading ? 'Loading...' : <span>↑ Load previous</span>}
55
+ </PreviousLink>
56
+ <CollectionsGrid collections={nodes} />
57
+ <NextLink>
58
+ {isLoading ? 'Loading...' : <span>Load more ↓</span>}
59
+ </NextLink>
60
+ </div>
61
+ )}
62
+ </Pagination>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ function CollectionsGrid({collections}: {collections: CollectionFragment[]}) {
68
+ return (
69
+ <div className="collections-grid">
70
+ {collections.map((collection, index) => (
71
+ <CollectionItem
72
+ key={collection.id}
73
+ collection={collection}
74
+ index={index}
75
+ />
76
+ ))}
77
+ </div>
78
+ );
79
+ }
80
+
81
+ function CollectionItem({
82
+ collection,
83
+ index,
84
+ }: {
85
+ collection: CollectionFragment;
86
+ index: number;
87
+ }) {
88
+ return (
89
+ <Link
90
+ className="collection-item"
91
+ key={collection.id}
92
+ to={`/collections/${collection.handle}`}
93
+ prefetch="intent"
94
+ >
95
+ {collection?.image && (
96
+ <Image
97
+ alt={collection.image.altText || collection.title}
98
+ aspectRatio="1/1"
99
+ data={collection.image}
100
+ loading={index < 3 ? 'eager' : undefined}
101
+ />
102
+ )}
103
+ <h5>{collection.title}</h5>
104
+ </Link>
105
+ );
106
+ }
107
+
108
+ const COLLECTIONS_QUERY = `#graphql
109
+ fragment Collection on Collection {
110
+ id
111
+ title
112
+ handle
113
+ image {
114
+ id
115
+ url
116
+ altText
117
+ width
118
+ height
119
+ }
120
+ }
121
+ query StoreCollections(
122
+ $country: CountryCode
123
+ $endCursor: String
124
+ $first: Int
125
+ $language: LanguageCode
126
+ $last: Int
127
+ $startCursor: String
128
+ ) @inContext(country: $country, language: $language) {
129
+ collections(
130
+ first: $first,
131
+ last: $last,
132
+ before: $startCursor,
133
+ after: $endCursor
134
+ ) {
135
+ nodes {
136
+ ...Collection
137
+ }
138
+ pageInfo {
139
+ hasNextPage
140
+ hasPreviousPage
141
+ startCursor
142
+ endCursor
143
+ }
144
+ }
145
+ }
146
+ ` as const;
@@ -0,0 +1,185 @@
1
+ import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
+ import {useLoaderData, Link, type MetaFunction} from '@remix-run/react';
3
+ import {
4
+ Pagination,
5
+ getPaginationVariables,
6
+ Image,
7
+ Money,
8
+ } from '@shopify/hydrogen';
9
+ import type {ProductItemFragment} from 'storefrontapi.generated';
10
+ import {useVariantUrl} from '~/lib/variants';
11
+
12
+ export const meta: MetaFunction<typeof loader> = () => {
13
+ return [{title: `Hydrogen | Products`}];
14
+ };
15
+
16
+ export async function loader(args: LoaderFunctionArgs) {
17
+ // Start fetching non-critical data without blocking time to first byte
18
+ const deferredData = loadDeferredData(args);
19
+
20
+ // Await the critical data required to render initial state of the page
21
+ const criticalData = await loadCriticalData(args);
22
+
23
+ return defer({...deferredData, ...criticalData});
24
+ }
25
+
26
+ /**
27
+ * Load data necessary for rendering content above the fold. This is the critical data
28
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
29
+ */
30
+ async function loadCriticalData({context, request}: LoaderFunctionArgs) {
31
+ const {storefront} = context;
32
+ const paginationVariables = getPaginationVariables(request, {
33
+ pageBy: 8,
34
+ });
35
+
36
+ const [{products}] = await Promise.all([
37
+ storefront.query(CATALOG_QUERY, {
38
+ variables: {...paginationVariables},
39
+ }),
40
+ // Add other queries here, so that they are loaded in parallel
41
+ ]);
42
+ return {products};
43
+ }
44
+
45
+ /**
46
+ * Load data for rendering content below the fold. This data is deferred and will be
47
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
48
+ * Make sure to not throw any errors here, as it will cause the page to 500.
49
+ */
50
+ function loadDeferredData({context}: LoaderFunctionArgs) {
51
+ return {};
52
+ }
53
+
54
+ export default function Collection() {
55
+ const {products} = useLoaderData<typeof loader>();
56
+
57
+ return (
58
+ <div className="collection">
59
+ <h1>Products</h1>
60
+ <Pagination connection={products}>
61
+ {({nodes, isLoading, PreviousLink, NextLink}) => (
62
+ <>
63
+ <PreviousLink>
64
+ {isLoading ? 'Loading...' : <span>↑ Load previous</span>}
65
+ </PreviousLink>
66
+ <ProductsGrid products={nodes} />
67
+ <br />
68
+ <NextLink>
69
+ {isLoading ? 'Loading...' : <span>Load more ↓</span>}
70
+ </NextLink>
71
+ </>
72
+ )}
73
+ </Pagination>
74
+ </div>
75
+ );
76
+ }
77
+
78
+ function ProductsGrid({products}: {products: ProductItemFragment[]}) {
79
+ return (
80
+ <div className="products-grid">
81
+ {products.map((product, index) => {
82
+ return (
83
+ <ProductItem
84
+ key={product.id}
85
+ product={product}
86
+ loading={index < 8 ? 'eager' : undefined}
87
+ />
88
+ );
89
+ })}
90
+ </div>
91
+ );
92
+ }
93
+
94
+ function ProductItem({
95
+ product,
96
+ loading,
97
+ }: {
98
+ product: ProductItemFragment;
99
+ loading?: 'eager' | 'lazy';
100
+ }) {
101
+ const variant = product.variants.nodes[0];
102
+ const variantUrl = useVariantUrl(product.handle, variant.selectedOptions);
103
+ return (
104
+ <Link
105
+ className="product-item"
106
+ key={product.id}
107
+ prefetch="intent"
108
+ to={variantUrl}
109
+ >
110
+ {product.featuredImage && (
111
+ <Image
112
+ alt={product.featuredImage.altText || product.title}
113
+ aspectRatio="1/1"
114
+ data={product.featuredImage}
115
+ loading={loading}
116
+ sizes="(min-width: 45em) 400px, 100vw"
117
+ />
118
+ )}
119
+ <h4>{product.title}</h4>
120
+ <small>
121
+ <Money data={product.priceRange.minVariantPrice} />
122
+ </small>
123
+ </Link>
124
+ );
125
+ }
126
+
127
+ const PRODUCT_ITEM_FRAGMENT = `#graphql
128
+ fragment MoneyProductItem on MoneyV2 {
129
+ amount
130
+ currencyCode
131
+ }
132
+ fragment ProductItem on Product {
133
+ id
134
+ handle
135
+ title
136
+ featuredImage {
137
+ id
138
+ altText
139
+ url
140
+ width
141
+ height
142
+ }
143
+ priceRange {
144
+ minVariantPrice {
145
+ ...MoneyProductItem
146
+ }
147
+ maxVariantPrice {
148
+ ...MoneyProductItem
149
+ }
150
+ }
151
+ variants(first: 1) {
152
+ nodes {
153
+ selectedOptions {
154
+ name
155
+ value
156
+ }
157
+ }
158
+ }
159
+ }
160
+ ` as const;
161
+
162
+ // NOTE: https://shopify.dev/docs/api/storefront/2024-01/objects/product
163
+ const CATALOG_QUERY = `#graphql
164
+ query Catalog(
165
+ $country: CountryCode
166
+ $language: LanguageCode
167
+ $first: Int
168
+ $last: Int
169
+ $startCursor: String
170
+ $endCursor: String
171
+ ) @inContext(country: $country, language: $language) {
172
+ products(first: $first, last: $last, before: $startCursor, after: $endCursor) {
173
+ nodes {
174
+ ...ProductItem
175
+ }
176
+ pageInfo {
177
+ hasPreviousPage
178
+ hasNextPage
179
+ startCursor
180
+ endCursor
181
+ }
182
+ }
183
+ }
184
+ ${PRODUCT_ITEM_FRAGMENT}
185
+ ` as const;
@@ -0,0 +1,47 @@
1
+ import {redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
+
3
+ /**
4
+ * Automatically applies a discount found on the url
5
+ * If a cart exists it's updated with the discount, otherwise a cart is created with the discount already applied
6
+ *
7
+ * @example
8
+ * Example path applying a discount and optional redirecting (defaults to the home page)
9
+ * ```js
10
+ * /discount/FREESHIPPING?redirect=/products
11
+ *
12
+ * ```
13
+ */
14
+ export async function loader({request, context, params}: LoaderFunctionArgs) {
15
+ const {cart} = context;
16
+ const {code} = params;
17
+
18
+ const url = new URL(request.url);
19
+ const searchParams = new URLSearchParams(url.search);
20
+ let redirectParam =
21
+ searchParams.get('redirect') || searchParams.get('return_to') || '/';
22
+
23
+ if (redirectParam.includes('//')) {
24
+ // Avoid redirecting to external URLs to prevent phishing attacks
25
+ redirectParam = '/';
26
+ }
27
+
28
+ searchParams.delete('redirect');
29
+ searchParams.delete('return_to');
30
+
31
+ const redirectUrl = `${redirectParam}?${searchParams}`;
32
+
33
+ if (!code) {
34
+ return redirect(redirectUrl);
35
+ }
36
+
37
+ const result = await cart.updateDiscountCodes([code]);
38
+ const headers = cart.setCartId(result.cart.id);
39
+
40
+ // Using set-cookie on a 303 redirect will not work if the domain origin have port number (:3000)
41
+ // If there is no cart id and a new cart id is created in the progress, it will not be set in the cookie
42
+ // on localhost:3000
43
+ return redirect(redirectUrl, {
44
+ status: 303,
45
+ headers,
46
+ });
47
+ }
@@ -0,0 +1,84 @@
1
+ import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
+ import {useLoaderData, type MetaFunction} from '@remix-run/react';
3
+
4
+ export const meta: MetaFunction<typeof loader> = ({data}) => {
5
+ return [{title: `Hydrogen | ${data?.page.title ?? ''}`}];
6
+ };
7
+
8
+ export async function loader(args: LoaderFunctionArgs) {
9
+ // Start fetching non-critical data without blocking time to first byte
10
+ const deferredData = loadDeferredData(args);
11
+
12
+ // Await the critical data required to render initial state of the page
13
+ const criticalData = await loadCriticalData(args);
14
+
15
+ return defer({...deferredData, ...criticalData});
16
+ }
17
+
18
+ /**
19
+ * Load data necessary for rendering content above the fold. This is the critical data
20
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
21
+ */
22
+ async function loadCriticalData({context, params}: LoaderFunctionArgs) {
23
+ if (!params.handle) {
24
+ throw new Error('Missing page handle');
25
+ }
26
+
27
+ const [{page}] = await Promise.all([
28
+ context.storefront.query(PAGE_QUERY, {
29
+ variables: {
30
+ handle: params.handle,
31
+ },
32
+ }),
33
+ // Add other queries here, so that they are loaded in parallel
34
+ ]);
35
+
36
+ if (!page) {
37
+ throw new Response('Not Found', {status: 404});
38
+ }
39
+
40
+ return {
41
+ page,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Load data for rendering content below the fold. This data is deferred and will be
47
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
48
+ * Make sure to not throw any errors here, as it will cause the page to 500.
49
+ */
50
+ function loadDeferredData({context}: LoaderFunctionArgs) {
51
+ return {};
52
+ }
53
+
54
+ export default function Page() {
55
+ const {page} = useLoaderData<typeof loader>();
56
+
57
+ return (
58
+ <div className="page">
59
+ <header>
60
+ <h1>{page.title}</h1>
61
+ </header>
62
+ <main dangerouslySetInnerHTML={{__html: page.body}} />
63
+ </div>
64
+ );
65
+ }
66
+
67
+ const PAGE_QUERY = `#graphql
68
+ query Page(
69
+ $language: LanguageCode,
70
+ $country: CountryCode,
71
+ $handle: String!
72
+ )
73
+ @inContext(language: $language, country: $country) {
74
+ page(handle: $handle) {
75
+ id
76
+ title
77
+ body
78
+ seo {
79
+ description
80
+ title
81
+ }
82
+ }
83
+ }
84
+ ` as const;