@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,277 @@
1
+ import {useEffect, useMemo, useState} from 'react';
2
+ import type {
3
+ Maybe,
4
+ PageInfo,
5
+ ProductConnection,
6
+ } from '@shopify/hydrogen-react/storefront-api-types';
7
+
8
+ import {useInView, type IntersectionOptions} from 'react-intersection-observer';
9
+ import {useTransition, useLocation, useNavigate} from '@remix-run/react';
10
+
11
+ type Connection = {
12
+ nodes: ProductConnection['nodes'] | any[];
13
+ pageInfo: PageInfo;
14
+ };
15
+
16
+ type PaginationState = {
17
+ nodes: ProductConnection['nodes'] | any[];
18
+ pageInfo: PageInfo | null;
19
+ };
20
+
21
+ type Props<Resource extends Connection> = {
22
+ connection: Resource;
23
+ autoLoadOnScroll?: boolean | IntersectionOptions;
24
+ };
25
+
26
+ interface PaginationInfo {
27
+ endCursor: Maybe<string> | undefined;
28
+ hasNextPage: boolean;
29
+ hasPreviousPage: boolean;
30
+ isLoading: boolean;
31
+ nextLinkRef: any;
32
+ nextPageUrl: string;
33
+ nodes: ProductConnection['nodes'] | any[];
34
+ prevPageUrl: string;
35
+ startCursor: Maybe<string> | undefined;
36
+ }
37
+
38
+ export function Pagination<Resource extends Connection>({
39
+ connection,
40
+ children = () => null,
41
+ autoLoadOnScroll = true,
42
+ }: Props<Resource> & {
43
+ children: ({
44
+ endCursor,
45
+ hasNextPage,
46
+ hasPreviousPage,
47
+ isLoading,
48
+ nextPageUrl,
49
+ nodes,
50
+ prevPageUrl,
51
+ startCursor,
52
+ }: PaginationInfo) => JSX.Element | null;
53
+ }) {
54
+ const transition = useTransition();
55
+ const isLoading = transition.state === 'loading';
56
+ const autoScrollEnabled = Boolean(autoLoadOnScroll);
57
+ const autoScrollConfig = (
58
+ autoScrollEnabled
59
+ ? autoLoadOnScroll
60
+ : {
61
+ threshold: 0,
62
+ rootMargin: '1000px 0px 0px 0px',
63
+ }
64
+ ) as IntersectionOptions;
65
+ const {ref: nextLinkRef, inView} = useInView(autoScrollConfig);
66
+ const {
67
+ endCursor,
68
+ hasNextPage,
69
+ hasPreviousPage,
70
+ nextPageUrl,
71
+ nodes,
72
+ prevPageUrl,
73
+ startCursor,
74
+ } = usePagination(connection);
75
+
76
+ // auto load next page if in view
77
+ useLoadMoreWhenInView({
78
+ disabled: !autoScrollEnabled,
79
+ connection: {
80
+ pageInfo: {startCursor, endCursor, hasPreviousPage, hasNextPage},
81
+ nodes,
82
+ },
83
+ inView,
84
+ isLoading,
85
+ });
86
+
87
+ return children({
88
+ endCursor,
89
+ hasNextPage,
90
+ hasPreviousPage,
91
+ isLoading,
92
+ nextLinkRef,
93
+ nextPageUrl,
94
+ nodes,
95
+ prevPageUrl,
96
+ startCursor,
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Get cumulative pagination logic for a given connection
102
+ */
103
+ export function usePagination(
104
+ connection: Connection,
105
+ ): Omit<PaginationInfo, 'isLoading' | 'nextLinkRef'> {
106
+ const [nodes, setNodes] = useState(connection.nodes);
107
+ const {state, search} = useLocation() as {
108
+ state: PaginationState;
109
+ search: string;
110
+ };
111
+ const params = new URLSearchParams(search);
112
+ const direction = params.get('direction');
113
+ const isPrevious = direction === 'previous';
114
+
115
+ const {hasNextPage, hasPreviousPage, startCursor, endCursor} =
116
+ connection.pageInfo;
117
+
118
+ const currentPageInfo = useMemo(() => {
119
+ let pageStartCursor =
120
+ state?.pageInfo?.startCursor === undefined
121
+ ? startCursor
122
+ : state.pageInfo.startCursor;
123
+
124
+ let pageEndCursor =
125
+ state?.pageInfo?.endCursor === undefined
126
+ ? endCursor
127
+ : state.pageInfo.endCursor;
128
+
129
+ if (state?.nodes) {
130
+ if (isPrevious) {
131
+ pageStartCursor = startCursor;
132
+ } else {
133
+ pageEndCursor = endCursor;
134
+ }
135
+ }
136
+
137
+ const previousPageExists =
138
+ state?.pageInfo?.hasPreviousPage === undefined
139
+ ? hasPreviousPage
140
+ : state.pageInfo.hasPreviousPage;
141
+
142
+ const nextPageExists =
143
+ state?.pageInfo?.hasNextPage === undefined
144
+ ? hasNextPage
145
+ : state.pageInfo.hasNextPage;
146
+
147
+ return {
148
+ startCursor: pageStartCursor,
149
+ endCursor: pageEndCursor,
150
+ hasPreviousPage: previousPageExists,
151
+ hasNextPage: nextPageExists,
152
+ };
153
+ }, [isPrevious, state, hasNextPage, hasPreviousPage, startCursor, endCursor]);
154
+
155
+ const prevPageUrl = useMemo(() => {
156
+ const params = new URLSearchParams(search);
157
+ params.set('direction', 'previous');
158
+ currentPageInfo.startCursor &&
159
+ params.set('cursor', currentPageInfo.startCursor);
160
+ return `?${params.toString()}`;
161
+ }, [search, currentPageInfo.startCursor]);
162
+
163
+ const nextPageUrl = useMemo(() => {
164
+ const params = new URLSearchParams(search);
165
+ params.set('direction', 'next');
166
+ currentPageInfo.endCursor &&
167
+ params.set('cursor', currentPageInfo.endCursor);
168
+ return `?${params.toString()}`;
169
+ }, [search, currentPageInfo.endCursor]);
170
+
171
+ // the only way to prevent hydration mismatches
172
+ useEffect(() => {
173
+ if (!state || !state?.nodes) {
174
+ setNodes(connection.nodes);
175
+ return;
176
+ }
177
+
178
+ if (isPrevious) {
179
+ setNodes([...connection.nodes, ...state.nodes]);
180
+ } else {
181
+ setNodes([...state.nodes, ...connection.nodes]);
182
+ }
183
+ }, [state, isPrevious, connection.nodes]);
184
+
185
+ return {...currentPageInfo, prevPageUrl, nextPageUrl, nodes};
186
+ }
187
+
188
+ /**
189
+ * Auto load the next pagination page when in view and autoLoadOnScroll is true
190
+ * @param disabled disable auto loading
191
+ * @param inView trigger element is in viewport
192
+ * @param isIdle page transition is idle
193
+ * @param connection Storefront API connection
194
+ */
195
+ function useLoadMoreWhenInView<Resource extends Connection>({
196
+ disabled,
197
+ inView,
198
+ isLoading,
199
+ connection,
200
+ }: Pick<Props<Resource>, 'autoLoadOnScroll' | 'connection'> & {
201
+ disabled: boolean;
202
+ inView: boolean;
203
+ isLoading: boolean;
204
+ }) {
205
+ const location = useLocation();
206
+ const navigate = useNavigate();
207
+
208
+ const {
209
+ pageInfo: {startCursor, endCursor, hasPreviousPage, hasNextPage},
210
+ nodes,
211
+ } = connection;
212
+
213
+ // load next when in view and autoLoadOnScroll
214
+ useEffect(() => {
215
+ if (!inView) return;
216
+ if (!hasNextPage) return;
217
+ if (!endCursor) return;
218
+ if (disabled) return;
219
+ if (isLoading) return;
220
+
221
+ const nextPageUrl =
222
+ location.pathname + `?index&cursor=${endCursor}&direction=next`;
223
+
224
+ navigate(nextPageUrl, {
225
+ state: {
226
+ pageInfo: {
227
+ endCursor,
228
+ hasPreviousPage,
229
+ startCursor,
230
+ },
231
+ nodes,
232
+ },
233
+ });
234
+ }, [
235
+ disabled,
236
+ endCursor,
237
+ hasNextPage,
238
+ hasPreviousPage,
239
+ inView,
240
+ isLoading,
241
+ nodes,
242
+ location.pathname,
243
+ navigate,
244
+ startCursor,
245
+ ]);
246
+ }
247
+
248
+ /**
249
+ * Get variables for route loader to support pagination
250
+ * @param autoLoadOnScroll enable auto loading
251
+ * @param inView trigger element is in viewport
252
+ * @param isIdle page transition is idle
253
+ * @param connection Storefront API connection
254
+ * @returns cumulativePageInfo {startCursor, endCursor, hasPreviousPage, hasNextPage}
255
+ */
256
+ export function getPaginationVariables(request: Request, pageBy: number) {
257
+ const searchParams = new URLSearchParams(new URL(request.url).search);
258
+
259
+ const cursor = searchParams.get('cursor') ?? undefined;
260
+ const direction =
261
+ searchParams.get('direction') === 'previous' ? 'previous' : 'next';
262
+ const isPrevious = direction === 'previous';
263
+
264
+ const prevPage = {
265
+ last: pageBy,
266
+ startCursor: cursor ?? null,
267
+ };
268
+
269
+ const nextPage = {
270
+ first: pageBy,
271
+ endCursor: cursor ?? null,
272
+ };
273
+
274
+ const variables = isPrevious ? prevPage : nextPage;
275
+
276
+ return variables;
277
+ }
@@ -0,0 +1,146 @@
1
+ import clsx from 'clsx';
2
+ import {
3
+ flattenConnection,
4
+ Image,
5
+ Money,
6
+ useMoney,
7
+ } from '@shopify/hydrogen-react';
8
+ import type {SerializeFrom} from '@shopify/remix-oxygen';
9
+ import {Text, Link, Button, AddToCartButton} from '~/components';
10
+ import {isDiscounted, isNewArrival} from '~/lib/utils';
11
+ import {getProductPlaceholder} from '~/lib/placeholders';
12
+ import type {
13
+ CartLineInput,
14
+ MoneyV2,
15
+ Product,
16
+ ProductVariant,
17
+ ProductVariantConnection,
18
+ } from '@shopify/hydrogen-react/storefront-api-types';
19
+ import {useFetcher, useMatches} from '@remix-run/react';
20
+
21
+ export function ProductCard({
22
+ product,
23
+ label,
24
+ className,
25
+ loading,
26
+ onClick,
27
+ }: {
28
+ product: SerializeFrom<Product>;
29
+ label?: string;
30
+ className?: string;
31
+ loading?: HTMLImageElement['loading'];
32
+ onClick?: () => void;
33
+ }) {
34
+ let cardLabel;
35
+
36
+ const cartProduct = product?.variants ? product : getProductPlaceholder();
37
+ if (!cartProduct?.variants?.nodes?.length) return null;
38
+
39
+ const firstVariant = flattenConnection<ProductVariant>(
40
+ cartProduct?.variants as ProductVariantConnection,
41
+ )[0];
42
+
43
+ if (!firstVariant) return null;
44
+ const {image, price, compareAtPrice} = firstVariant;
45
+
46
+ if (label) {
47
+ cardLabel = label;
48
+ } else if (isDiscounted(price as MoneyV2, compareAtPrice as MoneyV2)) {
49
+ cardLabel = 'Sale';
50
+ } else if (isNewArrival(product.publishedAt)) {
51
+ cardLabel = 'New';
52
+ }
53
+
54
+ return (
55
+ <div className="flex flex-col">
56
+ <Link
57
+ onClick={onClick}
58
+ to={`/products/${product.handle}`}
59
+ prefetch="intent"
60
+ >
61
+ <div className={clsx('grid gap-6', className)}>
62
+ <div className="card-image aspect-[4/5] bg-primary/5">
63
+ {image && (
64
+ <Image
65
+ className="aspect-[4/5] w-full object-cover fadeIn"
66
+ widths={[320]}
67
+ sizes="320px"
68
+ loaderOptions={{
69
+ crop: 'center',
70
+ scale: 2,
71
+ width: 320,
72
+ height: 400,
73
+ }}
74
+ data={image}
75
+ alt={image.altText || `Picture of ${product.title}`}
76
+ loading={loading}
77
+ />
78
+ )}
79
+ <Text
80
+ as="label"
81
+ size="fine"
82
+ className="absolute top-0 right-0 m-4 text-right text-notice"
83
+ >
84
+ {cardLabel}
85
+ </Text>
86
+ </div>
87
+ <div className="grid gap-1">
88
+ <Text
89
+ className="w-full overflow-hidden whitespace-nowrap text-ellipsis "
90
+ as="h3"
91
+ >
92
+ {product.title}
93
+ </Text>
94
+ <div className="flex gap-4">
95
+ <Text className="flex gap-4">
96
+ <Money withoutTrailingZeros data={price!} />
97
+ {isDiscounted(price as MoneyV2, compareAtPrice as MoneyV2) && (
98
+ <CompareAtPrice
99
+ className={'opacity-50'}
100
+ data={compareAtPrice as MoneyV2}
101
+ />
102
+ )}
103
+ </Text>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </Link>
108
+ {firstVariant?.id && (
109
+ <AddToCartButton
110
+ lines={[
111
+ {
112
+ quantity: 1,
113
+ merchandiseId: firstVariant.id,
114
+ },
115
+ ]}
116
+ variant="secondary"
117
+ className="mt-2"
118
+ >
119
+ <Text as="span" className="flex items-center justify-center gap-2">
120
+ Add to Bag
121
+ </Text>
122
+ </AddToCartButton>
123
+ )}
124
+ </div>
125
+ );
126
+ }
127
+
128
+ function CompareAtPrice({
129
+ data,
130
+ className,
131
+ }: {
132
+ data: MoneyV2;
133
+ className?: string;
134
+ }) {
135
+ const {currencyNarrowSymbol, withoutTrailingZerosAndCurrency} =
136
+ useMoney(data);
137
+
138
+ const styles = clsx('strike', className);
139
+
140
+ return (
141
+ <span className={styles}>
142
+ {currencyNarrowSymbol}
143
+ {withoutTrailingZerosAndCurrency}
144
+ </span>
145
+ );
146
+ }
@@ -0,0 +1,114 @@
1
+ import type {MediaEdge} from '@shopify/hydrogen-react/storefront-api-types';
2
+ import {ATTR_LOADING_EAGER} from '~/lib/const';
3
+ import type {MediaImage} from '@shopify/hydrogen-react/storefront-api-types';
4
+
5
+ /**
6
+ * A client component that defines a media gallery for hosting images, 3D models, and videos of products
7
+ */
8
+ export function ProductGallery({
9
+ media,
10
+ className,
11
+ }: {
12
+ media: MediaEdge['node'][];
13
+ className?: string;
14
+ }) {
15
+ if (!media.length) {
16
+ return null;
17
+ }
18
+
19
+ return (
20
+ <div
21
+ className={`swimlane md:grid-flow-row hiddenScroll md:p-0 md:overflow-x-auto md:grid-cols-2 ${className}`}
22
+ >
23
+ {media.map((med, i) => {
24
+ let mediaProps: Record<string, any> = {};
25
+ const isFirst = i === 0;
26
+ const isFourth = i === 3;
27
+ const isFullWidth = i % 3 === 0;
28
+
29
+ const data = {
30
+ ...med,
31
+ image: {
32
+ // @ts-ignore
33
+ ...med.image,
34
+ altText: med.alt || 'Product image',
35
+ },
36
+ } as MediaImage;
37
+
38
+ switch (med.mediaContentType) {
39
+ case 'IMAGE':
40
+ mediaProps = {
41
+ width: 800,
42
+ widths: [400, 800, 1200, 1600, 2000, 2400],
43
+ };
44
+ break;
45
+ case 'VIDEO':
46
+ mediaProps = {
47
+ width: '100%',
48
+ autoPlay: true,
49
+ controls: false,
50
+ muted: true,
51
+ loop: true,
52
+ preload: 'auto',
53
+ };
54
+ break;
55
+ case 'EXTERNAL_VIDEO':
56
+ mediaProps = {width: '100%'};
57
+ break;
58
+ case 'MODEL_3D':
59
+ mediaProps = {
60
+ width: '100%',
61
+ interactionPromptThreshold: '0',
62
+ ar: true,
63
+ loading: ATTR_LOADING_EAGER,
64
+ disableZoom: true,
65
+ };
66
+ break;
67
+ }
68
+
69
+ if (i === 0 && med.mediaContentType === 'IMAGE') {
70
+ mediaProps.loading = ATTR_LOADING_EAGER;
71
+ }
72
+
73
+ const style = [
74
+ isFullWidth ? 'md:col-span-2' : 'md:col-span-1',
75
+ isFirst || isFourth ? '' : 'md:aspect-[4/5]',
76
+ 'aspect-square snap-center card-image bg-white dark:bg-contrast/10 w-mobileGallery md:w-full',
77
+ ].join(' ');
78
+
79
+ return (
80
+ <div
81
+ className={style}
82
+ // @ts-ignore
83
+ key={med.id || med.image.id}
84
+ >
85
+ {/* TODO: Replace with MediaFile when it's available */}
86
+ {(med as MediaImage).image && (
87
+ <img
88
+ src={data.image!.url}
89
+ alt={data.image!.altText!}
90
+ className="w-full h-full aspect-square fadeIn object-cover"
91
+ />
92
+ )}
93
+ {/* <MediaFile
94
+ tabIndex="0"
95
+ className={`w-full h-full aspect-square fadeIn object-cover`}
96
+ data={data}
97
+ sizes={
98
+ isFullWidth
99
+ ? '(min-width: 64em) 60vw, (min-width: 48em) 50vw, 90vw'
100
+ : '(min-width: 64em) 30vw, (min-width: 48em) 25vw, 90vw'
101
+ }
102
+ // @ts-ignore
103
+ options={{
104
+ crop: 'center',
105
+ scale: 2,
106
+ }}
107
+ {...mediaProps}
108
+ /> */}
109
+ </div>
110
+ );
111
+ })}
112
+ </div>
113
+ );
114
+ }
@@ -0,0 +1,93 @@
1
+ import {Button, Grid, ProductCard, Link} from '~/components';
2
+ import {getImageLoadingPriority} from '~/lib/const';
3
+ import type {
4
+ Collection,
5
+ Product,
6
+ } from '@shopify/hydrogen-react/storefront-api-types';
7
+ import {useFetcher} from '@remix-run/react';
8
+ import {useEffect, useState} from 'react';
9
+
10
+ export function ProductGrid({
11
+ url,
12
+ collection,
13
+ ...props
14
+ }: {
15
+ url: string;
16
+ collection: Collection;
17
+ }) {
18
+ const [initialProducts, setInitialProducts] = useState(
19
+ collection?.products?.nodes || [],
20
+ );
21
+
22
+ const [nextPage, setNextPage] = useState(
23
+ collection?.products?.pageInfo?.hasNextPage,
24
+ );
25
+ const [endCursor, setEndCursor] = useState(
26
+ collection?.products?.pageInfo?.endCursor,
27
+ );
28
+ const [products, setProducts] = useState(initialProducts);
29
+
30
+ // props have changes, reset component state
31
+ const productProps = collection?.products?.nodes || [];
32
+ if (initialProducts !== productProps) {
33
+ setInitialProducts(productProps);
34
+ setProducts(productProps);
35
+ }
36
+
37
+ const fetcher = useFetcher();
38
+
39
+ function fetchMoreProducts() {
40
+ fetcher.load(`${url}?index&cursor=${endCursor}`);
41
+ }
42
+
43
+ useEffect(() => {
44
+ if (!fetcher.data) return;
45
+
46
+ const {collection} = fetcher.data;
47
+
48
+ setProducts((prev: Product[]) => [...prev, ...collection.products.nodes]);
49
+ setNextPage(collection.products.pageInfo.hasNextPage);
50
+ setEndCursor(collection.products.pageInfo.endCursor);
51
+ }, [fetcher.data]);
52
+
53
+ const haveProducts = initialProducts.length > 0;
54
+
55
+ if (!haveProducts) {
56
+ return (
57
+ <>
58
+ <p>No products found on this collection</p>
59
+ <Link to="/products">
60
+ <p className="underline">Browse catalog</p>
61
+ </Link>
62
+ </>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <>
68
+ <Grid layout="products" {...props}>
69
+ {products.map((product, i) => (
70
+ <ProductCard
71
+ key={product.id}
72
+ product={product}
73
+ loading={getImageLoadingPriority(i)}
74
+ />
75
+ ))}
76
+ </Grid>
77
+
78
+ {nextPage && (
79
+ <div className="flex items-center justify-center mt-6">
80
+ <Button
81
+ disabled={fetcher.state !== 'idle'}
82
+ variant="secondary"
83
+ onClick={fetchMoreProducts}
84
+ width="full"
85
+ prefetch="intent"
86
+ >
87
+ {fetcher.state !== 'idle' ? 'Loading...' : 'Load more products'}
88
+ </Button>
89
+ </div>
90
+ )}
91
+ </>
92
+ );
93
+ }
@@ -0,0 +1,30 @@
1
+ import type {SerializeFrom} from '@shopify/remix-oxygen';
2
+ import type {Product} from '@shopify/hydrogen-react/storefront-api-types';
3
+ import {ProductCard, Section} from '~/components';
4
+
5
+ const mockProducts = new Array(12).fill('');
6
+
7
+ export function ProductSwimlane({
8
+ title = 'Featured Products',
9
+ products = mockProducts,
10
+ count = 12,
11
+ ...props
12
+ }: {
13
+ title?: string;
14
+ products?: SerializeFrom<Product[]>;
15
+ count?: number;
16
+ }) {
17
+ return (
18
+ <Section heading={title} padding="y" {...props}>
19
+ <div className="swimlane hiddenScroll md:pb-8 md:scroll-px-8 lg:scroll-px-12 md:px-8 lg:px-12">
20
+ {products.map((product) => (
21
+ <ProductCard
22
+ product={product}
23
+ key={product.id}
24
+ className="snap-start w-80"
25
+ />
26
+ ))}
27
+ </div>
28
+ </Section>
29
+ );
30
+ }
@@ -0,0 +1,24 @@
1
+ import clsx from 'clsx';
2
+
3
+ /**
4
+ * A shared component and Suspense call that's used in `App.server.jsx` to let your app wait for code to load while declaring a loading state
5
+ */
6
+ export function Skeleton({
7
+ as: Component = 'div',
8
+ width,
9
+ height,
10
+ className,
11
+ ...props
12
+ }: {
13
+ as?: React.ElementType;
14
+ width?: string;
15
+ height?: string;
16
+ className?: string;
17
+ [key: string]: any;
18
+ }) {
19
+ const styles = clsx('rounded bg-primary/10', className);
20
+
21
+ return (
22
+ <Component {...props} width={width} height={height} className={styles} />
23
+ );
24
+ }