@shopify/cli-hydrogen 3.26.0 → 4.0.0-alpha.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 +89 -0
  2. package/dist/commands/hydrogen/dev.js +116 -0
  3. package/dist/commands/hydrogen/init.js +42 -0
  4. package/dist/commands/hydrogen/preview.js +34 -0
  5. package/dist/hooks/init.js +21 -0
  6. package/dist/templates/demo-store/.editorconfig +8 -0
  7. package/dist/templates/demo-store/.eslintignore +4 -0
  8. package/dist/templates/demo-store/.eslintrc.js +16 -0
  9. package/dist/templates/demo-store/.graphqlrc.yml +1 -0
  10. package/dist/templates/demo-store/.prettierignore +2 -0
  11. package/dist/templates/demo-store/.turbo/turbo-build.log +13 -0
  12. package/dist/templates/demo-store/app/components/AccountAddressBook.tsx +97 -0
  13. package/dist/templates/demo-store/app/components/AccountDetails.tsx +41 -0
  14. package/dist/templates/demo-store/app/components/AddToCartButton.tsx +42 -0
  15. package/dist/templates/demo-store/app/components/Breadcrumbs.tsx +36 -0
  16. package/dist/templates/demo-store/app/components/Button.tsx +56 -0
  17. package/dist/templates/demo-store/app/components/Cart.tsx +431 -0
  18. package/dist/templates/demo-store/app/components/CartLoading.tsx +50 -0
  19. package/dist/templates/demo-store/app/components/CountrySelector.tsx +180 -0
  20. package/dist/templates/demo-store/app/components/Drawer.tsx +115 -0
  21. package/dist/templates/demo-store/app/components/FeaturedCollections.tsx +54 -0
  22. package/dist/templates/demo-store/app/components/FeaturedProducts.tsx +116 -0
  23. package/dist/templates/demo-store/app/components/FeaturedSection.tsx +39 -0
  24. package/dist/templates/demo-store/app/components/GenericError.tsx +58 -0
  25. package/dist/templates/demo-store/app/components/Grid.tsx +44 -0
  26. package/dist/templates/demo-store/app/components/Hero.tsx +136 -0
  27. package/dist/templates/demo-store/app/components/Icon.tsx +253 -0
  28. package/dist/templates/demo-store/app/components/Input.tsx +24 -0
  29. package/dist/templates/demo-store/app/components/Layout.tsx +492 -0
  30. package/dist/templates/demo-store/app/components/Link.tsx +46 -0
  31. package/dist/templates/demo-store/app/components/Modal.tsx +46 -0
  32. package/dist/templates/demo-store/app/components/NotFound.tsx +22 -0
  33. package/dist/templates/demo-store/app/components/OrderCard.tsx +85 -0
  34. package/dist/templates/demo-store/app/components/Pagination.tsx +277 -0
  35. package/dist/templates/demo-store/app/components/ProductCard.tsx +146 -0
  36. package/dist/templates/demo-store/app/components/ProductGallery.tsx +114 -0
  37. package/dist/templates/demo-store/app/components/ProductGrid.tsx +93 -0
  38. package/dist/templates/demo-store/app/components/ProductSwimlane.tsx +30 -0
  39. package/dist/templates/demo-store/app/components/Skeleton.tsx +24 -0
  40. package/dist/templates/demo-store/app/components/SortFilter.tsx +411 -0
  41. package/dist/templates/demo-store/app/components/Text.tsx +192 -0
  42. package/dist/templates/demo-store/app/components/index.ts +28 -0
  43. package/dist/templates/demo-store/app/data/countries.ts +194 -0
  44. package/dist/templates/demo-store/app/data/index.ts +1037 -0
  45. package/dist/templates/demo-store/app/entry.client.tsx +4 -0
  46. package/dist/templates/demo-store/app/entry.server.tsx +26 -0
  47. package/dist/templates/demo-store/app/hooks/useCartFetchers.tsx +14 -0
  48. package/dist/templates/demo-store/app/hooks/useIsHydrated.tsx +12 -0
  49. package/dist/templates/demo-store/app/lib/const.ts +10 -0
  50. package/dist/templates/demo-store/app/lib/placeholders.ts +242 -0
  51. package/dist/templates/demo-store/app/lib/seo/common.tsx +324 -0
  52. package/dist/templates/demo-store/app/lib/seo/debugger.tsx +175 -0
  53. package/dist/templates/demo-store/app/lib/seo/image.tsx +32 -0
  54. package/dist/templates/demo-store/app/lib/seo/index.ts +4 -0
  55. package/dist/templates/demo-store/app/lib/seo/seo.tsx +24 -0
  56. package/dist/templates/demo-store/app/lib/seo/types.ts +70 -0
  57. package/dist/templates/demo-store/app/lib/session.server.ts +57 -0
  58. package/dist/templates/demo-store/app/lib/type.ts +21 -0
  59. package/dist/templates/demo-store/app/lib/utils.ts +310 -0
  60. package/dist/templates/demo-store/app/root.tsx +282 -0
  61. package/dist/templates/demo-store/app/routes/$.tsx +7 -0
  62. package/dist/templates/demo-store/app/routes/$lang/$.tsx +1 -0
  63. package/dist/templates/demo-store/app/routes/$lang/[robots.txt].tsx +1 -0
  64. package/dist/templates/demo-store/app/routes/$lang/[sitemap.xml].tsx +1 -0
  65. package/dist/templates/demo-store/app/routes/$lang/account/__private/address/$id.tsx +1 -0
  66. package/dist/templates/demo-store/app/routes/$lang/account/__private/edit.tsx +1 -0
  67. package/dist/templates/demo-store/app/routes/$lang/account/__private/logout.ts +1 -0
  68. package/dist/templates/demo-store/app/routes/$lang/account/__private/orders.$id.tsx +1 -0
  69. package/dist/templates/demo-store/app/routes/$lang/account/__public/activate.$id.$activationToken.tsx +6 -0
  70. package/dist/templates/demo-store/app/routes/$lang/account/__public/login.tsx +7 -0
  71. package/dist/templates/demo-store/app/routes/$lang/account/__public/recover.tsx +1 -0
  72. package/dist/templates/demo-store/app/routes/$lang/account/__public/register.tsx +6 -0
  73. package/dist/templates/demo-store/app/routes/$lang/account/__public/reset.$id.$resetToken.tsx +5 -0
  74. package/dist/templates/demo-store/app/routes/$lang/account.tsx +1 -0
  75. package/dist/templates/demo-store/app/routes/$lang/api/countries.tsx +1 -0
  76. package/dist/templates/demo-store/app/routes/$lang/api/products.tsx +1 -0
  77. package/dist/templates/demo-store/app/routes/$lang/cart.tsx +1 -0
  78. package/dist/templates/demo-store/app/routes/$lang/collections/$collectionHandle.tsx +6 -0
  79. package/dist/templates/demo-store/app/routes/$lang/collections/all.tsx +1 -0
  80. package/dist/templates/demo-store/app/routes/$lang/collections/index.tsx +1 -0
  81. package/dist/templates/demo-store/app/routes/$lang/featured-products.tsx +1 -0
  82. package/dist/templates/demo-store/app/routes/$lang/index.tsx +7 -0
  83. package/dist/templates/demo-store/app/routes/$lang/journal/$journalHandle.tsx +7 -0
  84. package/dist/templates/demo-store/app/routes/$lang/journal/index.tsx +1 -0
  85. package/dist/templates/demo-store/app/routes/$lang/og-image.tsx +1 -0
  86. package/dist/templates/demo-store/app/routes/$lang/pages/$pageHandle.tsx +1 -0
  87. package/dist/templates/demo-store/app/routes/$lang/policies/$policyHandle.tsx +1 -0
  88. package/dist/templates/demo-store/app/routes/$lang/policies/index.tsx +1 -0
  89. package/dist/templates/demo-store/app/routes/$lang/products/$productHandle.tsx +6 -0
  90. package/dist/templates/demo-store/app/routes/$lang/products/index.tsx +1 -0
  91. package/dist/templates/demo-store/app/routes/$lang/search.tsx +6 -0
  92. package/dist/templates/demo-store/app/routes/[robots.txt].tsx +40 -0
  93. package/dist/templates/demo-store/app/routes/[sitemap.xml].tsx +198 -0
  94. package/dist/templates/demo-store/app/routes/account/__private/address/$id.tsx +320 -0
  95. package/dist/templates/demo-store/app/routes/account/__private/edit.tsx +273 -0
  96. package/dist/templates/demo-store/app/routes/account/__private/logout.ts +29 -0
  97. package/dist/templates/demo-store/app/routes/account/__private/orders.$id.tsx +324 -0
  98. package/dist/templates/demo-store/app/routes/account/__public/activate.$id.$activationToken.tsx +218 -0
  99. package/dist/templates/demo-store/app/routes/account/__public/login.tsx +197 -0
  100. package/dist/templates/demo-store/app/routes/account/__public/recover.tsx +144 -0
  101. package/dist/templates/demo-store/app/routes/account/__public/register.tsx +184 -0
  102. package/dist/templates/demo-store/app/routes/account/__public/reset.$id.$resetToken.tsx +214 -0
  103. package/dist/templates/demo-store/app/routes/account.tsx +191 -0
  104. package/dist/templates/demo-store/app/routes/api/countries.tsx +22 -0
  105. package/dist/templates/demo-store/app/routes/api/products.tsx +116 -0
  106. package/dist/templates/demo-store/app/routes/cart.tsx +498 -0
  107. package/dist/templates/demo-store/app/routes/collections/$collectionHandle.tsx +308 -0
  108. package/dist/templates/demo-store/app/routes/collections/all.tsx +5 -0
  109. package/dist/templates/demo-store/app/routes/collections/index.tsx +195 -0
  110. package/dist/templates/demo-store/app/routes/discounts.$code.tsx +60 -0
  111. package/dist/templates/demo-store/app/routes/featured-products.tsx +58 -0
  112. package/dist/templates/demo-store/app/routes/index.tsx +254 -0
  113. package/dist/templates/demo-store/app/routes/journal/$journalHandle.tsx +147 -0
  114. package/dist/templates/demo-store/app/routes/journal/index.tsx +150 -0
  115. package/dist/templates/demo-store/app/routes/og-image.tsx +19 -0
  116. package/dist/templates/demo-store/app/routes/pages/$pageHandle.tsx +82 -0
  117. package/dist/templates/demo-store/app/routes/policies/$policyHandle.tsx +117 -0
  118. package/dist/templates/demo-store/app/routes/policies/index.tsx +104 -0
  119. package/dist/templates/demo-store/app/routes/products/$productHandle.tsx +561 -0
  120. package/dist/templates/demo-store/app/routes/products/index.tsx +155 -0
  121. package/dist/templates/demo-store/app/routes/search.tsx +205 -0
  122. package/dist/templates/demo-store/app/styles/custom-font.css +13 -0
  123. package/dist/templates/demo-store/package-lock.json +25515 -0
  124. package/dist/templates/demo-store/package.json +67 -0
  125. package/dist/templates/demo-store/playwright.config.ts +109 -0
  126. package/dist/templates/demo-store/postcss.config.js +10 -0
  127. package/dist/templates/demo-store/public/favicon.svg +28 -0
  128. package/dist/templates/demo-store/public/fonts/IBMPlexSerif-Text.woff2 +0 -0
  129. package/dist/templates/demo-store/public/fonts/IBMPlexSerif-TextItalic.woff2 +0 -0
  130. package/dist/templates/demo-store/remix.config.js +12 -0
  131. package/dist/templates/demo-store/remix.env.d.ts +34 -0
  132. package/dist/templates/demo-store/remix.init/index.ts +15 -0
  133. package/dist/templates/demo-store/remix.init/package.json +7 -0
  134. package/dist/templates/demo-store/server.ts +87 -0
  135. package/dist/templates/demo-store/styles/app.css +182 -0
  136. package/dist/templates/demo-store/tailwind.config.js +70 -0
  137. package/dist/templates/demo-store/tests/cart.test.ts +70 -0
  138. package/dist/templates/demo-store/tests/seo.test.ts +36 -0
  139. package/dist/templates/demo-store/tests/utils.ts +100 -0
  140. package/dist/templates/demo-store/tsconfig.json +26 -0
  141. package/dist/templates/hello-world/.eslintignore +4 -0
  142. package/dist/templates/hello-world/.eslintrc.js +6 -0
  143. package/dist/templates/hello-world/.graphqlrc.yml +1 -0
  144. package/dist/templates/hello-world/.turbo/turbo-build.log +9 -0
  145. package/dist/templates/hello-world/README.md +20 -0
  146. package/dist/templates/hello-world/app/components/Layout.tsx +15 -0
  147. package/dist/templates/hello-world/app/components/index.ts +1 -0
  148. package/dist/templates/hello-world/app/entry.client.tsx +4 -0
  149. package/dist/templates/hello-world/app/entry.server.tsx +21 -0
  150. package/dist/templates/hello-world/app/root.tsx +212 -0
  151. package/dist/templates/hello-world/app/routes/index.tsx +7 -0
  152. package/dist/templates/hello-world/app/styles/app.css +38 -0
  153. package/dist/templates/hello-world/package-lock.json +27641 -0
  154. package/dist/templates/hello-world/package.json +41 -0
  155. package/dist/templates/hello-world/public/favicon.svg +28 -0
  156. package/dist/templates/hello-world/remix.env.d.ts +29 -0
  157. package/dist/templates/hello-world/server.ts +127 -0
  158. package/dist/templates/hello-world/tsconfig.json +25 -0
  159. package/dist/utils/config.js +81 -0
  160. package/dist/utils/flags.js +15 -0
  161. package/dist/utils/log.js +20 -0
  162. package/dist/utils/mini-oxygen.js +70 -0
  163. package/package.json +27 -64
  164. package/tmp-create-app.mjs +29 -0
  165. package/LICENSE +0 -8
  166. package/README.md +0 -61
  167. package/dist/cli/commands/hydrogen/add/eslint.d.ts +0 -11
  168. package/dist/cli/commands/hydrogen/add/eslint.js +0 -26
  169. package/dist/cli/commands/hydrogen/add/eslint.js.map +0 -1
  170. package/dist/cli/commands/hydrogen/add/tailwind.d.ts +0 -11
  171. package/dist/cli/commands/hydrogen/add/tailwind.js +0 -26
  172. package/dist/cli/commands/hydrogen/add/tailwind.js.map +0 -1
  173. package/dist/cli/commands/hydrogen/build.d.ts +0 -14
  174. package/dist/cli/commands/hydrogen/build.js +0 -49
  175. package/dist/cli/commands/hydrogen/build.js.map +0 -1
  176. package/dist/cli/commands/hydrogen/deploy.d.ts +0 -19
  177. package/dist/cli/commands/hydrogen/deploy.js +0 -58
  178. package/dist/cli/commands/hydrogen/deploy.js.map +0 -1
  179. package/dist/cli/commands/hydrogen/dev.d.ts +0 -13
  180. package/dist/cli/commands/hydrogen/dev.js +0 -31
  181. package/dist/cli/commands/hydrogen/dev.js.map +0 -1
  182. package/dist/cli/commands/hydrogen/info.d.ts +0 -12
  183. package/dist/cli/commands/hydrogen/info.js +0 -28
  184. package/dist/cli/commands/hydrogen/info.js.map +0 -1
  185. package/dist/cli/commands/hydrogen/preview.d.ts +0 -13
  186. package/dist/cli/commands/hydrogen/preview.js +0 -46
  187. package/dist/cli/commands/hydrogen/preview.js.map +0 -1
  188. package/dist/cli/constants.d.ts +0 -15
  189. package/dist/cli/constants.js +0 -16
  190. package/dist/cli/constants.js.map +0 -1
  191. package/dist/cli/flags.d.ts +0 -4
  192. package/dist/cli/flags.js +0 -16
  193. package/dist/cli/flags.js.map +0 -1
  194. package/dist/cli/models/hydrogen.d.ts +0 -22
  195. package/dist/cli/models/hydrogen.js +0 -82
  196. package/dist/cli/models/hydrogen.js.map +0 -1
  197. package/dist/cli/prompts/git-init.d.ts +0 -1
  198. package/dist/cli/prompts/git-init.js +0 -16
  199. package/dist/cli/prompts/git-init.js.map +0 -1
  200. package/dist/cli/services/build/check-lockfile.d.ts +0 -3
  201. package/dist/cli/services/build/check-lockfile.js +0 -80
  202. package/dist/cli/services/build/check-lockfile.js.map +0 -1
  203. package/dist/cli/services/build.d.ts +0 -14
  204. package/dist/cli/services/build.js +0 -44
  205. package/dist/cli/services/build.js.map +0 -1
  206. package/dist/cli/services/deploy/config.d.ts +0 -4
  207. package/dist/cli/services/deploy/config.js +0 -49
  208. package/dist/cli/services/deploy/config.js.map +0 -1
  209. package/dist/cli/services/deploy/error.d.ts +0 -4
  210. package/dist/cli/services/deploy/error.js +0 -11
  211. package/dist/cli/services/deploy/error.js.map +0 -1
  212. package/dist/cli/services/deploy/graphql/create_deployment.d.ts +0 -10
  213. package/dist/cli/services/deploy/graphql/create_deployment.js +0 -15
  214. package/dist/cli/services/deploy/graphql/create_deployment.js.map +0 -1
  215. package/dist/cli/services/deploy/graphql/upload_deployment.d.ts +0 -1
  216. package/dist/cli/services/deploy/graphql/upload_deployment.js +0 -16
  217. package/dist/cli/services/deploy/graphql/upload_deployment.js.map +0 -1
  218. package/dist/cli/services/deploy/types.d.ts +0 -37
  219. package/dist/cli/services/deploy/types.js +0 -2
  220. package/dist/cli/services/deploy/types.js.map +0 -1
  221. package/dist/cli/services/deploy/upload.d.ts +0 -5
  222. package/dist/cli/services/deploy/upload.js +0 -81
  223. package/dist/cli/services/deploy/upload.js.map +0 -1
  224. package/dist/cli/services/deploy.d.ts +0 -2
  225. package/dist/cli/services/deploy.js +0 -103
  226. package/dist/cli/services/deploy.js.map +0 -1
  227. package/dist/cli/services/dev/check-version.d.ts +0 -1
  228. package/dist/cli/services/dev/check-version.js +0 -30
  229. package/dist/cli/services/dev/check-version.js.map +0 -1
  230. package/dist/cli/services/dev.d.ts +0 -10
  231. package/dist/cli/services/dev.js +0 -36
  232. package/dist/cli/services/dev.js.map +0 -1
  233. package/dist/cli/services/eslint.d.ts +0 -8
  234. package/dist/cli/services/eslint.js +0 -74
  235. package/dist/cli/services/eslint.js.map +0 -1
  236. package/dist/cli/services/info.d.ts +0 -7
  237. package/dist/cli/services/info.js +0 -131
  238. package/dist/cli/services/info.js.map +0 -1
  239. package/dist/cli/services/preview.d.ts +0 -12
  240. package/dist/cli/services/preview.js +0 -63
  241. package/dist/cli/services/preview.js.map +0 -1
  242. package/dist/cli/services/tailwind.d.ts +0 -9
  243. package/dist/cli/services/tailwind.js +0 -103
  244. package/dist/cli/services/tailwind.js.map +0 -1
  245. package/dist/cli/utilities/load-config.d.ts +0 -5
  246. package/dist/cli/utilities/load-config.js +0 -6
  247. package/dist/cli/utilities/load-config.js.map +0 -1
  248. package/dist/tsconfig.tsbuildinfo +0 -1
  249. package/oclif.manifest.json +0 -1
@@ -0,0 +1,308 @@
1
+ import {
2
+ json,
3
+ type MetaFunction,
4
+ type SerializeFrom,
5
+ type LoaderArgs,
6
+ } from '@shopify/remix-oxygen';
7
+ import {RESOURCE_TYPES, notFoundMaybeRedirect} from '@shopify/hydrogen';
8
+ import {useLoaderData} from '@remix-run/react';
9
+ import type {
10
+ Collection as CollectionType,
11
+ CollectionConnection,
12
+ MetafieldReference,
13
+ Filter,
14
+ } from '@shopify/hydrogen-react/storefront-api-types';
15
+ import {flattenConnection} from '@shopify/hydrogen-react';
16
+ import invariant from 'tiny-invariant';
17
+ import {PageHeader, Section, Text, SortFilter, Breadcrumbs} from '~/components';
18
+ import {ProductGrid} from '~/components/ProductGrid';
19
+
20
+ import {PRODUCT_CARD_FRAGMENT} from '~/data';
21
+
22
+ const PAGINATION_SIZE = 48;
23
+
24
+ type VariantFilterParam = Record<string, string | boolean>;
25
+ type PriceFiltersQueryParam = Record<'price', {max?: number; min?: number}>;
26
+ type VariantOptionFiltersQueryParam = Record<
27
+ 'variantOption',
28
+ {name: string; value: string}
29
+ >;
30
+
31
+ export type AppliedFilter = {
32
+ label: string;
33
+ urlParam: {
34
+ key: string;
35
+ value: string;
36
+ };
37
+ };
38
+
39
+ type FiltersQueryParams = Array<
40
+ VariantFilterParam | PriceFiltersQueryParam | VariantOptionFiltersQueryParam
41
+ >;
42
+
43
+ export type SortParam =
44
+ | 'price-low-high'
45
+ | 'price-high-low'
46
+ | 'best-selling'
47
+ | 'newest'
48
+ | 'featured';
49
+
50
+ export const handle = {
51
+ hydrogen: {
52
+ resourceType: RESOURCE_TYPES.COLLECTION,
53
+ },
54
+ };
55
+
56
+ export async function loader({params, request, context}: LoaderArgs) {
57
+ const {collectionHandle} = params;
58
+
59
+ invariant(collectionHandle, 'Missing collectionHandle param');
60
+
61
+ const searchParams = new URL(request.url).searchParams;
62
+ const knownFilters = ['cursor', 'productVendor', 'productType'];
63
+ const available = 'available';
64
+ const variantOption = 'variantOption';
65
+ const {sortKey, reverse} = getSortValuesFromParam(
66
+ searchParams.get('sort') as SortParam,
67
+ );
68
+ const filters: FiltersQueryParams = [];
69
+ const appliedFilters: AppliedFilter[] = [];
70
+
71
+ for (const [key, value] of searchParams.entries()) {
72
+ if (available === key) {
73
+ filters.push({available: value === 'true'});
74
+ appliedFilters.push({
75
+ label: value === 'true' ? 'In stock' : 'Out of stock',
76
+ urlParam: {
77
+ key: available,
78
+ value,
79
+ },
80
+ });
81
+ } else if (knownFilters.includes(key)) {
82
+ filters.push({[key]: value});
83
+ appliedFilters.push({label: value, urlParam: {key, value}});
84
+ } else if (key.includes(variantOption)) {
85
+ const [name, val] = value.split(':');
86
+ filters.push({variantOption: {name, value: val}});
87
+ appliedFilters.push({label: val, urlParam: {key, value}});
88
+ }
89
+ }
90
+
91
+ // Builds min and max price filter since we can't stack them separately into
92
+ // the filters array. See price filters limitations:
93
+ // https://shopify.dev/custom-storefronts/products-collections/filter-products#limitations
94
+ if (searchParams.has('minPrice') || searchParams.has('maxPrice')) {
95
+ const price: {min?: number; max?: number} = {};
96
+ if (searchParams.has('minPrice')) {
97
+ price.min = Number(searchParams.get('minPrice')) || 0;
98
+ appliedFilters.push({
99
+ label: `Min: $${price.min}`,
100
+ urlParam: {key: 'minPrice', value: searchParams.get('minPrice')!},
101
+ });
102
+ }
103
+ if (searchParams.has('maxPrice')) {
104
+ price.max = Number(searchParams.get('maxPrice')) || 0;
105
+ appliedFilters.push({
106
+ label: `Max: $${price.max}`,
107
+ urlParam: {key: 'maxPrice', value: searchParams.get('maxPrice')!},
108
+ });
109
+ }
110
+ filters.push({
111
+ price,
112
+ });
113
+ }
114
+
115
+ const {collection, collections} = await context.storefront.query<{
116
+ collection: CollectionType;
117
+ collections: CollectionConnection;
118
+ }>(COLLECTION_QUERY, {
119
+ variables: {
120
+ handle: collectionHandle,
121
+ pageBy: PAGINATION_SIZE,
122
+ filters,
123
+ sortKey,
124
+ reverse,
125
+ country: context.storefront.i18n?.country,
126
+ language: context.storefront.i18n?.language,
127
+ },
128
+ });
129
+
130
+ if (!collection) {
131
+ throw await notFoundMaybeRedirect(request, context);
132
+ }
133
+
134
+ const collectionNodes = flattenConnection(collections);
135
+
136
+ return json({collection, appliedFilters, collections: collectionNodes});
137
+ }
138
+
139
+ export const meta: MetaFunction = ({
140
+ data,
141
+ }: {
142
+ data: SerializeFrom<typeof loader> | undefined;
143
+ }) => {
144
+ return {
145
+ title: data?.collection?.seo?.title ?? 'Collection',
146
+ description: data?.collection?.seo?.description,
147
+ };
148
+ };
149
+
150
+ export default function Collection() {
151
+ const {collection, collections, appliedFilters} =
152
+ useLoaderData<typeof loader>();
153
+ const breadcrumbs =
154
+ collection.metafield?.references &&
155
+ flattenConnection<MetafieldReference>(collection.metafield.references)
156
+ .reverse()
157
+ .reduce<any[]>((acc, collection) => [collection, ...acc], [collection]);
158
+
159
+ return (
160
+ <>
161
+ <PageHeader heading={collection.title}>
162
+ {collection?.description && (
163
+ <div className="flex items-baseline justify-between w-full">
164
+ <div>
165
+ <Text format width="narrow" as="p" className="inline-block">
166
+ {collection.description}
167
+ </Text>
168
+ </div>
169
+ </div>
170
+ )}
171
+
172
+ <Breadcrumbs breadcrumbs={breadcrumbs} />
173
+ </PageHeader>
174
+ <Section>
175
+ <SortFilter
176
+ filters={collection.products.filters as Filter[]}
177
+ appliedFilters={appliedFilters}
178
+ collections={collections as CollectionType[]}
179
+ >
180
+ <ProductGrid
181
+ key={collection.id}
182
+ collection={collection as CollectionType}
183
+ url={`/collections/${collection.handle}`}
184
+ data-test="product-grid"
185
+ />
186
+ </SortFilter>
187
+ </Section>
188
+ </>
189
+ );
190
+ }
191
+
192
+ const COLLECTION_QUERY = `#graphql
193
+ ${PRODUCT_CARD_FRAGMENT}
194
+ query CollectionDetails(
195
+ $handle: String!
196
+ $country: CountryCode
197
+ $language: LanguageCode
198
+ $pageBy: Int!
199
+ $cursor: String
200
+ $filters: [ProductFilter!]
201
+ $sortKey: ProductCollectionSortKeys!
202
+ $reverse: Boolean
203
+ ) @inContext(country: $country, language: $language) {
204
+ collection(handle: $handle) {
205
+ id
206
+ handle
207
+ title
208
+ description
209
+ seo {
210
+ description
211
+ title
212
+ }
213
+ image {
214
+ id
215
+ url
216
+ width
217
+ height
218
+ altText
219
+ }
220
+ metafield(namespace: "breadcrumbs", key: "parents") {
221
+ id
222
+ value
223
+ references(first: 10) {
224
+ edges {
225
+ node {
226
+ ... on Collection {
227
+ id
228
+ handle
229
+ title
230
+ }
231
+ }
232
+ }
233
+ }
234
+ namespace
235
+ key
236
+ }
237
+ products(
238
+ first: $pageBy,
239
+ after: $cursor,
240
+ filters: $filters,
241
+ sortKey: $sortKey,
242
+ reverse: $reverse
243
+ ) {
244
+ filters {
245
+ id
246
+ label
247
+ type
248
+ values {
249
+ id
250
+ label
251
+ count
252
+ input
253
+ }
254
+ }
255
+ nodes {
256
+ ...ProductCard
257
+ }
258
+ pageInfo {
259
+ hasNextPage
260
+ endCursor
261
+ }
262
+ }
263
+ }
264
+ collections(first: 100) {
265
+ edges {
266
+ node {
267
+ title
268
+ handle
269
+ }
270
+ }
271
+ }
272
+ }
273
+ `;
274
+
275
+ function getSortValuesFromParam(sortParam: SortParam | null) {
276
+ switch (sortParam) {
277
+ case 'price-high-low':
278
+ return {
279
+ sortKey: 'PRICE',
280
+ reverse: true,
281
+ };
282
+ case 'price-low-high':
283
+ return {
284
+ sortKey: 'PRICE',
285
+ reverse: false,
286
+ };
287
+ case 'best-selling':
288
+ return {
289
+ sortKey: 'BEST_SELLING',
290
+ reverse: false,
291
+ };
292
+ case 'newest':
293
+ return {
294
+ sortKey: 'CREATED',
295
+ reverse: true,
296
+ };
297
+ case 'featured':
298
+ return {
299
+ sortKey: 'MANUAL',
300
+ reverse: false,
301
+ };
302
+ default:
303
+ return {
304
+ sortKey: 'RELEVANCE',
305
+ reverse: false,
306
+ };
307
+ }
308
+ }
@@ -0,0 +1,5 @@
1
+ import {redirect, type LoaderArgs} from '@shopify/remix-oxygen';
2
+
3
+ export async function loader({params}: LoaderArgs) {
4
+ return redirect(params?.lang ? `${params.lang}/products` : '/products');
5
+ }
@@ -0,0 +1,195 @@
1
+ import {json, type MetaFunction, type LoaderArgs} from '@shopify/remix-oxygen';
2
+ import {RESOURCE_TYPES} from '@shopify/hydrogen';
3
+ import {useLoaderData} from '@remix-run/react';
4
+ import type {
5
+ Collection,
6
+ CollectionConnection,
7
+ } from '@shopify/hydrogen-react/storefront-api-types';
8
+ import {
9
+ Grid,
10
+ Heading,
11
+ PageHeader,
12
+ Section,
13
+ Link,
14
+ Pagination,
15
+ getPaginationVariables,
16
+ Button,
17
+ } from '~/components';
18
+ import {getImageLoadingPriority} from '~/lib/const';
19
+
20
+ const PAGINATION_SIZE = 8;
21
+
22
+ export const loader = async ({request, context: {storefront}}: LoaderArgs) => {
23
+ const variables = getPaginationVariables(request, PAGINATION_SIZE);
24
+ const {collections} = await storefront.query<{
25
+ collections: CollectionConnection;
26
+ }>(COLLECTIONS_QUERY, {
27
+ variables: {
28
+ ...variables,
29
+ country: storefront.i18n?.country,
30
+ language: storefront.i18n?.language,
31
+ },
32
+ });
33
+
34
+ return json({collections});
35
+ };
36
+
37
+ export const handle = {
38
+ hydrogen: {
39
+ resourceType: RESOURCE_TYPES.COLLECTIONS,
40
+ },
41
+ };
42
+
43
+ export const meta: MetaFunction = () => {
44
+ return {
45
+ title: 'All Collections',
46
+ };
47
+ };
48
+
49
+ export default function Collections() {
50
+ const {collections} = useLoaderData<typeof loader>();
51
+
52
+ return (
53
+ <>
54
+ <PageHeader heading="Collections" />
55
+ <Section>
56
+ <Pagination connection={collections}>
57
+ {({
58
+ endCursor,
59
+ hasNextPage,
60
+ hasPreviousPage,
61
+ nextPageUrl,
62
+ nodes,
63
+ prevPageUrl,
64
+ startCursor,
65
+ nextLinkRef,
66
+ isLoading,
67
+ }) => (
68
+ <>
69
+ {hasPreviousPage && (
70
+ <div className="flex items-center justify-center mt-6">
71
+ <Button
72
+ to={prevPageUrl}
73
+ variant="secondary"
74
+ width="full"
75
+ prefetch="intent"
76
+ disabled={!isLoading}
77
+ state={{
78
+ pageInfo: {
79
+ endCursor,
80
+ hasNextPage,
81
+ startCursor,
82
+ },
83
+ nodes,
84
+ }}
85
+ >
86
+ {isLoading ? 'Loading...' : 'Previous products'}
87
+ </Button>
88
+ </div>
89
+ )}
90
+ <Grid
91
+ items={nodes.length === 3 ? 3 : 2}
92
+ data-test="collection-grid"
93
+ >
94
+ {nodes.map((collection, i) => (
95
+ <CollectionCard
96
+ collection={collection as Collection}
97
+ key={collection.id}
98
+ loading={getImageLoadingPriority(i, 2)}
99
+ />
100
+ ))}
101
+ </Grid>
102
+ {hasNextPage && (
103
+ <div className="flex items-center justify-center mt-6">
104
+ <Button
105
+ ref={nextLinkRef}
106
+ to={nextPageUrl}
107
+ variant="secondary"
108
+ width="full"
109
+ prefetch="intent"
110
+ disabled={!isLoading}
111
+ state={{
112
+ pageInfo: {
113
+ endCursor,
114
+ hasPreviousPage,
115
+ startCursor,
116
+ },
117
+ nodes,
118
+ }}
119
+ >
120
+ {isLoading ? 'Loading...' : 'Next products'}
121
+ </Button>
122
+ </div>
123
+ )}
124
+ </>
125
+ )}
126
+ </Pagination>
127
+ </Section>
128
+ </>
129
+ );
130
+ }
131
+
132
+ function CollectionCard({
133
+ collection,
134
+ loading,
135
+ }: {
136
+ collection: Collection;
137
+ loading?: HTMLImageElement['loading'];
138
+ }) {
139
+ return (
140
+ <Link to={`/collections/${collection.handle}`} className="grid gap-4">
141
+ <div className="card-image bg-primary/5 aspect-[3/2]">
142
+ {collection?.image && (
143
+ <img
144
+ alt={collection.title}
145
+ src={collection.image.url}
146
+ height={400}
147
+ sizes="(max-width: 32em) 100vw, 33vw"
148
+ width={600}
149
+ loading={loading}
150
+ />
151
+ )}
152
+ </div>
153
+ <Heading as="h3" size="copy">
154
+ {collection.title}
155
+ </Heading>
156
+ </Link>
157
+ );
158
+ }
159
+
160
+ const COLLECTIONS_QUERY = `#graphql
161
+ query Collections(
162
+ $country: CountryCode
163
+ $language: LanguageCode
164
+ $first: Int
165
+ $last: Int
166
+ $startCursor: String
167
+ $endCursor: String
168
+ ) @inContext(country: $country, language: $language) {
169
+ collections(first: $first, last: $last, before: $startCursor, after: $endCursor) {
170
+ nodes {
171
+ id
172
+ title
173
+ description
174
+ handle
175
+ seo {
176
+ description
177
+ title
178
+ }
179
+ image {
180
+ id
181
+ url
182
+ width
183
+ height
184
+ altText
185
+ }
186
+ }
187
+ pageInfo {
188
+ hasPreviousPage
189
+ hasNextPage
190
+ startCursor
191
+ endCursor
192
+ }
193
+ }
194
+ }
195
+ `;
@@ -0,0 +1,60 @@
1
+ import {redirect, type LoaderArgs} from '@shopify/remix-oxygen';
2
+ import {cartCreate, cartDiscountCodesUpdate} from './cart';
3
+
4
+ /**
5
+ * Automatically applies a discount found on the url
6
+ * If a cart exists it's updated with the discount, otherwise a cart is created with the discount already applied
7
+ * @param ?redirect an optional path to return to otherwise return to the home page
8
+ * @example
9
+ * Example path applying a discount and redirecting
10
+ * ```ts
11
+ * /discounts/FREESHIPPING?redirect=/products
12
+ *
13
+ * ```
14
+ * @preserve
15
+ */
16
+ export async function loader({request, context, params}: LoaderArgs) {
17
+ const {storefront} = context;
18
+ // N.B. This route will probably be removed in the future.
19
+ const session = context.session as any;
20
+ const {code} = params;
21
+
22
+ const url = new URL(request.url);
23
+ const searchParams = new URLSearchParams(url.search);
24
+ const redirectUrl =
25
+ searchParams.get('redirect') || searchParams.get('return_to') || '/';
26
+
27
+ const headers = new Headers();
28
+
29
+ if (!code) {
30
+ return redirect(redirectUrl);
31
+ }
32
+
33
+ let cartId = await session.get('cartId');
34
+
35
+ //! if no existing cart, create one
36
+ if (!cartId) {
37
+ const {cart, errors: graphqlCartErrors} = await cartCreate({
38
+ input: {},
39
+ storefront,
40
+ });
41
+
42
+ if (graphqlCartErrors?.length) {
43
+ return redirect(redirectUrl);
44
+ }
45
+
46
+ //! cart created - we only need a Set-Cookie header if we're creating
47
+ cartId = cart.id;
48
+ session.set('cartId', cartId);
49
+ headers.set('Set-Cookie', await session.commit());
50
+ }
51
+
52
+ //! apply discount to the cart
53
+ await cartDiscountCodesUpdate({
54
+ cartId,
55
+ discountCodes: [code],
56
+ storefront,
57
+ });
58
+
59
+ return redirect(redirectUrl, {headers});
60
+ }
@@ -0,0 +1,58 @@
1
+ import {json, type LoaderArgs} from '@shopify/remix-oxygen';
2
+ import {flattenConnection} from '@shopify/hydrogen-react';
3
+ import type {
4
+ CollectionConnection,
5
+ ProductConnection,
6
+ } from '@shopify/hydrogen-react/storefront-api-types';
7
+ import invariant from 'tiny-invariant';
8
+ import {PRODUCT_CARD_FRAGMENT} from '~/data';
9
+
10
+ export async function loader({context: {storefront}}: LoaderArgs) {
11
+ return json(await getFeaturedData(storefront));
12
+ }
13
+
14
+ export async function getFeaturedData(
15
+ storefront: LoaderArgs['context']['storefront'],
16
+ ) {
17
+ const data = await storefront.query<{
18
+ featuredCollections: CollectionConnection;
19
+ featuredProducts: ProductConnection;
20
+ }>(FEATURED_QUERY, {
21
+ variables: {
22
+ country: storefront.i18n?.country,
23
+ language: storefront.i18n?.language,
24
+ },
25
+ });
26
+
27
+ invariant(data, 'No data returned from Shopify API');
28
+
29
+ return {
30
+ featuredCollections: flattenConnection(data.featuredCollections),
31
+ featuredProducts: flattenConnection(data.featuredProducts),
32
+ };
33
+ }
34
+
35
+ const FEATURED_QUERY = `#graphql
36
+ ${PRODUCT_CARD_FRAGMENT}
37
+ query homepage($country: CountryCode, $language: LanguageCode)
38
+ @inContext(country: $country, language: $language) {
39
+ featuredCollections: collections(first: 3, sortKey: UPDATED_AT) {
40
+ nodes {
41
+ id
42
+ title
43
+ handle
44
+ image {
45
+ altText
46
+ width
47
+ height
48
+ url
49
+ }
50
+ }
51
+ }
52
+ featuredProducts: products(first: 12) {
53
+ nodes {
54
+ ...ProductCard
55
+ }
56
+ }
57
+ }
58
+ `;